mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 13:02:37 -05:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2ff06dfcb | ||
|
|
74f0fb1731 | ||
|
|
479d27cd49 |
+9
@@ -12,6 +12,9 @@ use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::build::build_fal_flux_1_dev;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::build::build_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::build::build_fal_gpt_image_1;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::build::build_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::build::build_fal_gpt_image_2;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::build::build_fal_nano_banana_2;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::build::build_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::plan_generate_image_artcraft_flux_1_dev;
|
||||
@@ -99,6 +102,9 @@ impl GenerateImageRequestBuilder {
|
||||
match (self.provider, self.model) {
|
||||
(Provider::Fal, CommonImageModel::Flux1Dev) => true,
|
||||
(Provider::Fal, CommonImageModel::Flux1Schnell) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage1) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage1p5) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage2) => true,
|
||||
(Provider::Fal, CommonImageModel::NanoBanana2) => true,
|
||||
(Provider::Fal, CommonImageModel::NanoBananaPro) => true,
|
||||
_ => false,
|
||||
@@ -109,6 +115,9 @@ impl GenerateImageRequestBuilder {
|
||||
match (self.provider, self.model) {
|
||||
(Provider::Fal, CommonImageModel::Flux1Dev) => build_fal_flux_1_dev(self),
|
||||
(Provider::Fal, CommonImageModel::Flux1Schnell) => build_fal_flux_1_schnell(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage1) => build_fal_gpt_image_1(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage1p5) => build_fal_gpt_image_1p5(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage2) => build_fal_gpt_image_2(self),
|
||||
(Provider::Fal, CommonImageModel::NanoBanana2) => build_fal_nano_banana_2(self),
|
||||
(Provider::Fal, CommonImageModel::NanoBananaPro) => build_fal_nano_banana_pro(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
|
||||
+33
@@ -7,6 +7,12 @@ use crate::generate::generate_image_v2::providers::fal::flux_1_dev::cost::FalFlu
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::cost::FalFlux1SchnellCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::cost::FalGptImage1CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::cost::FalGptImage1p5CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::cost::FalGptImage2CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::cost::FalNanoBanana2CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::cost::FalNanoBananaProCostState;
|
||||
@@ -16,6 +22,9 @@ use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::request
|
||||
pub enum ImageGenerationRequest {
|
||||
FalFlux1Dev(FalFlux1DevRequestState),
|
||||
FalFlux1Schnell(FalFlux1SchnellRequestState),
|
||||
FalGptImage1(FalGptImage1RequestState),
|
||||
FalGptImage1p5(FalGptImage1p5RequestState),
|
||||
FalGptImage2(FalGptImage2RequestState),
|
||||
FalNanoBanana2(FalNanoBanana2RequestState),
|
||||
FalNanoBananaPro(FalNanoBananaProRequestState),
|
||||
}
|
||||
@@ -25,6 +34,9 @@ impl ImageGenerationRequest {
|
||||
match self {
|
||||
Self::FalFlux1Dev(_) => Provider::Fal,
|
||||
Self::FalFlux1Schnell(_) => Provider::Fal,
|
||||
Self::FalGptImage1(_) => Provider::Fal,
|
||||
Self::FalGptImage1p5(_) => Provider::Fal,
|
||||
Self::FalGptImage2(_) => Provider::Fal,
|
||||
Self::FalNanoBanana2(_) => Provider::Fal,
|
||||
Self::FalNanoBananaPro(_) => Provider::Fal,
|
||||
}
|
||||
@@ -38,6 +50,15 @@ impl ImageGenerationRequest {
|
||||
Self::FalFlux1Schnell(request) => {
|
||||
Ok(FalFlux1SchnellCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage1(request) => {
|
||||
Ok(FalGptImage1CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage1p5(request) => {
|
||||
Ok(FalGptImage1p5CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage2(request) => {
|
||||
Ok(FalGptImage2CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalNanoBanana2(request) => {
|
||||
Ok(FalNanoBanana2CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
@@ -57,6 +78,18 @@ impl ImageGenerationRequest {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalGptImage1(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalGptImage1p5(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalGptImage2(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalNanoBanana2(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::{
|
||||
GptImage1EditImageNumImages, GptImage1EditImageQuality, GptImage1EditImageRequest,
|
||||
GptImage1EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::{
|
||||
GptImage1TextToImageNumImages, GptImage1TextToImageQuality,
|
||||
GptImage1TextToImageRequest, GptImage1TextToImageSize,
|
||||
};
|
||||
|
||||
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_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
|
||||
pub fn build_fal_gpt_image_1(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let prompt = builder.prompt.clone().unwrap_or_default();
|
||||
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, builder.request_mismatch_mitigation_strategy)?;
|
||||
let image_size = plan_image_size(builder.aspect_ratio);
|
||||
let quality = plan_quality(builder.quality);
|
||||
|
||||
let state = if image_urls.is_empty() {
|
||||
FalGptImage1RequestState::TextToImage(GptImage1TextToImageRequest {
|
||||
prompt,
|
||||
num_images: to_t2i_num_images(num_images),
|
||||
image_size: image_size.map(to_t2i_image_size),
|
||||
quality: Some(to_t2i_quality(quality)),
|
||||
background: None,
|
||||
output_format: None,
|
||||
})
|
||||
} else {
|
||||
FalGptImage1RequestState::EditImage(GptImage1EditImageRequest {
|
||||
prompt,
|
||||
image_urls,
|
||||
num_images: to_edit_num_images(num_images),
|
||||
mask_image_url: None,
|
||||
image_size: image_size.map(to_edit_image_size),
|
||||
quality: Some(to_edit_quality(quality)),
|
||||
input_fidelity: None,
|
||||
background: None,
|
||||
output_format: None,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1(state),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedImageSize {
|
||||
Square,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<PlannedNumImages, ArtcraftRouterError> {
|
||||
let count = count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(PlannedNumImages::One),
|
||||
2 => Ok(PlannedNumImages::Two),
|
||||
3 => Ok(PlannedNumImages::Three),
|
||||
4 => Ok(PlannedNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(PlannedNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> PlannedQuality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => PlannedQuality::Low,
|
||||
Some(CommonQuality::Medium) => PlannedQuality::Medium,
|
||||
Some(CommonQuality::High) | None => PlannedQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<PlannedImageSize> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Some(PlannedImageSize::Square)
|
||||
}
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(PlannedImageSize::Horizontal),
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(PlannedImageSize::Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: PlannedNumImages) -> GptImage1TextToImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage1TextToImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage1TextToImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage1TextToImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage1TextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: PlannedNumImages) -> GptImage1EditImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage1EditImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage1EditImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage1EditImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage1EditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_quality(q: PlannedQuality) -> GptImage1TextToImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage1TextToImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage1TextToImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage1TextToImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_quality(q: PlannedQuality) -> GptImage1EditImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage1EditImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage1EditImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage1EditImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_image_size(s: PlannedImageSize) -> GptImage1TextToImageSize {
|
||||
match s {
|
||||
PlannedImageSize::Square => GptImage1TextToImageSize::Square,
|
||||
PlannedImageSize::Horizontal => GptImage1TextToImageSize::Horizontal,
|
||||
PlannedImageSize::Vertical => GptImage1TextToImageSize::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_image_size(s: PlannedImageSize) -> GptImage1EditImageSize {
|
||||
match s {
|
||||
PlannedImageSize::Square => GptImage1EditImageSize::Square,
|
||||
PlannedImageSize::Horizontal => GptImage1EditImageSize::Horizontal,
|
||||
PlannedImageSize::Vertical => GptImage1EditImageSize::Vertical,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_urls(
|
||||
image_inputs: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage1TextToImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1(
|
||||
FalGptImage1RequestState::TextToImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 1 text-to-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage1EditImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1(
|
||||
FalGptImage1RequestState::EditImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 1 edit-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn assert_debug<T: Debug>(actual: T, expected: &str) {
|
||||
assert_eq!(format!("{:?}", actual), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build2_routes_to_gpt_image_1_request() {
|
||||
let req = unwrap_t2i(base_builder().build2());
|
||||
assert_eq!(req.prompt, "a cat in space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_detection_is_based_on_image_urls() {
|
||||
let text_req = unwrap_t2i(build_fal_gpt_image_1(base_builder()));
|
||||
assert_eq!(text_req.prompt, "a cat in space");
|
||||
|
||||
let edit_req = unwrap_edit(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_eq!(edit_req.image_urls, vec!["https://example.com/img.jpg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_file_tokens_are_rejected() {
|
||||
let result = build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_maps_exhaustively_for_text_and_edit() {
|
||||
let text_cases = [
|
||||
(1, "One"),
|
||||
(2, "Two"),
|
||||
(3, "Three"),
|
||||
(4, "Four"),
|
||||
];
|
||||
|
||||
for (count, expected) in text_cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
|
||||
for (count, expected) in text_cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_rejects_zero_and_handles_overflow_by_strategy() {
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, "Four");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "Some(High)"),
|
||||
(Some(CommonQuality::Low), "Some(Low)"),
|
||||
(Some(CommonQuality::Medium), "Some(Medium)"),
|
||||
(Some(CommonQuality::High), "Some(High)"),
|
||||
];
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "None"),
|
||||
(Some(CommonAspectRatio::Auto), "None"),
|
||||
(Some(CommonAspectRatio::Auto2k), "None"),
|
||||
(Some(CommonAspectRatio::Auto3k), "None"),
|
||||
(Some(CommonAspectRatio::Auto4k), "None"),
|
||||
(Some(CommonAspectRatio::Square), "Some(Square)"),
|
||||
(Some(CommonAspectRatio::SquareHd), "Some(Square)"),
|
||||
(Some(CommonAspectRatio::WideThreeByTwo), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::WideFourByThree), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::WideFiveByFour), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::WideSixteenByNine), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::WideTwentyOneByNine), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::Wide), "Some(Horizontal)"),
|
||||
(Some(CommonAspectRatio::TallTwoByThree), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::TallThreeByFour), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::TallFourByFive), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::TallNineBySixteen), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::TallNineByTwentyOne), "Some(Vertical)"),
|
||||
(Some(CommonAspectRatio::Tall), "Some(Vertical)"),
|
||||
];
|
||||
|
||||
for (aspect_ratio, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected);
|
||||
}
|
||||
|
||||
for (aspect_ratio, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage1CostState {
|
||||
request: FalGptImage1RequestState,
|
||||
}
|
||||
|
||||
impl FalGptImage1CostState {
|
||||
pub fn from_request(request: &FalGptImage1RequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage1RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
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::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::{GptImage1EditImageNumImages, GptImage1EditImageQuality, GptImage1EditImageRequest, GptImage1EditImageSize};
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::{GptImage1TextToImageNumImages, GptImage1TextToImageQuality, GptImage1TextToImageRequest, GptImage1TextToImageSize};
|
||||
|
||||
#[test]
|
||||
fn text_to_image_cost_table_covers_quality_size_and_count() {
|
||||
for (quality, image_size, expected_by_count) in text_to_image_price_cases() {
|
||||
for (num_images, expected) in text_to_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage1RequestState::TextToImage(GptImage1TextToImageRequest { prompt: "test".to_string(), num_images, image_size, quality, background: None, output_format: None });
|
||||
let cost = FalGptImage1CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_cost_table_covers_quality_size_and_count() {
|
||||
for (quality, image_size, expected_by_count) in edit_image_price_cases() {
|
||||
for (num_images, expected) in edit_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage1RequestState::EditImage(GptImage1EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_image_url: None, image_size, quality, input_fidelity: None, background: None, output_format: None });
|
||||
let cost = FalGptImage1CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_to_image_price_cases() -> [(Option<GptImage1TextToImageQuality>, Option<GptImage1TextToImageSize>, [u64; 4]); 13] {
|
||||
[
|
||||
(None, None, [5, 10, 15, 20]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Auto), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Square), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Horizontal), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Vertical), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Auto), [5, 10, 15, 20]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Square), [5, 10, 15, 20]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Horizontal), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Vertical), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Auto), [17, 34, 51, 68]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Square), [17, 34, 51, 68]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Horizontal), [25, 50, 75, 100]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Vertical), [25, 50, 75, 100]),
|
||||
]
|
||||
}
|
||||
|
||||
fn edit_image_price_cases() -> [(Option<GptImage1EditImageQuality>, Option<GptImage1EditImageSize>, [u64; 4]); 13] {
|
||||
[
|
||||
(None, None, [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Auto), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Square), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Horizontal), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Vertical), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Auto), [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Square), [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal), [7, 14, 21, 28]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Vertical), [7, 14, 21, 28]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Auto), [17, 34, 51, 68]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square), [17, 34, 51, 68]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal), [25, 50, 75, 100]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Vertical), [25, 50, 75, 100]),
|
||||
]
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1TextToImageNumImages, u64); 4] {
|
||||
[(GptImage1TextToImageNumImages::One, expected_by_count[0]), (GptImage1TextToImageNumImages::Two, expected_by_count[1]), (GptImage1TextToImageNumImages::Three, expected_by_count[2]), (GptImage1TextToImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn edit_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1EditImageNumImages, u64); 4] {
|
||||
[(GptImage1EditImageNumImages::One, expected_by_count[0]), (GptImage1EditImageNumImages::Two, expected_by_count[1]), (GptImage1EditImageNumImages::Three, expected_by_count[2]), (GptImage1EditImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::GptImage1EditImageRequest;
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::GptImage1TextToImageRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::{FalImageResponsePayload, GenerateImageResponse};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalGptImage1RequestState {
|
||||
TextToImage(GptImage1TextToImageRequest),
|
||||
EditImage(GptImage1EditImageRequest),
|
||||
}
|
||||
|
||||
impl FalGptImage1RequestState {
|
||||
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
Self::TextToImage(request) => send_request(request, client).await,
|
||||
Self::EditImage(request) => send_request(request, client).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FalResponseIds {
|
||||
request_id: Option<String>,
|
||||
gateway_request_id: Option<String>,
|
||||
status_url: Option<String>,
|
||||
response_url: Option<String>,
|
||||
}
|
||||
|
||||
async fn send_request<T>(request: &T, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError>
|
||||
where
|
||||
T: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let payload = send_fal_request(request, client).await?;
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: payload.request_id,
|
||||
gateway_request_id: payload.gateway_request_id,
|
||||
maybe_status_url: payload.status_url,
|
||||
maybe_response_url: payload.response_url,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn send_fal_request<T: FalEndpoint>(
|
||||
request: &T,
|
||||
client: &RouterFalWebhookOptionalClient,
|
||||
) -> Result<FalResponseIds, ArtcraftRouterError> {
|
||||
if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
status_url: None,
|
||||
response_url: None,
|
||||
})
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
status_url: Some(response.status_url),
|
||||
response_url: Some(response.response_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::{
|
||||
GptImage1EditImageNumImages, GptImage1EditImageQuality, GptImage1EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::{
|
||||
GptImage1TextToImageNumImages, GptImage1TextToImageQuality,
|
||||
GptImage1TextToImageSize,
|
||||
};
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
RouterFalWebhookOptionalClient::new(FalApiKey::from_str(secret.trim()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_text_to_image_queue() {
|
||||
let request = FalGptImage1RequestState::TextToImage(GptImage1TextToImageRequest {
|
||||
prompt: "a small ceramic robot reading by a window".to_string(),
|
||||
num_images: GptImage1TextToImageNumImages::One,
|
||||
image_size: Some(GptImage1TextToImageSize::Square),
|
||||
quality: Some(GptImage1TextToImageQuality::Low),
|
||||
background: None,
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_edit_image_queue() {
|
||||
let request = FalGptImage1RequestState::EditImage(GptImage1EditImageRequest {
|
||||
prompt: "make the image feel like a quiet watercolor study".to_string(),
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
num_images: GptImage1EditImageNumImages::One,
|
||||
mask_image_url: None,
|
||||
image_size: Some(GptImage1EditImageSize::Square),
|
||||
quality: Some(GptImage1EditImageQuality::Low),
|
||||
input_fidelity: None,
|
||||
background: None,
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::{
|
||||
GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality,
|
||||
GptImage1p5EditImageRequest, GptImage1p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::{
|
||||
GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality,
|
||||
GptImage1p5TextToImageRequest, GptImage1p5TextToImageSize,
|
||||
};
|
||||
|
||||
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_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
|
||||
pub fn build_fal_gpt_image_1p5(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let prompt = builder.prompt.clone().unwrap_or_default();
|
||||
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, builder.request_mismatch_mitigation_strategy)?;
|
||||
let image_size = plan_image_size(builder.aspect_ratio);
|
||||
let quality = plan_quality(builder.quality);
|
||||
|
||||
let state = if image_urls.is_empty() {
|
||||
FalGptImage1p5RequestState::TextToImage(GptImage1p5TextToImageRequest {
|
||||
prompt,
|
||||
num_images: to_t2i_num_images(num_images),
|
||||
image_size: image_size.map(to_t2i_image_size),
|
||||
background: None,
|
||||
quality: Some(to_t2i_quality(quality)),
|
||||
output_format: None,
|
||||
})
|
||||
} else {
|
||||
FalGptImage1p5RequestState::EditImage(GptImage1p5EditImageRequest {
|
||||
prompt,
|
||||
image_urls,
|
||||
num_images: to_edit_num_images(num_images),
|
||||
mask_image_url: None,
|
||||
image_size: image_size.map(to_edit_image_size),
|
||||
background: None,
|
||||
quality: Some(to_edit_quality(quality)),
|
||||
input_fidelity: None,
|
||||
output_format: None,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1p5(state),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedImageSize {
|
||||
Square,
|
||||
Wide,
|
||||
Tall,
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<PlannedNumImages, ArtcraftRouterError> {
|
||||
let count = count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(PlannedNumImages::One),
|
||||
2 => Ok(PlannedNumImages::Two),
|
||||
3 => Ok(PlannedNumImages::Three),
|
||||
4 => Ok(PlannedNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(PlannedNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> PlannedQuality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => PlannedQuality::Low,
|
||||
Some(CommonQuality::Medium) => PlannedQuality::Medium,
|
||||
Some(CommonQuality::High) | None => PlannedQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<PlannedImageSize> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Some(PlannedImageSize::Square)
|
||||
}
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(PlannedImageSize::Wide),
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(PlannedImageSize::Tall),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: PlannedNumImages) -> GptImage1p5TextToImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage1p5TextToImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage1p5TextToImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage1p5TextToImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage1p5TextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: PlannedNumImages) -> GptImage1p5EditImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage1p5EditImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage1p5EditImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage1p5EditImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage1p5EditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_quality(q: PlannedQuality) -> GptImage1p5TextToImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage1p5TextToImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage1p5TextToImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage1p5TextToImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_quality(q: PlannedQuality) -> GptImage1p5EditImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage1p5EditImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage1p5EditImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage1p5EditImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_image_size(s: PlannedImageSize) -> GptImage1p5TextToImageSize {
|
||||
match s {
|
||||
PlannedImageSize::Square => GptImage1p5TextToImageSize::Square,
|
||||
PlannedImageSize::Wide => GptImage1p5TextToImageSize::Wide,
|
||||
PlannedImageSize::Tall => GptImage1p5TextToImageSize::Tall,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_image_size(s: PlannedImageSize) -> GptImage1p5EditImageSize {
|
||||
match s {
|
||||
PlannedImageSize::Square => GptImage1p5EditImageSize::Square,
|
||||
PlannedImageSize::Wide => GptImage1p5EditImageSize::Wide,
|
||||
PlannedImageSize::Tall => GptImage1p5EditImageSize::Tall,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_urls(
|
||||
image_inputs: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage1p5,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage1p5TextToImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1p5(
|
||||
FalGptImage1p5RequestState::TextToImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 1.5 text-to-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage1p5EditImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage1p5(
|
||||
FalGptImage1p5RequestState::EditImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 1.5 edit-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn assert_debug<T: Debug>(actual: T, expected: &str) {
|
||||
assert_eq!(format!("{:?}", actual), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build2_routes_to_gpt_image_1p5_request() {
|
||||
let req = unwrap_t2i(base_builder().build2());
|
||||
assert_eq!(req.prompt, "a cat in space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_detection_is_based_on_image_urls() {
|
||||
let text_req = unwrap_t2i(build_fal_gpt_image_1p5(base_builder()));
|
||||
assert_eq!(text_req.prompt, "a cat in space");
|
||||
|
||||
let edit_req = unwrap_edit(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_eq!(edit_req.image_urls, vec!["https://example.com/img.jpg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_file_tokens_are_rejected() {
|
||||
let result = build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(1, "One"),
|
||||
(2, "Two"),
|
||||
(3, "Three"),
|
||||
(4, "Four"),
|
||||
];
|
||||
|
||||
for (count, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
|
||||
for (count, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_rejects_zero_and_handles_overflow_by_strategy() {
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, "Four");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "Some(High)"),
|
||||
(Some(CommonQuality::Low), "Some(Low)"),
|
||||
(Some(CommonQuality::Medium), "Some(Medium)"),
|
||||
(Some(CommonQuality::High), "Some(High)"),
|
||||
];
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "None"),
|
||||
(Some(CommonAspectRatio::Auto), "None"),
|
||||
(Some(CommonAspectRatio::Auto2k), "None"),
|
||||
(Some(CommonAspectRatio::Auto3k), "None"),
|
||||
(Some(CommonAspectRatio::Auto4k), "None"),
|
||||
(Some(CommonAspectRatio::Square), "Some(Square)"),
|
||||
(Some(CommonAspectRatio::SquareHd), "Some(Square)"),
|
||||
(Some(CommonAspectRatio::WideThreeByTwo), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::WideFourByThree), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::WideFiveByFour), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::WideSixteenByNine), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::WideTwentyOneByNine), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::Wide), "Some(Wide)"),
|
||||
(Some(CommonAspectRatio::TallTwoByThree), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::TallThreeByFour), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::TallFourByFive), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::TallNineBySixteen), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::TallNineByTwentyOne), "Some(Tall)"),
|
||||
(Some(CommonAspectRatio::Tall), "Some(Tall)"),
|
||||
];
|
||||
|
||||
for (aspect_ratio, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected);
|
||||
}
|
||||
|
||||
for (aspect_ratio, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_1p5(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage1p5CostState {
|
||||
request: FalGptImage1p5RequestState,
|
||||
}
|
||||
|
||||
impl FalGptImage1p5CostState {
|
||||
pub fn from_request(request: &FalGptImage1p5RequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage1p5RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1p5RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
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::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::{GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality, GptImage1p5EditImageRequest, GptImage1p5EditImageSize};
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::{GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality, GptImage1p5TextToImageRequest, GptImage1p5TextToImageSize};
|
||||
|
||||
#[test]
|
||||
fn text_to_image_cost_table_covers_quality_size_and_count() {
|
||||
for (quality, image_size, expected_by_count) in text_to_image_price_cases() {
|
||||
for (num_images, expected) in text_to_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage1p5RequestState::TextToImage(GptImage1p5TextToImageRequest { prompt: "test".to_string(), num_images, image_size, background: None, quality, output_format: None });
|
||||
let cost = FalGptImage1p5CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_cost_table_covers_quality_size_and_count() {
|
||||
for (quality, image_size, expected_by_count) in edit_image_price_cases() {
|
||||
for (num_images, expected) in edit_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage1p5RequestState::EditImage(GptImage1p5EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_image_url: None, image_size, background: None, quality, input_fidelity: None, output_format: None });
|
||||
let cost = FalGptImage1p5CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_to_image_price_cases() -> [(Option<GptImage1p5TextToImageQuality>, Option<GptImage1p5TextToImageSize>, [u64; 4]); 10] {
|
||||
[(None, None, [3, 6, 9, 12]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Square), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Wide), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Tall), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square), [3, 6, 9, 12]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Wide), [5, 10, 15, 20]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Tall), [5, 10, 15, 20]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square), [13, 26, 39, 52]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide), [20, 40, 60, 80]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Tall), [20, 40, 60, 80])]
|
||||
}
|
||||
|
||||
fn edit_image_price_cases() -> [(Option<GptImage1p5EditImageQuality>, Option<GptImage1p5EditImageSize>, [u64; 4]); 10] {
|
||||
[(None, None, [3, 6, 9, 12]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Square), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Wide), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Tall), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Square), [3, 6, 9, 12]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide), [5, 10, 15, 20]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Tall), [5, 10, 15, 20]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square), [13, 26, 39, 52]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide), [20, 40, 60, 80]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Tall), [20, 40, 60, 80])]
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1p5TextToImageNumImages, u64); 4] {
|
||||
[(GptImage1p5TextToImageNumImages::One, expected_by_count[0]), (GptImage1p5TextToImageNumImages::Two, expected_by_count[1]), (GptImage1p5TextToImageNumImages::Three, expected_by_count[2]), (GptImage1p5TextToImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn edit_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1p5EditImageNumImages, u64); 4] {
|
||||
[(GptImage1p5EditImageNumImages::One, expected_by_count[0]), (GptImage1p5EditImageNumImages::Two, expected_by_count[1]), (GptImage1p5EditImageNumImages::Three, expected_by_count[2]), (GptImage1p5EditImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::GptImage1p5EditImageRequest;
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::GptImage1p5TextToImageRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::{FalImageResponsePayload, GenerateImageResponse};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalGptImage1p5RequestState {
|
||||
TextToImage(GptImage1p5TextToImageRequest),
|
||||
EditImage(GptImage1p5EditImageRequest),
|
||||
}
|
||||
|
||||
impl FalGptImage1p5RequestState {
|
||||
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
Self::TextToImage(request) => send_request(request, client).await,
|
||||
Self::EditImage(request) => send_request(request, client).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FalResponseIds {
|
||||
request_id: Option<String>,
|
||||
gateway_request_id: Option<String>,
|
||||
status_url: Option<String>,
|
||||
response_url: Option<String>,
|
||||
}
|
||||
|
||||
async fn send_request<T>(request: &T, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError>
|
||||
where
|
||||
T: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let payload = send_fal_request(request, client).await?;
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: payload.request_id,
|
||||
gateway_request_id: payload.gateway_request_id,
|
||||
maybe_status_url: payload.status_url,
|
||||
maybe_response_url: payload.response_url,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn send_fal_request<T: FalEndpoint>(
|
||||
request: &T,
|
||||
client: &RouterFalWebhookOptionalClient,
|
||||
) -> Result<FalResponseIds, ArtcraftRouterError> {
|
||||
if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
status_url: None,
|
||||
response_url: None,
|
||||
})
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
status_url: Some(response.status_url),
|
||||
response_url: Some(response.response_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::{
|
||||
GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality,
|
||||
GptImage1p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::{
|
||||
GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality,
|
||||
GptImage1p5TextToImageSize,
|
||||
};
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
RouterFalWebhookOptionalClient::new(FalApiKey::from_str(secret.trim()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_text_to_image_queue() {
|
||||
let request = FalGptImage1p5RequestState::TextToImage(GptImage1p5TextToImageRequest {
|
||||
prompt: "a tiny brass observatory in a snowy field".to_string(),
|
||||
num_images: GptImage1p5TextToImageNumImages::One,
|
||||
image_size: Some(GptImage1p5TextToImageSize::Square),
|
||||
background: None,
|
||||
quality: Some(GptImage1p5TextToImageQuality::Low),
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_edit_image_queue() {
|
||||
let request = FalGptImage1p5RequestState::EditImage(GptImage1p5EditImageRequest {
|
||||
prompt: "turn this into a softly lit editorial photograph".to_string(),
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
num_images: GptImage1p5EditImageNumImages::One,
|
||||
mask_image_url: None,
|
||||
image_size: Some(GptImage1p5EditImageSize::Square),
|
||||
background: None,
|
||||
quality: Some(GptImage1p5EditImageQuality::Low),
|
||||
input_fidelity: None,
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
use fal_client::requests::api::image::common::gpt_image_2_resolution::GptImage2Resolution;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{
|
||||
GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageRequest,
|
||||
GptImage2EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{
|
||||
GptImage2TextToImageNumImages, GptImage2TextToImageQuality,
|
||||
GptImage2TextToImageRequest, GptImage2TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
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_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
|
||||
pub fn build_fal_gpt_image_2(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let prompt = builder.prompt.clone().unwrap_or_default();
|
||||
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, builder.request_mismatch_mitigation_strategy)?;
|
||||
let image_size = plan_image_size(builder.aspect_ratio);
|
||||
let resolution = plan_resolution(builder.resolution);
|
||||
let quality = plan_quality(builder.quality);
|
||||
|
||||
let state = if image_urls.is_empty() {
|
||||
FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest {
|
||||
prompt,
|
||||
num_images: to_t2i_num_images(num_images),
|
||||
image_size: image_size.and_then(to_t2i_image_size),
|
||||
resolution,
|
||||
quality: Some(to_t2i_quality(quality)),
|
||||
output_format: None,
|
||||
})
|
||||
} else {
|
||||
FalGptImage2RequestState::EditImage(GptImage2EditImageRequest {
|
||||
prompt,
|
||||
image_urls,
|
||||
num_images: to_edit_num_images(num_images),
|
||||
mask_url: None,
|
||||
image_size: image_size.map(to_edit_image_size),
|
||||
resolution,
|
||||
quality: Some(to_edit_quality(quality)),
|
||||
output_format: None,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage2(state),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedImageSize {
|
||||
SquareHd,
|
||||
Square,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
Auto,
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<PlannedNumImages, ArtcraftRouterError> {
|
||||
let count = count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(PlannedNumImages::One),
|
||||
2 => Ok(PlannedNumImages::Two),
|
||||
3 => Ok(PlannedNumImages::Three),
|
||||
4 => Ok(PlannedNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(PlannedNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> PlannedQuality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => PlannedQuality::Low,
|
||||
Some(CommonQuality::Medium) => PlannedQuality::Medium,
|
||||
Some(CommonQuality::High) | None => PlannedQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(resolution: Option<CommonResolution>) -> Option<GptImage2Resolution> {
|
||||
resolution.map(|r| match r {
|
||||
CommonResolution::HalfK
|
||||
| CommonResolution::FourEightyP
|
||||
| CommonResolution::SevenTwentyP
|
||||
| CommonResolution::OneK => GptImage2Resolution::OneK,
|
||||
CommonResolution::TenEightyP | CommonResolution::TwoK => GptImage2Resolution::TwoK,
|
||||
CommonResolution::ThreeK => GptImage2Resolution::ThreeK,
|
||||
CommonResolution::FourK => GptImage2Resolution::FourK,
|
||||
})
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<PlannedImageSize> {
|
||||
match aspect_ratio {
|
||||
None => None,
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Some(PlannedImageSize::Auto),
|
||||
Some(CommonAspectRatio::Square) => Some(PlannedImageSize::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Some(PlannedImageSize::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour) => Some(PlannedImageSize::Landscape4x3),
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(PlannedImageSize::Landscape16x9),
|
||||
Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive) => Some(PlannedImageSize::Portrait4x3),
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(PlannedImageSize::Portrait16x9),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: PlannedNumImages) -> GptImage2TextToImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage2TextToImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage2TextToImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage2TextToImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage2TextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: PlannedNumImages) -> GptImage2EditImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => GptImage2EditImageNumImages::One,
|
||||
PlannedNumImages::Two => GptImage2EditImageNumImages::Two,
|
||||
PlannedNumImages::Three => GptImage2EditImageNumImages::Three,
|
||||
PlannedNumImages::Four => GptImage2EditImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_quality(q: PlannedQuality) -> GptImage2TextToImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage2TextToImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage2TextToImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage2TextToImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_quality(q: PlannedQuality) -> GptImage2EditImageQuality {
|
||||
match q {
|
||||
PlannedQuality::Low => GptImage2EditImageQuality::Low,
|
||||
PlannedQuality::Medium => GptImage2EditImageQuality::Medium,
|
||||
PlannedQuality::High => GptImage2EditImageQuality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_image_size(s: PlannedImageSize) -> Option<GptImage2TextToImageSize> {
|
||||
match s {
|
||||
PlannedImageSize::SquareHd => Some(GptImage2TextToImageSize::SquareHd),
|
||||
PlannedImageSize::Square => Some(GptImage2TextToImageSize::Square),
|
||||
PlannedImageSize::Portrait4x3 => Some(GptImage2TextToImageSize::Portrait4x3),
|
||||
PlannedImageSize::Portrait16x9 => Some(GptImage2TextToImageSize::Portrait16x9),
|
||||
PlannedImageSize::Landscape4x3 => Some(GptImage2TextToImageSize::Landscape4x3),
|
||||
PlannedImageSize::Landscape16x9 => Some(GptImage2TextToImageSize::Landscape16x9),
|
||||
PlannedImageSize::Auto => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_image_size(s: PlannedImageSize) -> GptImage2EditImageSize {
|
||||
match s {
|
||||
PlannedImageSize::SquareHd => GptImage2EditImageSize::SquareHd,
|
||||
PlannedImageSize::Square => GptImage2EditImageSize::Square,
|
||||
PlannedImageSize::Portrait4x3 => GptImage2EditImageSize::Portrait4x3,
|
||||
PlannedImageSize::Portrait16x9 => GptImage2EditImageSize::Portrait16x9,
|
||||
PlannedImageSize::Landscape4x3 => GptImage2EditImageSize::Landscape4x3,
|
||||
PlannedImageSize::Landscape16x9 => GptImage2EditImageSize::Landscape16x9,
|
||||
PlannedImageSize::Auto => GptImage2EditImageSize::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_urls(
|
||||
image_inputs: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage2,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage2TextToImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage2(
|
||||
FalGptImage2RequestState::TextToImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 2 text-to-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> GptImage2EditImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalGptImage2(
|
||||
FalGptImage2RequestState::EditImage(req)
|
||||
)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected GPT Image 2 edit-image request")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn assert_debug<T: Debug>(actual: T, expected: &str) {
|
||||
assert_eq!(format!("{:?}", actual), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build2_routes_to_gpt_image_2_request() {
|
||||
let req = unwrap_t2i(base_builder().build2());
|
||||
assert_eq!(req.prompt, "a cat in space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_detection_is_based_on_image_urls() {
|
||||
let text_req = unwrap_t2i(build_fal_gpt_image_2(base_builder()));
|
||||
assert_eq!(text_req.prompt, "a cat in space");
|
||||
|
||||
let edit_req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_eq!(edit_req.image_urls, vec!["https://example.com/img.jpg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_file_tokens_are_rejected() {
|
||||
let result = build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(1, "One"),
|
||||
(2, "Two"),
|
||||
(3, "Three"),
|
||||
(4, "Four"),
|
||||
];
|
||||
|
||||
for (count, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
|
||||
for (count, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
image_batch_count: Some(count),
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_rejects_zero_and_handles_overflow_by_strategy() {
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
assert!(matches!(
|
||||
build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
..base_builder()
|
||||
}),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
|
||||
));
|
||||
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.num_images, "Four");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "Some(High)"),
|
||||
(Some(CommonQuality::Low), "Some(Low)"),
|
||||
(Some(CommonQuality::Medium), "Some(Medium)"),
|
||||
(Some(CommonQuality::High), "Some(High)"),
|
||||
];
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
|
||||
for (quality, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
quality,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.quality, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "None"),
|
||||
(Some(CommonResolution::HalfK), "Some(OneK)"),
|
||||
(Some(CommonResolution::FourEightyP), "Some(OneK)"),
|
||||
(Some(CommonResolution::SevenTwentyP), "Some(OneK)"),
|
||||
(Some(CommonResolution::OneK), "Some(OneK)"),
|
||||
(Some(CommonResolution::TenEightyP), "Some(TwoK)"),
|
||||
(Some(CommonResolution::TwoK), "Some(TwoK)"),
|
||||
(Some(CommonResolution::ThreeK), "Some(ThreeK)"),
|
||||
(Some(CommonResolution::FourK), "Some(FourK)"),
|
||||
];
|
||||
|
||||
for (resolution, expected) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
resolution,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.resolution, expected);
|
||||
}
|
||||
|
||||
for (resolution, expected) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
resolution,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.resolution, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_maps_exhaustively_for_text_and_edit() {
|
||||
let cases = [
|
||||
(None, "None", "None"),
|
||||
(Some(CommonAspectRatio::Auto), "None", "Some(Auto)"),
|
||||
(Some(CommonAspectRatio::Auto2k), "None", "Some(Auto)"),
|
||||
(Some(CommonAspectRatio::Auto3k), "None", "Some(Auto)"),
|
||||
(Some(CommonAspectRatio::Auto4k), "None", "Some(Auto)"),
|
||||
(Some(CommonAspectRatio::Square), "Some(Square)", "Some(Square)"),
|
||||
(Some(CommonAspectRatio::SquareHd), "Some(SquareHd)", "Some(SquareHd)"),
|
||||
(Some(CommonAspectRatio::WideThreeByTwo), "Some(Landscape16x9)", "Some(Landscape16x9)"),
|
||||
(Some(CommonAspectRatio::WideFourByThree), "Some(Landscape4x3)", "Some(Landscape4x3)"),
|
||||
(Some(CommonAspectRatio::WideFiveByFour), "Some(Landscape4x3)", "Some(Landscape4x3)"),
|
||||
(Some(CommonAspectRatio::WideSixteenByNine), "Some(Landscape16x9)", "Some(Landscape16x9)"),
|
||||
(Some(CommonAspectRatio::WideTwentyOneByNine), "Some(Landscape16x9)", "Some(Landscape16x9)"),
|
||||
(Some(CommonAspectRatio::Wide), "Some(Landscape16x9)", "Some(Landscape16x9)"),
|
||||
(Some(CommonAspectRatio::TallTwoByThree), "Some(Portrait16x9)", "Some(Portrait16x9)"),
|
||||
(Some(CommonAspectRatio::TallThreeByFour), "Some(Portrait4x3)", "Some(Portrait4x3)"),
|
||||
(Some(CommonAspectRatio::TallFourByFive), "Some(Portrait4x3)", "Some(Portrait4x3)"),
|
||||
(Some(CommonAspectRatio::TallNineBySixteen), "Some(Portrait16x9)", "Some(Portrait16x9)"),
|
||||
(Some(CommonAspectRatio::TallNineByTwentyOne), "Some(Portrait16x9)", "Some(Portrait16x9)"),
|
||||
(Some(CommonAspectRatio::Tall), "Some(Portrait16x9)", "Some(Portrait16x9)"),
|
||||
];
|
||||
|
||||
for (aspect_ratio, expected_t2i, _) in cases {
|
||||
let req = unwrap_t2i(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected_t2i);
|
||||
}
|
||||
|
||||
for (aspect_ratio, _, expected_edit) in cases {
|
||||
let req = unwrap_edit(build_fal_gpt_image_2(GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
aspect_ratio,
|
||||
..base_builder()
|
||||
}));
|
||||
assert_debug(req.image_size, expected_edit);
|
||||
}
|
||||
}
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage2CostState {
|
||||
request: FalGptImage2RequestState,
|
||||
}
|
||||
|
||||
impl FalGptImage2CostState {
|
||||
pub fn from_request(request: &FalGptImage2RequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage2RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage2RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
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::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use fal_client::requests::api::image::common::gpt_image_2_resolution::GptImage2Resolution;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageRequest, GptImage2EditImageSize};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{GptImage2TextToImageNumImages, GptImage2TextToImageQuality, GptImage2TextToImageRequest, GptImage2TextToImageSize};
|
||||
|
||||
type TextPriceCase = (Option<GptImage2TextToImageQuality>, Option<GptImage2TextToImageSize>, Option<GptImage2Resolution>, [u64; 4]);
|
||||
type EditPriceCase = (Option<GptImage2EditImageQuality>, Option<GptImage2EditImageSize>, Option<GptImage2Resolution>, [u64; 4]);
|
||||
|
||||
#[test]
|
||||
fn text_to_image_cost_table_covers_quality_size_resolution_and_count() {
|
||||
for (quality, image_size, resolution, expected_by_count) in text_to_image_price_cases() {
|
||||
for (num_images, expected) in text_to_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest { prompt: "test".to_string(), num_images, image_size, resolution, quality, output_format: None });
|
||||
let cost = FalGptImage2CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_cost_table_covers_quality_size_resolution_and_count() {
|
||||
for (quality, image_size, resolution, expected_by_count) in edit_image_price_cases() {
|
||||
for (num_images, expected) in edit_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage2RequestState::EditImage(GptImage2EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_url: None, image_size, resolution, quality, output_format: None });
|
||||
let cost = FalGptImage2CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_to_image_price_cases() -> Vec<TextPriceCase> {
|
||||
let mut cases = vec![
|
||||
(None, None, None, [22, 44, 66, 88]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None, [2, 3, 4, 5]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), None, [2, 3, 4, 5]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), None, [2, 3, 5, 6]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), None, [2, 4, 6, 7]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), None, [2, 4, 6, 7]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), None, [5, 9, 13, 18]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), None, [5, 9, 13, 18]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), None, [7, 13, 19, 25]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), None, [6, 11, 16, 22]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), None, [6, 11, 16, 22]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), None, [7, 14, 21, 28]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), None, [16, 31, 46, 61]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), None, [16, 31, 46, 61]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), None, [22, 44, 66, 88]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), None, [24, 47, 71, 94]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in text_to_image_resolution_price_cases() {
|
||||
cases.push((Some(GptImage2TextToImageQuality::Low), Some(size), Some(resolution), low));
|
||||
cases.push((Some(GptImage2TextToImageQuality::Medium), Some(size), Some(resolution), medium));
|
||||
cases.push((Some(GptImage2TextToImageQuality::High), Some(size), Some(resolution), high));
|
||||
}
|
||||
|
||||
cases
|
||||
}
|
||||
|
||||
fn edit_image_price_cases() -> Vec<EditPriceCase> {
|
||||
let mut cases = vec![
|
||||
(None, None, None, [22, 44, 66, 88]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), None, [3, 5, 8, 10]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), None, [12, 23, 34, 46]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), None, [42, 83, 124, 166]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [3, 5, 8, 10]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [12, 23, 34, 46]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [42, 83, 124, 166]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None, [2, 3, 4, 5]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), None, [2, 3, 4, 5]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), None, [2, 3, 5, 6]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), None, [2, 4, 6, 7]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), None, [2, 4, 6, 7]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), None, [5, 9, 13, 18]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), None, [5, 9, 13, 18]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), None, [7, 13, 19, 25]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), None, [6, 11, 16, 22]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), None, [6, 11, 16, 22]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd), None, [7, 14, 21, 28]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), None, [16, 31, 46, 61]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), None, [16, 31, 46, 61]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), None, [22, 44, 66, 88]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), None, [24, 47, 71, 94]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in edit_image_resolution_price_cases() {
|
||||
cases.push((Some(GptImage2EditImageQuality::Low), Some(size), Some(resolution), low));
|
||||
cases.push((Some(GptImage2EditImageQuality::Medium), Some(size), Some(resolution), medium));
|
||||
cases.push((Some(GptImage2EditImageQuality::High), Some(size), Some(resolution), high));
|
||||
}
|
||||
|
||||
cases
|
||||
}
|
||||
|
||||
fn text_to_image_resolution_price_cases() -> Vec<(GptImage2TextToImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
vec![
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, [2, 4, 6, 7], [6, 11, 16, 22], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, [2, 4, 6, 7], [6, 11, 16, 22], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, [2, 3, 5, 6], [7, 13, 19, 25], [22, 44, 66, 88]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, [2, 3, 5, 6], [7, 13, 19, 25], [22, 44, 66, 88]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
]
|
||||
}
|
||||
|
||||
fn edit_image_resolution_price_cases() -> Vec<(GptImage2EditImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
text_to_image_resolution_price_cases()
|
||||
.into_iter()
|
||||
.map(|(size, resolution, low, medium, high)| {
|
||||
let edit_size = match size {
|
||||
GptImage2TextToImageSize::SquareHd => GptImage2EditImageSize::SquareHd,
|
||||
GptImage2TextToImageSize::Square => GptImage2EditImageSize::Square,
|
||||
GptImage2TextToImageSize::Portrait4x3 => GptImage2EditImageSize::Portrait4x3,
|
||||
GptImage2TextToImageSize::Portrait16x9 => GptImage2EditImageSize::Portrait16x9,
|
||||
GptImage2TextToImageSize::Landscape4x3 => GptImage2EditImageSize::Landscape4x3,
|
||||
GptImage2TextToImageSize::Landscape16x9 => GptImage2EditImageSize::Landscape16x9,
|
||||
};
|
||||
(edit_size, resolution, low, medium, high)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage2TextToImageNumImages, u64); 4] {
|
||||
[(GptImage2TextToImageNumImages::One, expected_by_count[0]), (GptImage2TextToImageNumImages::Two, expected_by_count[1]), (GptImage2TextToImageNumImages::Three, expected_by_count[2]), (GptImage2TextToImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn edit_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage2EditImageNumImages, u64); 4] {
|
||||
[(GptImage2EditImageNumImages::One, expected_by_count[0]), (GptImage2EditImageNumImages::Two, expected_by_count[1]), (GptImage2EditImageNumImages::Three, expected_by_count[2]), (GptImage2EditImageNumImages::Four, expected_by_count[3])]
|
||||
}
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::GptImage2EditImageRequest;
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::GptImage2TextToImageRequest;
|
||||
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::{FalImageResponsePayload, GenerateImageResponse};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalGptImage2RequestState {
|
||||
TextToImage(GptImage2TextToImageRequest),
|
||||
EditImage(GptImage2EditImageRequest),
|
||||
}
|
||||
|
||||
impl FalGptImage2RequestState {
|
||||
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
Self::TextToImage(request) => send_request(request, client).await,
|
||||
Self::EditImage(request) => send_request(request, client).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FalResponseIds {
|
||||
request_id: Option<String>,
|
||||
gateway_request_id: Option<String>,
|
||||
status_url: Option<String>,
|
||||
response_url: Option<String>,
|
||||
}
|
||||
|
||||
async fn send_request<T>(request: &T, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError>
|
||||
where
|
||||
T: FalEndpoint + Clone + Debug + Send + Sync + 'static,
|
||||
{
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let payload = send_fal_request(request, client).await?;
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: payload.request_id,
|
||||
gateway_request_id: payload.gateway_request_id,
|
||||
maybe_status_url: payload.status_url,
|
||||
maybe_response_url: payload.response_url,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn send_fal_request<T: FalEndpoint>(
|
||||
request: &T,
|
||||
client: &RouterFalWebhookOptionalClient,
|
||||
) -> Result<FalResponseIds, ArtcraftRouterError> {
|
||||
if let Some(webhook_url) = &client.webhook_url {
|
||||
let response = request.send_webhook_request(&client.api_key, webhook_url).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: response.request_id,
|
||||
gateway_request_id: response.gateway_request_id,
|
||||
status_url: None,
|
||||
response_url: None,
|
||||
})
|
||||
} else {
|
||||
let response = request.send_queue_request(&client.api_key).await?;
|
||||
Ok(FalResponseIds {
|
||||
request_id: Some(response.request_id),
|
||||
gateway_request_id: None,
|
||||
status_url: Some(response.status_url),
|
||||
response_url: Some(response.response_url),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::api::image::common::gpt_image_2_resolution::GptImage2Resolution;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{
|
||||
GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{
|
||||
GptImage2TextToImageNumImages, GptImage2TextToImageQuality,
|
||||
GptImage2TextToImageSize,
|
||||
};
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
RouterFalWebhookOptionalClient::new(FalApiKey::from_str(secret.trim()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_text_to_image_queue() {
|
||||
let request = FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest {
|
||||
prompt: "a polished product photo of a glass compass".to_string(),
|
||||
num_images: GptImage2TextToImageNumImages::One,
|
||||
image_size: Some(GptImage2TextToImageSize::Square),
|
||||
resolution: Some(GptImage2Resolution::OneK),
|
||||
quality: Some(GptImage2TextToImageQuality::Low),
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_edit_image_queue() {
|
||||
let request = FalGptImage2RequestState::EditImage(GptImage2EditImageRequest {
|
||||
prompt: "make this look like an elegant magazine cover image".to_string(),
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
num_images: GptImage2EditImageNumImages::One,
|
||||
mask_url: None,
|
||||
image_size: Some(GptImage2EditImageSize::Square),
|
||||
resolution: Some(GptImage2Resolution::OneK),
|
||||
quality: Some(GptImage2EditImageQuality::Low),
|
||||
output_format: None,
|
||||
});
|
||||
let response = request.send(&client_without_webhook()).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some());
|
||||
}
|
||||
}
|
||||
+3
@@ -1,4 +1,7 @@
|
||||
pub mod flux_1_dev;
|
||||
pub mod flux_1_schnell;
|
||||
pub mod gpt_image_1;
|
||||
pub mod gpt_image_1p5;
|
||||
pub mod gpt_image_2;
|
||||
pub mod nano_banana_2;
|
||||
pub mod nano_banana_pro;
|
||||
|
||||
Reference in New Issue
Block a user