mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-23 10:09:29 -04:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2330acc05f | ||
|
|
9b83cece63 | ||
|
|
adcf10d109 | ||
|
|
c34663ed3d | ||
|
|
d6c033425d | ||
|
|
cf97e4cb32 | ||
|
|
4f43b198ad | ||
|
|
e1260d2c08 | ||
|
|
5aca0db36a | ||
|
|
130842c889 | ||
|
|
03a96e0732 | ||
|
|
9a3dd2c9ae |
+207
@@ -0,0 +1,207 @@
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::image::edit::gpt_image_1_edit_image::raw_request::{
|
||||
GptImage1EditImageInput, GptImage1EditImageOutput,
|
||||
};
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GptImage1EditImageRequest {
|
||||
/// Text prompt describing the edit to make.
|
||||
pub prompt: String,
|
||||
|
||||
/// One or more source image URLs to edit.
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// Number of images to generate.
|
||||
pub num_images: GptImage1EditImageNumImages,
|
||||
|
||||
/// Optional mask URL indicating what part of the image to edit.
|
||||
pub mask_image_url: Option<String>,
|
||||
|
||||
/// Output image size.
|
||||
pub image_size: Option<GptImage1EditImageSize>,
|
||||
|
||||
/// Quality level.
|
||||
pub quality: Option<GptImage1EditImageQuality>,
|
||||
|
||||
/// Input fidelity level.
|
||||
pub input_fidelity: Option<GptImage1EditImageInputFidelity>,
|
||||
|
||||
/// Background mode.
|
||||
pub background: Option<GptImage1EditImageBackground>,
|
||||
|
||||
/// Output format.
|
||||
pub output_format: Option<GptImage1EditImageOutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1EditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1EditImageSize {
|
||||
Auto,
|
||||
/// 1024x1024
|
||||
Square,
|
||||
/// 1536x1024
|
||||
Horizontal,
|
||||
/// 1024x1536
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1EditImageQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1EditImageInputFidelity {
|
||||
Low,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1EditImageBackground {
|
||||
Auto,
|
||||
Transparent,
|
||||
Opaque,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1EditImageOutputFormat {
|
||||
Jpeg,
|
||||
Png,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl FalEndpoint for GptImage1EditImageRequest {
|
||||
const ENDPOINT: &str = "fal-ai/gpt-image-1/edit-image";
|
||||
|
||||
type RawRequest = GptImage1EditImageInput;
|
||||
type RawResponse = GptImage1EditImageOutput;
|
||||
|
||||
fn to_raw_request(&self) -> Result<Self::RawRequest, FalErrorPlus> {
|
||||
let num_images = match self.num_images {
|
||||
GptImage1EditImageNumImages::One => 1,
|
||||
GptImage1EditImageNumImages::Two => 2,
|
||||
GptImage1EditImageNumImages::Three => 3,
|
||||
GptImage1EditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let image_size = self.image_size.map(|s| match s {
|
||||
GptImage1EditImageSize::Auto => "auto",
|
||||
GptImage1EditImageSize::Square => "1024x1024",
|
||||
GptImage1EditImageSize::Horizontal => "1536x1024",
|
||||
GptImage1EditImageSize::Vertical => "1024x1536",
|
||||
}.to_string());
|
||||
|
||||
let quality = self.quality.map(|q| match q {
|
||||
GptImage1EditImageQuality::Low => "low",
|
||||
GptImage1EditImageQuality::Medium => "medium",
|
||||
GptImage1EditImageQuality::High => "high",
|
||||
}.to_string());
|
||||
|
||||
let input_fidelity = self.input_fidelity.map(|f| match f {
|
||||
GptImage1EditImageInputFidelity::Low => "low",
|
||||
GptImage1EditImageInputFidelity::High => "high",
|
||||
}.to_string());
|
||||
|
||||
let background = self.background.map(|b| match b {
|
||||
GptImage1EditImageBackground::Auto => "auto",
|
||||
GptImage1EditImageBackground::Transparent => "transparent",
|
||||
GptImage1EditImageBackground::Opaque => "opaque",
|
||||
}.to_string());
|
||||
|
||||
let output_format = Some(match self.output_format {
|
||||
Some(GptImage1EditImageOutputFormat::Jpeg) => "jpeg",
|
||||
Some(GptImage1EditImageOutputFormat::Png) => "png",
|
||||
Some(GptImage1EditImageOutputFormat::Webp) => "webp",
|
||||
None => "png",
|
||||
}.to_string());
|
||||
|
||||
Ok(Self::RawRequest {
|
||||
prompt: self.prompt.clone(),
|
||||
image_urls: self.image_urls.clone(),
|
||||
num_images: Some(num_images),
|
||||
mask_image_url: self.mask_image_url.clone(),
|
||||
image_size,
|
||||
quality,
|
||||
input_fidelity,
|
||||
background,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_edit_image_queue() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage1EditImageRequest {
|
||||
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
|
||||
image_urls: vec![
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
|
||||
],
|
||||
num_images: GptImage1EditImageNumImages::One,
|
||||
mask_image_url: None,
|
||||
image_size: Some(GptImage1EditImageSize::Horizontal),
|
||||
quality: Some(GptImage1EditImageQuality::Medium),
|
||||
input_fidelity: None,
|
||||
background: None,
|
||||
output_format: None,
|
||||
};
|
||||
|
||||
let result = request.send_queue_request(&api_key).await?;
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_edit_image_webhook() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage1EditImageRequest {
|
||||
image_urls: vec![GHOST_IMAGE_URL.to_string()],
|
||||
prompt: "make the ghost wear a top hat".to_string(),
|
||||
num_images: GptImage1EditImageNumImages::One,
|
||||
mask_image_url: None,
|
||||
image_size: None,
|
||||
quality: Some(GptImage1EditImageQuality::High),
|
||||
input_fidelity: None,
|
||||
background: None,
|
||||
output_format: Some(GptImage1EditImageOutputFormat::Png),
|
||||
};
|
||||
|
||||
let result = request.send_webhook_request(
|
||||
&api_key,
|
||||
"https://example.com/webhook",
|
||||
).await?;
|
||||
println!("Request ID: {:?}", result.request_id);
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NB: Pricing tests are in cost.rs
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
use crate::requests::api::image::edit::gpt_image_1_edit_image::api::{
|
||||
GptImage1EditImageNumImages, GptImage1EditImageQuality,
|
||||
GptImage1EditImageRequest, GptImage1EditImageSize,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
|
||||
impl FalRequestCostCalculator for GptImage1EditImageRequest {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Per fal docs (fal-ai/gpt-image-1/edit-image):
|
||||
// Low: $0.011 (1024x1024) / $0.016 (other) per image
|
||||
// Medium: $0.042 (1024x1024) / $0.063 (other) per image
|
||||
// High: $0.167 (1024x1024) / $0.25 (other) per image
|
||||
let use_quality = self.quality.unwrap_or(GptImage1EditImageQuality::Medium);
|
||||
let is_square = matches!(
|
||||
self.image_size,
|
||||
None | Some(GptImage1EditImageSize::Square) | Some(GptImage1EditImageSize::Auto)
|
||||
);
|
||||
|
||||
let base_cost: u64 = match (use_quality, is_square) {
|
||||
(GptImage1EditImageQuality::Low, true) => 2,
|
||||
(GptImage1EditImageQuality::Low, false) => 2,
|
||||
(GptImage1EditImageQuality::Medium, true) => 5,
|
||||
(GptImage1EditImageQuality::Medium, false) => 7,
|
||||
(GptImage1EditImageQuality::High, true) => 17,
|
||||
(GptImage1EditImageQuality::High, false) => 25,
|
||||
};
|
||||
|
||||
let n: u64 = match self.num_images {
|
||||
GptImage1EditImageNumImages::One => 1,
|
||||
GptImage1EditImageNumImages::Two => 2,
|
||||
GptImage1EditImageNumImages::Three => 3,
|
||||
GptImage1EditImageNumImages::Four => 4,
|
||||
};
|
||||
base_cost * n
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_request(
|
||||
num_images: GptImage1EditImageNumImages,
|
||||
quality: Option<GptImage1EditImageQuality>,
|
||||
image_size: Option<GptImage1EditImageSize>,
|
||||
) -> GptImage1EditImageRequest {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
mod default_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_one_image() {
|
||||
// Default quality=Medium, size=Square => 5 cents
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, None, None)
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::Four, None, None)
|
||||
.calculate_cost_in_cents(), 20);
|
||||
}
|
||||
}
|
||||
|
||||
mod low_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_low_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_horizontal_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_auto_four_images() {
|
||||
// Auto is treated as square
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::Four, Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Auto))
|
||||
.calculate_cost_in_cents(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
mod medium_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_auto_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Auto))
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_horizontal_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_vertical_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Vertical))
|
||||
.calculate_cost_in_cents(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_horizontal_three_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::Three, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 21);
|
||||
}
|
||||
}
|
||||
|
||||
mod high_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_horizontal_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_vertical_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Vertical))
|
||||
.calculate_cost_in_cents(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_two_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::Two, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 34);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_horizontal_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1EditImageNumImages::Four, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod api;
|
||||
mod raw_request;
|
||||
mod cost;
|
||||
+5
-14
@@ -1,15 +1,12 @@
|
||||
use crate::requests::core_api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Non-BYOK edit-image (image-to-image) binding for
|
||||
/// `fal-ai/gpt-image-1/edit-image`. Fal hosts this model directly; pricing is
|
||||
/// billed by Fal at their published rates.
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage1NonByokEditImageInput {
|
||||
pub struct GptImage1EditImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// The URL of the mask image indicating what part of the image to edit.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mask_image_url: Option<String>,
|
||||
|
||||
@@ -41,17 +38,11 @@ pub struct GptImage1NonByokEditImageInput {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1NonByokEditImageFile {
|
||||
pub struct GptImage1EditImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1NonByokEditImageOutput {
|
||||
pub images: Vec<GptImage1NonByokEditImageFile>,
|
||||
}
|
||||
|
||||
pub fn gpt_image_1_non_byok_edit_image(
|
||||
params: GptImage1NonByokEditImageInput,
|
||||
) -> FalRequest<GptImage1NonByokEditImageInput, GptImage1NonByokEditImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/edit-image", params)
|
||||
pub struct GptImage1EditImageOutput {
|
||||
pub images: Vec<GptImage1EditImageFile>,
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::image::edit::gpt_image_1p5_edit_image::raw_request::{
|
||||
GptImage1p5EditImageInput, GptImage1p5EditImageOutput,
|
||||
};
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GptImage1p5EditImageRequest {
|
||||
/// Text prompt describing the edit to make.
|
||||
pub prompt: String,
|
||||
|
||||
/// One or more source image URLs to edit.
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// Number of images to generate.
|
||||
pub num_images: GptImage1p5EditImageNumImages,
|
||||
|
||||
/// Optional mask URL indicating what part of the image to edit.
|
||||
pub mask_image_url: Option<String>,
|
||||
|
||||
/// Output image size.
|
||||
pub image_size: Option<GptImage1p5EditImageSize>,
|
||||
|
||||
/// Background mode.
|
||||
pub background: Option<GptImage1p5EditImageBackground>,
|
||||
|
||||
/// Quality level.
|
||||
pub quality: Option<GptImage1p5EditImageQuality>,
|
||||
|
||||
/// Input fidelity level.
|
||||
pub input_fidelity: Option<GptImage1p5EditImageInputFidelity>,
|
||||
|
||||
/// Output format.
|
||||
pub output_format: Option<GptImage1p5EditImageOutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5EditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5EditImageSize {
|
||||
/// 1024x1024
|
||||
Square,
|
||||
/// 1536x1024
|
||||
Wide,
|
||||
/// 1024x1536
|
||||
Tall,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5EditImageBackground {
|
||||
Auto,
|
||||
Transparent,
|
||||
Opaque,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5EditImageQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5EditImageInputFidelity {
|
||||
Low,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5EditImageOutputFormat {
|
||||
Jpeg,
|
||||
Png,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl FalEndpoint for GptImage1p5EditImageRequest {
|
||||
const ENDPOINT: &str = "fal-ai/gpt-image-1.5/edit";
|
||||
|
||||
type RawRequest = GptImage1p5EditImageInput;
|
||||
type RawResponse = GptImage1p5EditImageOutput;
|
||||
|
||||
fn to_raw_request(&self) -> Result<Self::RawRequest, FalErrorPlus> {
|
||||
let num_images = match self.num_images {
|
||||
GptImage1p5EditImageNumImages::One => 1,
|
||||
GptImage1p5EditImageNumImages::Two => 2,
|
||||
GptImage1p5EditImageNumImages::Three => 3,
|
||||
GptImage1p5EditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let image_size = self.image_size.map(|s| match s {
|
||||
GptImage1p5EditImageSize::Square => "1024x1024",
|
||||
GptImage1p5EditImageSize::Wide => "1536x1024",
|
||||
GptImage1p5EditImageSize::Tall => "1024x1536",
|
||||
}.to_string());
|
||||
|
||||
let background = self.background.map(|b| match b {
|
||||
GptImage1p5EditImageBackground::Auto => "auto",
|
||||
GptImage1p5EditImageBackground::Transparent => "transparent",
|
||||
GptImage1p5EditImageBackground::Opaque => "opaque",
|
||||
}.to_string());
|
||||
|
||||
let quality = self.quality.map(|q| match q {
|
||||
GptImage1p5EditImageQuality::Low => "low",
|
||||
GptImage1p5EditImageQuality::Medium => "medium",
|
||||
GptImage1p5EditImageQuality::High => "high",
|
||||
}.to_string());
|
||||
|
||||
let input_fidelity = self.input_fidelity.map(|f| match f {
|
||||
GptImage1p5EditImageInputFidelity::Low => "low",
|
||||
GptImage1p5EditImageInputFidelity::High => "high",
|
||||
}.to_string());
|
||||
|
||||
let output_format = Some(match self.output_format {
|
||||
Some(GptImage1p5EditImageOutputFormat::Jpeg) => "jpeg",
|
||||
Some(GptImage1p5EditImageOutputFormat::Png) => "png",
|
||||
Some(GptImage1p5EditImageOutputFormat::Webp) => "webp",
|
||||
None => "png",
|
||||
}.to_string());
|
||||
|
||||
Ok(Self::RawRequest {
|
||||
prompt: self.prompt.clone(),
|
||||
image_urls: self.image_urls.clone(),
|
||||
num_images: Some(num_images),
|
||||
mask_image_url: self.mask_image_url.clone(),
|
||||
image_size,
|
||||
background,
|
||||
quality,
|
||||
input_fidelity,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_edit_image_queue() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage1p5EditImageRequest {
|
||||
image_urls: vec![
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
|
||||
],
|
||||
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
|
||||
num_images: GptImage1p5EditImageNumImages::Two,
|
||||
mask_image_url: None,
|
||||
image_size: None,
|
||||
background: None,
|
||||
quality: None,
|
||||
input_fidelity: None,
|
||||
output_format: None,
|
||||
};
|
||||
|
||||
let result = request.send_queue_request(&api_key).await?;
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_edit_image_webhook() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage1p5EditImageRequest {
|
||||
image_urls: vec![GHOST_IMAGE_URL.to_string()],
|
||||
prompt: "make the ghost wear a top hat".to_string(),
|
||||
num_images: GptImage1p5EditImageNumImages::One,
|
||||
mask_image_url: None,
|
||||
image_size: Some(GptImage1p5EditImageSize::Square),
|
||||
background: None,
|
||||
quality: Some(GptImage1p5EditImageQuality::High),
|
||||
input_fidelity: None,
|
||||
output_format: Some(GptImage1p5EditImageOutputFormat::Png),
|
||||
};
|
||||
|
||||
let result = request.send_webhook_request(
|
||||
&api_key,
|
||||
"https://example.com/webhook",
|
||||
).await?;
|
||||
println!("Request ID: {:?}", result.request_id);
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NB: Pricing tests are in cost.rs
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
use crate::requests::api::image::edit::gpt_image_1p5_edit_image::api::{
|
||||
GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality,
|
||||
GptImage1p5EditImageRequest, GptImage1p5EditImageSize,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
|
||||
impl FalRequestCostCalculator for GptImage1p5EditImageRequest {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Per fal docs (fal-ai/gpt-image-1.5/edit):
|
||||
// Low: $0.009 (1024x1024) / $0.013 (other) per image
|
||||
// Medium: $0.034 (1024x1024) / $0.050-$0.051 (other) per image
|
||||
// High: $0.133 (1024x1024) / $0.199-$0.200 (other) per image
|
||||
let use_quality = self.quality.unwrap_or(GptImage1p5EditImageQuality::Medium);
|
||||
let use_size = self.image_size.unwrap_or(GptImage1p5EditImageSize::Square);
|
||||
|
||||
let base_cost = match (use_quality, use_size) {
|
||||
(GptImage1p5EditImageQuality::Low, GptImage1p5EditImageSize::Square) => 1,
|
||||
(GptImage1p5EditImageQuality::Low, _) => 1,
|
||||
(GptImage1p5EditImageQuality::Medium, GptImage1p5EditImageSize::Square) => 3,
|
||||
(GptImage1p5EditImageQuality::Medium, _) => 5,
|
||||
(GptImage1p5EditImageQuality::High, GptImage1p5EditImageSize::Square) => 13,
|
||||
(GptImage1p5EditImageQuality::High, _) => 20,
|
||||
};
|
||||
|
||||
let cost = match self.num_images {
|
||||
GptImage1p5EditImageNumImages::One => base_cost,
|
||||
GptImage1p5EditImageNumImages::Two => base_cost * 2,
|
||||
GptImage1p5EditImageNumImages::Three => base_cost * 3,
|
||||
GptImage1p5EditImageNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_request(
|
||||
num_images: GptImage1p5EditImageNumImages,
|
||||
quality: Option<GptImage1p5EditImageQuality>,
|
||||
image_size: Option<GptImage1p5EditImageSize>,
|
||||
) -> GptImage1p5EditImageRequest {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
mod default_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_one_image() {
|
||||
// Default quality=Medium, size=Square => 3 cents
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, None, None)
|
||||
.calculate_cost_in_cents(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::Four, None, None)
|
||||
.calculate_cost_in_cents(), 12);
|
||||
}
|
||||
}
|
||||
|
||||
mod low_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_low_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_wide_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_tall_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::Four, Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Tall))
|
||||
.calculate_cost_in_cents(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
mod medium_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_wide_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_tall_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Tall))
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_wide_three_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::Three, Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 15);
|
||||
}
|
||||
}
|
||||
|
||||
mod high_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_wide_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_tall_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Tall))
|
||||
.calculate_cost_in_cents(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_two_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::Two, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_wide_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5EditImageNumImages::Four, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 80);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod api;
|
||||
mod raw_request;
|
||||
mod cost;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage1p5EditImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// The URL of the mask image indicating what part of the image to edit.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mask_image_url: Option<String>,
|
||||
|
||||
/// "1024x1024", "1536x1024", "1024x1536"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// "auto", "transparent", "opaque"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<String>,
|
||||
|
||||
/// "low", "medium", "high"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality: Option<String>,
|
||||
|
||||
/// "low", "high"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_fidelity: Option<String>,
|
||||
|
||||
/// 1 - 4
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<u8>,
|
||||
|
||||
/// "jpeg", "png", "webp"
|
||||
/// Default: "png"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1p5EditImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1p5EditImageOutput {
|
||||
pub images: Vec<GptImage1p5EditImageFile>,
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::image::edit::gpt_image_2_edit_image::raw_request::{
|
||||
GptImage2EditImageInput, GptImage2EditImageOutput,
|
||||
};
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GptImage2EditImageRequest {
|
||||
/// Text prompt describing the edit to make.
|
||||
pub prompt: String,
|
||||
|
||||
/// One or more source image URLs to edit.
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// Number of images to generate.
|
||||
pub num_images: GptImage2EditImageNumImages,
|
||||
|
||||
/// Optional mask URL indicating what part of the image to edit.
|
||||
pub mask_url: Option<String>,
|
||||
|
||||
/// Output image size.
|
||||
pub image_size: Option<GptImage2EditImageSize>,
|
||||
|
||||
/// Quality level.
|
||||
pub quality: Option<GptImage2EditImageQuality>,
|
||||
|
||||
/// Output format.
|
||||
pub output_format: Option<GptImage2EditImageOutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage2EditImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage2EditImageSize {
|
||||
SquareHd,
|
||||
Square,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage2EditImageQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage2EditImageOutputFormat {
|
||||
Jpeg,
|
||||
Png,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl FalEndpoint for GptImage2EditImageRequest {
|
||||
const ENDPOINT: &str = "openai/gpt-image-2/edit";
|
||||
|
||||
type RawRequest = GptImage2EditImageInput;
|
||||
type RawResponse = GptImage2EditImageOutput;
|
||||
|
||||
fn to_raw_request(&self) -> Result<Self::RawRequest, FalErrorPlus> {
|
||||
let num_images = match self.num_images {
|
||||
GptImage2EditImageNumImages::One => 1,
|
||||
GptImage2EditImageNumImages::Two => 2,
|
||||
GptImage2EditImageNumImages::Three => 3,
|
||||
GptImage2EditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let image_size = self.image_size.map(|s| match s {
|
||||
GptImage2EditImageSize::SquareHd => "square_hd",
|
||||
GptImage2EditImageSize::Square => "square",
|
||||
GptImage2EditImageSize::Portrait4x3 => "portrait_4_3",
|
||||
GptImage2EditImageSize::Portrait16x9 => "portrait_16_9",
|
||||
GptImage2EditImageSize::Landscape4x3 => "landscape_4_3",
|
||||
GptImage2EditImageSize::Landscape16x9 => "landscape_16_9",
|
||||
GptImage2EditImageSize::Auto => "auto",
|
||||
}.to_string());
|
||||
|
||||
let quality = self.quality.map(|q| match q {
|
||||
GptImage2EditImageQuality::Low => "low",
|
||||
GptImage2EditImageQuality::Medium => "medium",
|
||||
GptImage2EditImageQuality::High => "high",
|
||||
}.to_string());
|
||||
|
||||
let output_format = Some(match self.output_format {
|
||||
Some(GptImage2EditImageOutputFormat::Jpeg) => "jpeg",
|
||||
Some(GptImage2EditImageOutputFormat::Png) => "png",
|
||||
Some(GptImage2EditImageOutputFormat::Webp) => "webp",
|
||||
None => "png",
|
||||
}.to_string());
|
||||
|
||||
Ok(Self::RawRequest {
|
||||
prompt: self.prompt.clone(),
|
||||
image_urls: self.image_urls.clone(),
|
||||
num_images: Some(num_images),
|
||||
mask_url: self.mask_url.clone(),
|
||||
image_size,
|
||||
quality,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_edit_image_queue() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage2EditImageRequest {
|
||||
image_urls: vec![
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
|
||||
],
|
||||
prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(),
|
||||
num_images: GptImage2EditImageNumImages::Two,
|
||||
mask_url: None,
|
||||
image_size: None,
|
||||
quality: None,
|
||||
output_format: None,
|
||||
};
|
||||
|
||||
let result = request.send_queue_request(&api_key).await?;
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_edit_image_webhook() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage2EditImageRequest {
|
||||
image_urls: vec![GHOST_IMAGE_URL.to_string()],
|
||||
prompt: "make the ghost wear a top hat".to_string(),
|
||||
num_images: GptImage2EditImageNumImages::One,
|
||||
mask_url: None,
|
||||
image_size: Some(GptImage2EditImageSize::Square),
|
||||
quality: Some(GptImage2EditImageQuality::High),
|
||||
output_format: Some(GptImage2EditImageOutputFormat::Png),
|
||||
};
|
||||
|
||||
let result = request.send_webhook_request(
|
||||
&api_key,
|
||||
"https://example.com/webhook",
|
||||
).await?;
|
||||
println!("Request ID: {:?}", result.request_id);
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NB: Pricing tests are in cost.rs
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
use crate::requests::api::image::edit::gpt_image_2_edit_image::api::{
|
||||
GptImage2EditImageNumImages, GptImage2EditImageQuality,
|
||||
GptImage2EditImageRequest, GptImage2EditImageSize,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
|
||||
impl FalRequestCostCalculator for GptImage2EditImageRequest {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Cost table (per image) by approximate pixel dimensions:
|
||||
//
|
||||
// landscape_4_3 (~1024x768): low=$0.01, medium=$0.04, high=$0.15
|
||||
// square (~1024x1024): low=$0.01, medium=$0.06, high=$0.22
|
||||
// portrait_4_3 (~768x1024): low=$0.01, medium=$0.04, high=$0.15
|
||||
// landscape_16_9(~1920x1080): low=$0.01, medium=$0.04, high=$0.16
|
||||
// portrait_16_9 (~1080x1920): low=$0.01, medium=$0.04, high=$0.16
|
||||
// square_hd (~2048x2048): low=$0.01, medium=$0.06, high=$0.23
|
||||
// auto (varies): estimated as square_hd (worst case)
|
||||
let use_quality = self.quality.unwrap_or(GptImage2EditImageQuality::High);
|
||||
let use_size = self.image_size.unwrap_or(GptImage2EditImageSize::Square);
|
||||
|
||||
use GptImage2EditImageQuality::*;
|
||||
use GptImage2EditImageSize::*;
|
||||
|
||||
let base_cost = match (use_quality, use_size) {
|
||||
(Low, _) => 1,
|
||||
(Medium, Landscape4x3 | Portrait4x3 | Landscape16x9 | Portrait16x9) => 4,
|
||||
(Medium, Square) => 6,
|
||||
(Medium, SquareHd) => 6,
|
||||
(Medium, Auto) => 6,
|
||||
(High, Landscape4x3 | Portrait4x3) => 15,
|
||||
(High, Landscape16x9 | Portrait16x9) => 16,
|
||||
(High, Square) => 22,
|
||||
(High, SquareHd) => 23,
|
||||
(High, Auto) => 23,
|
||||
};
|
||||
|
||||
let cost = match self.num_images {
|
||||
GptImage2EditImageNumImages::One => base_cost,
|
||||
GptImage2EditImageNumImages::Two => base_cost * 2,
|
||||
GptImage2EditImageNumImages::Three => base_cost * 3,
|
||||
GptImage2EditImageNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_request(
|
||||
num_images: GptImage2EditImageNumImages,
|
||||
quality: Option<GptImage2EditImageQuality>,
|
||||
image_size: Option<GptImage2EditImageSize>,
|
||||
) -> GptImage2EditImageRequest {
|
||||
GptImage2EditImageRequest {
|
||||
prompt: "test".to_string(),
|
||||
image_urls: vec!["https://example.com/image.png".to_string()],
|
||||
num_images,
|
||||
mask_url: None,
|
||||
image_size,
|
||||
quality,
|
||||
output_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
mod default_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_one_image() {
|
||||
// Default quality=High, size=Square => 22 cents
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, None, None)
|
||||
.calculate_cost_in_cents(), 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::Four, None, None)
|
||||
.calculate_cost_in_cents(), 88);
|
||||
}
|
||||
}
|
||||
|
||||
mod low_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_low_any_size_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd))
|
||||
.calculate_cost_in_cents(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_any_size_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::Four, Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto))
|
||||
.calculate_cost_in_cents(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
mod medium_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_medium_landscape_4x3() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3))
|
||||
.calculate_cost_in_cents(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_portrait_16x9() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9))
|
||||
.calculate_cost_in_cents(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square_hd() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd))
|
||||
.calculate_cost_in_cents(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_auto() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto))
|
||||
.calculate_cost_in_cents(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_landscape_4x3_three_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::Three, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3))
|
||||
.calculate_cost_in_cents(), 12);
|
||||
}
|
||||
}
|
||||
|
||||
mod high_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_high_landscape_4x3() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3))
|
||||
.calculate_cost_in_cents(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_portrait_4x3() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3))
|
||||
.calculate_cost_in_cents(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_landscape_16x9() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9))
|
||||
.calculate_cost_in_cents(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_portrait_16x9() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9))
|
||||
.calculate_cost_in_cents(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square))
|
||||
.calculate_cost_in_cents(), 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_hd() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd))
|
||||
.calculate_cost_in_cents(), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_auto() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto))
|
||||
.calculate_cost_in_cents(), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_hd_two_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2EditImageNumImages::Two, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd))
|
||||
.calculate_cost_in_cents(), 46);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod api;
|
||||
mod raw_request;
|
||||
mod cost;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage2EditImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// The URL of the mask image to use for the generation.
|
||||
/// This indicates what part of the image to edit.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mask_url: Option<String>,
|
||||
|
||||
/// square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9, auto
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// "low", "medium", "high"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality: Option<String>,
|
||||
|
||||
/// 1 - 4
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<u8>,
|
||||
|
||||
/// "jpeg", "png", "webp"
|
||||
/// Default: "png"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage2EditImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage2EditImageOutput {
|
||||
pub images: Vec<GptImage2EditImageFile>,
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
pub mod flux_1_dev_edit_image;
|
||||
pub mod flux_1_schnell_edit_image;
|
||||
pub mod gpt_image_1_edit_image;
|
||||
pub mod gpt_image_1p5_edit_image;
|
||||
pub mod gpt_image_2_edit_image;
|
||||
pub mod nano_banana_2_edit_image;
|
||||
pub mod nano_banana_pro_edit_image;
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::image::text::gpt_image_1_text_to_image::raw_request::{
|
||||
GptImage1TextToImageInput, GptImage1TextToImageOutput,
|
||||
};
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GptImage1TextToImageRequest {
|
||||
/// Text prompt describing the image to generate.
|
||||
pub prompt: String,
|
||||
|
||||
/// Number of images to generate.
|
||||
pub num_images: GptImage1TextToImageNumImages,
|
||||
|
||||
/// Output image size.
|
||||
pub image_size: Option<GptImage1TextToImageSize>,
|
||||
|
||||
/// Quality level.
|
||||
pub quality: Option<GptImage1TextToImageQuality>,
|
||||
|
||||
/// Background mode.
|
||||
pub background: Option<GptImage1TextToImageBackground>,
|
||||
|
||||
/// Output format.
|
||||
pub output_format: Option<GptImage1TextToImageOutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1TextToImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1TextToImageSize {
|
||||
Auto,
|
||||
/// 1024x1024
|
||||
Square,
|
||||
/// 1536x1024
|
||||
Horizontal,
|
||||
/// 1024x1536
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1TextToImageQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1TextToImageBackground {
|
||||
Auto,
|
||||
Transparent,
|
||||
Opaque,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1TextToImageOutputFormat {
|
||||
Jpeg,
|
||||
Png,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl FalEndpoint for GptImage1TextToImageRequest {
|
||||
const ENDPOINT: &str = "fal-ai/gpt-image-1/text-to-image";
|
||||
|
||||
type RawRequest = GptImage1TextToImageInput;
|
||||
type RawResponse = GptImage1TextToImageOutput;
|
||||
|
||||
fn to_raw_request(&self) -> Result<Self::RawRequest, FalErrorPlus> {
|
||||
let num_images = match self.num_images {
|
||||
GptImage1TextToImageNumImages::One => 1,
|
||||
GptImage1TextToImageNumImages::Two => 2,
|
||||
GptImage1TextToImageNumImages::Three => 3,
|
||||
GptImage1TextToImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let image_size = self.image_size.map(|s| match s {
|
||||
GptImage1TextToImageSize::Auto => "auto",
|
||||
GptImage1TextToImageSize::Square => "1024x1024",
|
||||
GptImage1TextToImageSize::Horizontal => "1536x1024",
|
||||
GptImage1TextToImageSize::Vertical => "1024x1536",
|
||||
}.to_string());
|
||||
|
||||
let quality = self.quality.map(|q| match q {
|
||||
GptImage1TextToImageQuality::Low => "low",
|
||||
GptImage1TextToImageQuality::Medium => "medium",
|
||||
GptImage1TextToImageQuality::High => "high",
|
||||
}.to_string());
|
||||
|
||||
let background = self.background.map(|b| match b {
|
||||
GptImage1TextToImageBackground::Auto => "auto",
|
||||
GptImage1TextToImageBackground::Transparent => "transparent",
|
||||
GptImage1TextToImageBackground::Opaque => "opaque",
|
||||
}.to_string());
|
||||
|
||||
let output_format = Some(match self.output_format {
|
||||
Some(GptImage1TextToImageOutputFormat::Jpeg) => "jpeg",
|
||||
Some(GptImage1TextToImageOutputFormat::Png) => "png",
|
||||
Some(GptImage1TextToImageOutputFormat::Webp) => "webp",
|
||||
None => "png",
|
||||
}.to_string());
|
||||
|
||||
Ok(Self::RawRequest {
|
||||
prompt: self.prompt.clone(),
|
||||
num_images: Some(num_images),
|
||||
image_size,
|
||||
quality,
|
||||
background,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_text_to_image_queue() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage1TextToImageRequest {
|
||||
prompt: "an anime girl riding on the back of a t-rex".to_string(),
|
||||
num_images: GptImage1TextToImageNumImages::One,
|
||||
image_size: Some(GptImage1TextToImageSize::Horizontal),
|
||||
quality: Some(GptImage1TextToImageQuality::Medium),
|
||||
background: None,
|
||||
output_format: None,
|
||||
};
|
||||
|
||||
let result = request.send_queue_request(&api_key).await?;
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_text_to_image_webhook() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage1TextToImageRequest {
|
||||
prompt: "a corgi wearing sunglasses at the beach".to_string(),
|
||||
num_images: GptImage1TextToImageNumImages::Two,
|
||||
image_size: Some(GptImage1TextToImageSize::Square),
|
||||
quality: Some(GptImage1TextToImageQuality::High),
|
||||
background: Some(GptImage1TextToImageBackground::Opaque),
|
||||
output_format: Some(GptImage1TextToImageOutputFormat::Png),
|
||||
};
|
||||
|
||||
let result = request.send_webhook_request(
|
||||
&api_key,
|
||||
"https://example.com/webhook",
|
||||
).await?;
|
||||
println!("Request ID: {:?}", result.request_id);
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NB: Pricing tests are in cost.rs
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
use crate::requests::api::image::text::gpt_image_1_text_to_image::api::{
|
||||
GptImage1TextToImageNumImages, GptImage1TextToImageQuality,
|
||||
GptImage1TextToImageRequest, GptImage1TextToImageSize,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
|
||||
impl FalRequestCostCalculator for GptImage1TextToImageRequest {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Per fal docs (fal-ai/gpt-image-1/text-to-image):
|
||||
// Low: $0.011 (1024x1024) / $0.016 (other) per image
|
||||
// Medium: $0.042 (1024x1024) / $0.063 (other) per image
|
||||
// High: $0.167 (1024x1024) / $0.25 (other) per image
|
||||
// Default quality is Medium when unspecified, square when size unspecified.
|
||||
let use_quality = self.quality.unwrap_or(GptImage1TextToImageQuality::Medium);
|
||||
let is_square = matches!(
|
||||
self.image_size,
|
||||
None | Some(GptImage1TextToImageSize::Square) | Some(GptImage1TextToImageSize::Auto)
|
||||
);
|
||||
|
||||
let base_cost: u64 = match (use_quality, is_square) {
|
||||
(GptImage1TextToImageQuality::Low, true) => 2,
|
||||
(GptImage1TextToImageQuality::Low, false) => 2,
|
||||
(GptImage1TextToImageQuality::Medium, true) => 5,
|
||||
(GptImage1TextToImageQuality::Medium, false) => 7,
|
||||
(GptImage1TextToImageQuality::High, true) => 17,
|
||||
(GptImage1TextToImageQuality::High, false) => 25,
|
||||
};
|
||||
|
||||
let n: u64 = match self.num_images {
|
||||
GptImage1TextToImageNumImages::One => 1,
|
||||
GptImage1TextToImageNumImages::Two => 2,
|
||||
GptImage1TextToImageNumImages::Three => 3,
|
||||
GptImage1TextToImageNumImages::Four => 4,
|
||||
};
|
||||
base_cost * n
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_request(
|
||||
num_images: GptImage1TextToImageNumImages,
|
||||
quality: Option<GptImage1TextToImageQuality>,
|
||||
image_size: Option<GptImage1TextToImageSize>,
|
||||
) -> GptImage1TextToImageRequest {
|
||||
GptImage1TextToImageRequest {
|
||||
prompt: "test".to_string(),
|
||||
num_images,
|
||||
image_size,
|
||||
quality,
|
||||
background: None,
|
||||
output_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
mod default_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_one_image() {
|
||||
// Default quality=Medium, size=Square => 5 cents
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, None, None)
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::Four, None, None)
|
||||
.calculate_cost_in_cents(), 20);
|
||||
}
|
||||
}
|
||||
|
||||
mod low_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_low_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_horizontal_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_auto_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::Four, Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Auto))
|
||||
.calculate_cost_in_cents(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
mod medium_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_auto_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Auto))
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_horizontal_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_vertical_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Vertical))
|
||||
.calculate_cost_in_cents(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_horizontal_three_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::Three, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 21);
|
||||
}
|
||||
}
|
||||
|
||||
mod high_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_horizontal_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Horizontal))
|
||||
.calculate_cost_in_cents(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_vertical_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Vertical))
|
||||
.calculate_cost_in_cents(), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_two_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::Two, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 34);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_vertical_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1TextToImageNumImages::Four, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Vertical))
|
||||
.calculate_cost_in_cents(), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod api;
|
||||
mod raw_request;
|
||||
mod cost;
|
||||
+4
-15
@@ -1,12 +1,7 @@
|
||||
use crate::requests::core_api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Non-BYOK text-to-image binding for `fal-ai/gpt-image-1/text-to-image`.
|
||||
///
|
||||
/// Fal hosts this model directly (no caller-supplied OpenAI API key); pricing
|
||||
/// is billed by Fal at their published rates.
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage1NonByokTextToImageInput {
|
||||
pub struct GptImage1TextToImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// "auto", "1024x1024", "1536x1024", "1024x1536"
|
||||
@@ -33,17 +28,11 @@ pub struct GptImage1NonByokTextToImageInput {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1NonByokTextToImageFile {
|
||||
pub struct GptImage1TextToImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1NonByokTextToImageOutput {
|
||||
pub images: Vec<GptImage1NonByokTextToImageFile>,
|
||||
}
|
||||
|
||||
pub fn gpt_image_1_non_byok_text_to_image(
|
||||
params: GptImage1NonByokTextToImageInput,
|
||||
) -> FalRequest<GptImage1NonByokTextToImageInput, GptImage1NonByokTextToImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/text-to-image", params)
|
||||
pub struct GptImage1TextToImageOutput {
|
||||
pub images: Vec<GptImage1TextToImageFile>,
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::image::text::gpt_image_1p5_text_to_image::raw_request::{
|
||||
GptImage1p5TextToImageInput, GptImage1p5TextToImageOutput,
|
||||
};
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GptImage1p5TextToImageRequest {
|
||||
/// Text prompt describing the image to generate.
|
||||
pub prompt: String,
|
||||
|
||||
/// Number of images to generate.
|
||||
pub num_images: GptImage1p5TextToImageNumImages,
|
||||
|
||||
/// Output image size.
|
||||
pub image_size: Option<GptImage1p5TextToImageSize>,
|
||||
|
||||
/// Background mode.
|
||||
pub background: Option<GptImage1p5TextToImageBackground>,
|
||||
|
||||
/// Quality level.
|
||||
pub quality: Option<GptImage1p5TextToImageQuality>,
|
||||
|
||||
/// Output format.
|
||||
pub output_format: Option<GptImage1p5TextToImageOutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5TextToImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5TextToImageSize {
|
||||
/// 1024x1024
|
||||
Square,
|
||||
/// 1536x1024
|
||||
Wide,
|
||||
/// 1024x1536
|
||||
Tall,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5TextToImageBackground {
|
||||
Auto,
|
||||
Transparent,
|
||||
Opaque,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5TextToImageQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage1p5TextToImageOutputFormat {
|
||||
Jpeg,
|
||||
Png,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl FalEndpoint for GptImage1p5TextToImageRequest {
|
||||
const ENDPOINT: &str = "fal-ai/gpt-image-1.5";
|
||||
|
||||
type RawRequest = GptImage1p5TextToImageInput;
|
||||
type RawResponse = GptImage1p5TextToImageOutput;
|
||||
|
||||
fn to_raw_request(&self) -> Result<Self::RawRequest, FalErrorPlus> {
|
||||
let num_images = match self.num_images {
|
||||
GptImage1p5TextToImageNumImages::One => 1,
|
||||
GptImage1p5TextToImageNumImages::Two => 2,
|
||||
GptImage1p5TextToImageNumImages::Three => 3,
|
||||
GptImage1p5TextToImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let image_size = self.image_size.map(|s| match s {
|
||||
GptImage1p5TextToImageSize::Square => "1024x1024",
|
||||
GptImage1p5TextToImageSize::Wide => "1536x1024",
|
||||
GptImage1p5TextToImageSize::Tall => "1024x1536",
|
||||
}.to_string());
|
||||
|
||||
let background = self.background.map(|b| match b {
|
||||
GptImage1p5TextToImageBackground::Auto => "auto",
|
||||
GptImage1p5TextToImageBackground::Transparent => "transparent",
|
||||
GptImage1p5TextToImageBackground::Opaque => "opaque",
|
||||
}.to_string());
|
||||
|
||||
let quality = self.quality.map(|q| match q {
|
||||
GptImage1p5TextToImageQuality::Low => "low",
|
||||
GptImage1p5TextToImageQuality::Medium => "medium",
|
||||
GptImage1p5TextToImageQuality::High => "high",
|
||||
}.to_string());
|
||||
|
||||
let output_format = Some(match self.output_format {
|
||||
Some(GptImage1p5TextToImageOutputFormat::Jpeg) => "jpeg",
|
||||
Some(GptImage1p5TextToImageOutputFormat::Png) => "png",
|
||||
Some(GptImage1p5TextToImageOutputFormat::Webp) => "webp",
|
||||
None => "png",
|
||||
}.to_string());
|
||||
|
||||
Ok(Self::RawRequest {
|
||||
prompt: self.prompt.clone(),
|
||||
num_images: Some(num_images),
|
||||
image_size,
|
||||
background,
|
||||
quality,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_text_to_image_queue() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage1p5TextToImageRequest {
|
||||
prompt: "an anime girl riding on the back of a t-rex".to_string(),
|
||||
num_images: GptImage1p5TextToImageNumImages::One,
|
||||
image_size: None,
|
||||
background: None,
|
||||
quality: None,
|
||||
output_format: None,
|
||||
};
|
||||
|
||||
let result = request.send_queue_request(&api_key).await?;
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_text_to_image_webhook() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage1p5TextToImageRequest {
|
||||
prompt: "a corgi wearing sunglasses at the beach".to_string(),
|
||||
num_images: GptImage1p5TextToImageNumImages::Two,
|
||||
image_size: Some(GptImage1p5TextToImageSize::Wide),
|
||||
background: None,
|
||||
quality: Some(GptImage1p5TextToImageQuality::High),
|
||||
output_format: Some(GptImage1p5TextToImageOutputFormat::Png),
|
||||
};
|
||||
|
||||
let result = request.send_webhook_request(
|
||||
&api_key,
|
||||
"https://example.com/webhook",
|
||||
).await?;
|
||||
println!("Request ID: {:?}", result.request_id);
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NB: Pricing tests are in cost.rs
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
use crate::requests::api::image::text::gpt_image_1p5_text_to_image::api::{
|
||||
GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality,
|
||||
GptImage1p5TextToImageRequest, GptImage1p5TextToImageSize,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
|
||||
impl FalRequestCostCalculator for GptImage1p5TextToImageRequest {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Per fal docs (fal-ai/gpt-image-1.5):
|
||||
// Low: $0.009 (1024x1024) / $0.013 (other) per image
|
||||
// Medium: $0.034 (1024x1024) / $0.050-$0.051 (other) per image
|
||||
// High: $0.133 (1024x1024) / $0.199-$0.200 (other) per image
|
||||
let use_quality = self.quality.unwrap_or(GptImage1p5TextToImageQuality::Medium);
|
||||
let use_size = self.image_size.unwrap_or(GptImage1p5TextToImageSize::Square);
|
||||
|
||||
let base_cost = match (use_quality, use_size) {
|
||||
(GptImage1p5TextToImageQuality::Low, GptImage1p5TextToImageSize::Square) => 1,
|
||||
(GptImage1p5TextToImageQuality::Low, _) => 1,
|
||||
(GptImage1p5TextToImageQuality::Medium, GptImage1p5TextToImageSize::Square) => 3,
|
||||
(GptImage1p5TextToImageQuality::Medium, _) => 5,
|
||||
(GptImage1p5TextToImageQuality::High, GptImage1p5TextToImageSize::Square) => 13,
|
||||
(GptImage1p5TextToImageQuality::High, _) => 20,
|
||||
};
|
||||
|
||||
let cost = match self.num_images {
|
||||
GptImage1p5TextToImageNumImages::One => base_cost,
|
||||
GptImage1p5TextToImageNumImages::Two => base_cost * 2,
|
||||
GptImage1p5TextToImageNumImages::Three => base_cost * 3,
|
||||
GptImage1p5TextToImageNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_request(
|
||||
num_images: GptImage1p5TextToImageNumImages,
|
||||
quality: Option<GptImage1p5TextToImageQuality>,
|
||||
image_size: Option<GptImage1p5TextToImageSize>,
|
||||
) -> GptImage1p5TextToImageRequest {
|
||||
GptImage1p5TextToImageRequest {
|
||||
prompt: "test".to_string(),
|
||||
num_images,
|
||||
image_size,
|
||||
background: None,
|
||||
quality,
|
||||
output_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
mod default_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_one_image() {
|
||||
// Default quality=Medium, size=Square => 3 cents
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, None, None)
|
||||
.calculate_cost_in_cents(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::Four, None, None)
|
||||
.calculate_cost_in_cents(), 12);
|
||||
}
|
||||
}
|
||||
|
||||
mod low_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_low_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_wide_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_tall_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::Four, Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Tall))
|
||||
.calculate_cost_in_cents(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
mod medium_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_wide_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_tall_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Tall))
|
||||
.calculate_cost_in_cents(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square_three_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::Three, Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 9);
|
||||
}
|
||||
}
|
||||
|
||||
mod high_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_wide_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_tall_one_image() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Tall))
|
||||
.calculate_cost_in_cents(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_two_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::Two, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_wide_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage1p5TextToImageNumImages::Four, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide))
|
||||
.calculate_cost_in_cents(), 80);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod api;
|
||||
mod raw_request;
|
||||
mod cost;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage1p5TextToImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// "1024x1024", "1536x1024", "1024x1536"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// "auto", "transparent", "opaque"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<String>,
|
||||
|
||||
/// "low", "medium", "high"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality: Option<String>,
|
||||
|
||||
/// 1 - 4
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<u8>,
|
||||
|
||||
/// "jpeg", "png", "webp"
|
||||
/// Default: "png"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1p5TextToImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1p5TextToImageOutput {
|
||||
pub images: Vec<GptImage1p5TextToImageFile>,
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::api::image::text::gpt_image_2_text_to_image::raw_request::{
|
||||
GptImage2TextToImageInput, GptImage2TextToImageOutput,
|
||||
};
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GptImage2TextToImageRequest {
|
||||
/// Text prompt describing the image to generate.
|
||||
pub prompt: String,
|
||||
|
||||
/// Number of images to generate.
|
||||
pub num_images: GptImage2TextToImageNumImages,
|
||||
|
||||
/// Output image size.
|
||||
pub image_size: Option<GptImage2TextToImageSize>,
|
||||
|
||||
/// Quality level.
|
||||
pub quality: Option<GptImage2TextToImageQuality>,
|
||||
|
||||
/// Output format.
|
||||
pub output_format: Option<GptImage2TextToImageOutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage2TextToImageNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage2TextToImageSize {
|
||||
SquareHd,
|
||||
Square,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage2TextToImageQuality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum GptImage2TextToImageOutputFormat {
|
||||
Jpeg,
|
||||
Png,
|
||||
Webp,
|
||||
}
|
||||
|
||||
impl FalEndpoint for GptImage2TextToImageRequest {
|
||||
const ENDPOINT: &str = "openai/gpt-image-2";
|
||||
|
||||
type RawRequest = GptImage2TextToImageInput;
|
||||
type RawResponse = GptImage2TextToImageOutput;
|
||||
|
||||
fn to_raw_request(&self) -> Result<Self::RawRequest, FalErrorPlus> {
|
||||
let num_images = match self.num_images {
|
||||
GptImage2TextToImageNumImages::One => 1,
|
||||
GptImage2TextToImageNumImages::Two => 2,
|
||||
GptImage2TextToImageNumImages::Three => 3,
|
||||
GptImage2TextToImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let image_size = self.image_size.map(|s| match s {
|
||||
GptImage2TextToImageSize::SquareHd => "square_hd",
|
||||
GptImage2TextToImageSize::Square => "square",
|
||||
GptImage2TextToImageSize::Portrait4x3 => "portrait_4_3",
|
||||
GptImage2TextToImageSize::Portrait16x9 => "portrait_16_9",
|
||||
GptImage2TextToImageSize::Landscape4x3 => "landscape_4_3",
|
||||
GptImage2TextToImageSize::Landscape16x9 => "landscape_16_9",
|
||||
}.to_string());
|
||||
|
||||
let quality = self.quality.map(|q| match q {
|
||||
GptImage2TextToImageQuality::Low => "low",
|
||||
GptImage2TextToImageQuality::Medium => "medium",
|
||||
GptImage2TextToImageQuality::High => "high",
|
||||
}.to_string());
|
||||
|
||||
let output_format = Some(match self.output_format {
|
||||
Some(GptImage2TextToImageOutputFormat::Jpeg) => "jpeg",
|
||||
Some(GptImage2TextToImageOutputFormat::Png) => "png",
|
||||
Some(GptImage2TextToImageOutputFormat::Webp) => "webp",
|
||||
None => "png",
|
||||
}.to_string());
|
||||
|
||||
Ok(Self::RawRequest {
|
||||
prompt: self.prompt.clone(),
|
||||
num_images: Some(num_images),
|
||||
image_size,
|
||||
quality,
|
||||
output_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_text_to_image_queue() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage2TextToImageRequest {
|
||||
prompt: "an anime girl riding on the back of a t-rex".to_string(),
|
||||
num_images: GptImage2TextToImageNumImages::One,
|
||||
image_size: None,
|
||||
quality: None,
|
||||
output_format: None,
|
||||
};
|
||||
|
||||
let result = request.send_queue_request(&api_key).await?;
|
||||
println!("Request ID: {}", result.request_id);
|
||||
assert!(!result.request_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real API key, incurs costs
|
||||
async fn test_text_to_image_webhook() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let request = GptImage2TextToImageRequest {
|
||||
prompt: "a corgi wearing sunglasses at the beach".to_string(),
|
||||
num_images: GptImage2TextToImageNumImages::Two,
|
||||
image_size: Some(GptImage2TextToImageSize::Landscape16x9),
|
||||
quality: Some(GptImage2TextToImageQuality::High),
|
||||
output_format: Some(GptImage2TextToImageOutputFormat::Png),
|
||||
};
|
||||
|
||||
let result = request.send_webhook_request(
|
||||
&api_key,
|
||||
"https://example.com/webhook",
|
||||
).await?;
|
||||
println!("Request ID: {:?}", result.request_id);
|
||||
assert!(result.request_id.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NB: Pricing tests are in cost.rs
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
use crate::requests::api::image::text::gpt_image_2_text_to_image::api::{
|
||||
GptImage2TextToImageNumImages, GptImage2TextToImageQuality,
|
||||
GptImage2TextToImageRequest, GptImage2TextToImageSize,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
|
||||
impl FalRequestCostCalculator for GptImage2TextToImageRequest {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Cost table (per image) by approximate pixel dimensions:
|
||||
//
|
||||
// landscape_4_3 (~1024x768): low=$0.01, medium=$0.04, high=$0.15
|
||||
// square (~1024x1024): low=$0.01, medium=$0.06, high=$0.22
|
||||
// portrait_4_3 (~768x1024): low=$0.01, medium=$0.04, high=$0.15
|
||||
// landscape_16_9(~1920x1080): low=$0.01, medium=$0.04, high=$0.16
|
||||
// portrait_16_9 (~1080x1920): low=$0.01, medium=$0.04, high=$0.16
|
||||
// square_hd (~2048x2048): low=$0.01, medium=$0.06, high=$0.23
|
||||
// auto (varies): estimated as square
|
||||
let use_quality = self.quality.unwrap_or(GptImage2TextToImageQuality::High);
|
||||
let use_size = self.image_size.unwrap_or(GptImage2TextToImageSize::Square);
|
||||
|
||||
use GptImage2TextToImageQuality::*;
|
||||
use GptImage2TextToImageSize::*;
|
||||
|
||||
let base_cost = match (use_quality, use_size) {
|
||||
(Low, _) => 1,
|
||||
(Medium, Landscape4x3 | Portrait4x3 | Landscape16x9 | Portrait16x9) => 4,
|
||||
(Medium, Square) => 6,
|
||||
(Medium, SquareHd) => 6,
|
||||
(High, Landscape4x3 | Portrait4x3) => 15,
|
||||
(High, Landscape16x9 | Portrait16x9) => 16,
|
||||
(High, Square) => 22,
|
||||
(High, SquareHd) => 23,
|
||||
};
|
||||
|
||||
let cost = match self.num_images {
|
||||
GptImage2TextToImageNumImages::One => base_cost,
|
||||
GptImage2TextToImageNumImages::Two => base_cost * 2,
|
||||
GptImage2TextToImageNumImages::Three => base_cost * 3,
|
||||
GptImage2TextToImageNumImages::Four => base_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_request(
|
||||
num_images: GptImage2TextToImageNumImages,
|
||||
quality: Option<GptImage2TextToImageQuality>,
|
||||
image_size: Option<GptImage2TextToImageSize>,
|
||||
) -> GptImage2TextToImageRequest {
|
||||
GptImage2TextToImageRequest {
|
||||
prompt: "test".to_string(),
|
||||
num_images,
|
||||
image_size,
|
||||
quality,
|
||||
output_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
mod default_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_one_image() {
|
||||
// Default quality=High, size=Square => 22 cents
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, None, None)
|
||||
.calculate_cost_in_cents(), 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::Four, None, None)
|
||||
.calculate_cost_in_cents(), 88);
|
||||
}
|
||||
}
|
||||
|
||||
mod low_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_low_any_size_one_image() {
|
||||
// Low quality is always 1 cent per image regardless of size
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd))
|
||||
.calculate_cost_in_cents(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_low_any_size_four_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::Four, Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
mod medium_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_medium_landscape_4x3() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3))
|
||||
.calculate_cost_in_cents(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_square_hd() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd))
|
||||
.calculate_cost_in_cents(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_medium_portrait_16x9_two_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::Two, Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9))
|
||||
.calculate_cost_in_cents(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
mod high_quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_high_landscape_4x3() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3))
|
||||
.calculate_cost_in_cents(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_portrait_4x3() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3))
|
||||
.calculate_cost_in_cents(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_landscape_16x9() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9))
|
||||
.calculate_cost_in_cents(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_portrait_16x9() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9))
|
||||
.calculate_cost_in_cents(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square))
|
||||
.calculate_cost_in_cents(), 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_hd() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd))
|
||||
.calculate_cost_in_cents(), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_high_square_hd_three_images() {
|
||||
assert_eq!(
|
||||
make_request(GptImage2TextToImageNumImages::Three, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd))
|
||||
.calculate_cost_in_cents(), 69);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod api;
|
||||
mod raw_request;
|
||||
mod cost;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage2TextToImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// "low", "medium", "high"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality: Option<String>,
|
||||
|
||||
/// 1 - 4
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<u8>,
|
||||
|
||||
/// "jpeg", "png", "webp"
|
||||
/// Default: "png"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage2TextToImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage2TextToImageOutput {
|
||||
pub images: Vec<GptImage2TextToImageFile>,
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
pub mod flux_1_dev_text_to_image;
|
||||
pub mod flux_1_schnell_text_to_image;
|
||||
pub mod gpt_image_1_text_to_image;
|
||||
pub mod gpt_image_1p5_text_to_image;
|
||||
pub mod gpt_image_2_text_to_image;
|
||||
pub mod nano_banana_2_text_to_image;
|
||||
pub mod nano_banana_pro_text_to_image;
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use crate::requests::core_api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage1ByokEditImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// "auto", "1024x1024", "1536x1024", "1024x1536"
|
||||
pub image_size: String,
|
||||
|
||||
/// 1 - 4
|
||||
pub num_images: u8,
|
||||
|
||||
/// "auto", "low", "medium", "high"
|
||||
pub quality: String,
|
||||
|
||||
/// BYOK - caller supplies their own OpenAI API key
|
||||
pub openai_api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1ByokEditImageOutput {
|
||||
// Response fields parsed elsewhere
|
||||
}
|
||||
|
||||
pub fn gpt_image_1_byok_edit_image(
|
||||
params: GptImage1ByokEditImageInput,
|
||||
) -> FalRequest<GptImage1ByokEditImageInput, GptImage1ByokEditImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/edit-image/byok", params)
|
||||
}
|
||||
+33
-8
@@ -1,32 +1,57 @@
|
||||
use crate::requests::core_api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Non-BYOK edit-image (image-to-image) binding for
|
||||
/// `fal-ai/gpt-image-1/edit-image`. Fal hosts this model directly; pricing is
|
||||
/// billed by Fal at their published rates.
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage1EditImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mask_image_url: Option<String>,
|
||||
|
||||
/// "auto", "1024x1024", "1536x1024", "1024x1536"
|
||||
pub image_size: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// "low", "medium", "high"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality: Option<String>,
|
||||
|
||||
/// "low", "high"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_fidelity: Option<String>,
|
||||
|
||||
/// 1 - 4
|
||||
pub num_images: u8,
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<u8>,
|
||||
|
||||
/// "auto", "low", "medium", "high"
|
||||
pub quality: String,
|
||||
/// "auto", "transparent", "opaque"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<String>,
|
||||
|
||||
/// BYOK - caller supplies their own OpenAI API key
|
||||
pub openai_api_key: String,
|
||||
/// "jpeg", "png", "webp"
|
||||
/// Default: "png"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1EditImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1EditImageOutput {
|
||||
// Response fields parsed elsewhere
|
||||
pub images: Vec<GptImage1EditImageFile>,
|
||||
}
|
||||
|
||||
pub fn gpt_image_1_edit_image(
|
||||
params: GptImage1EditImageInput,
|
||||
) -> FalRequest<GptImage1EditImageInput, GptImage1EditImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/edit-image/byok", params)
|
||||
FalRequest::new("fal-ai/gpt-image-1/edit-image", params)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ pub mod http_flux_1_dev_edit_image;
|
||||
pub mod http_flux_1_schnell_edit_image;
|
||||
pub mod http_flux_pro_kontext_max_edit;
|
||||
pub mod http_gemini_25_flash_edit;
|
||||
pub mod http_gpt_image_1_byok_edit_image;
|
||||
pub mod http_gpt_image_1_edit_image;
|
||||
pub mod http_gpt_image_1_non_byok_edit_image;
|
||||
pub mod http_gpt_image_1p5_edit_image;
|
||||
pub mod http_gpt_image_2_edit_image;
|
||||
pub mod http_nano_banana_2_edit_image;
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
use crate::requests::core_api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage1ByokTextToImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// "auto", "1024x1024", "1536x1024", "1024x1536"
|
||||
pub image_size: String,
|
||||
|
||||
/// 1 - 4
|
||||
pub num_images: u8,
|
||||
|
||||
/// "auto", "low", "medium", "high"
|
||||
pub quality: String,
|
||||
|
||||
/// BYOK - caller supplies their own OpenAI API key
|
||||
pub openai_api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1ByokTextToImageOutput {
|
||||
// Response fields parsed elsewhere
|
||||
}
|
||||
|
||||
pub fn gpt_image_1_byok_text_to_image(
|
||||
params: GptImage1ByokTextToImageInput,
|
||||
) -> FalRequest<GptImage1ByokTextToImageInput, GptImage1ByokTextToImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/text-to-image/byok", params)
|
||||
}
|
||||
+27
-8
@@ -1,30 +1,49 @@
|
||||
use crate::requests::core_api::fal_request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Non-BYOK text-to-image binding for `fal-ai/gpt-image-1/text-to-image`.
|
||||
///
|
||||
/// Fal hosts this model directly (no caller-supplied OpenAI API key); pricing
|
||||
/// is billed by Fal at their published rates.
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct GptImage1TextToImageInput {
|
||||
pub prompt: String,
|
||||
|
||||
/// "auto", "1024x1024", "1536x1024", "1024x1536"
|
||||
pub image_size: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// "low", "medium", "high"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality: Option<String>,
|
||||
|
||||
/// 1 - 4
|
||||
pub num_images: u8,
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<u8>,
|
||||
|
||||
/// "auto", "low", "medium", "high"
|
||||
pub quality: String,
|
||||
/// "auto", "transparent", "opaque"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<String>,
|
||||
|
||||
/// BYOK - caller supplies their own OpenAI API key
|
||||
pub openai_api_key: String,
|
||||
/// "jpeg", "png", "webp"
|
||||
/// Default: "png"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1TextToImageFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GptImage1TextToImageOutput {
|
||||
// Response fields parsed elsewhere
|
||||
pub images: Vec<GptImage1TextToImageFile>,
|
||||
}
|
||||
|
||||
pub fn gpt_image_1_text_to_image(
|
||||
params: GptImage1TextToImageInput,
|
||||
) -> FalRequest<GptImage1TextToImageInput, GptImage1TextToImageOutput> {
|
||||
FalRequest::new("fal-ai/gpt-image-1/text-to-image/byok", params)
|
||||
FalRequest::new("fal-ai/gpt-image-1/text-to-image", params)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ pub mod http_flux_1_schnell_text_to_image;
|
||||
pub mod http_flux_pro_11_text_to_image;
|
||||
pub mod http_flux_pro_11_ultra_text_to_image;
|
||||
pub mod http_gemini_25_flash_text_to_image;
|
||||
pub mod http_gpt_image_1_non_byok_text_to_image;
|
||||
pub mod http_gpt_image_1_text_to_image;
|
||||
pub mod http_gpt_image_1_byok_text_to_image;
|
||||
pub mod http_gpt_image_1p5_text_to_image;
|
||||
pub mod http_gpt_image_2_text_to_image;
|
||||
pub mod http_nano_banana_2_text_to_image;
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use crate::requests::http::image::edit::http_gpt_image_1_edit_image::{gpt_image_1_edit_image, GptImage1EditImageInput};
|
||||
use crate::requests::http::image::edit::http_gpt_image_1_byok_edit_image::{gpt_image_1_byok_edit_image, GptImage1ByokEditImageInput};
|
||||
use crate::requests::core_api::webhook_response::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
@@ -101,7 +101,7 @@ pub async fn enqueue_gpt_image_1_byok_edit_image_webhook<V: IntoUrl>(
|
||||
GptEditImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = GptImage1EditImageInput {
|
||||
let request = GptImage1ByokEditImageInput {
|
||||
image_urls: req.image_urls,
|
||||
prompt: req.prompt,
|
||||
image_size: image_size.to_string(),
|
||||
@@ -110,7 +110,7 @@ pub async fn enqueue_gpt_image_1_byok_edit_image_webhook<V: IntoUrl>(
|
||||
openai_api_key: args.openai_api_key.0.to_string(),
|
||||
};
|
||||
|
||||
let result = gpt_image_1_edit_image(request)
|
||||
let result = gpt_image_1_byok_edit_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
+4
-4
@@ -2,8 +2,8 @@ use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::core_api::webhook_response::WebhookResponse;
|
||||
use crate::requests::http::image::edit::http_gpt_image_1_non_byok_edit_image::{
|
||||
gpt_image_1_non_byok_edit_image, GptImage1NonByokEditImageInput,
|
||||
use crate::requests::http::image::edit::http_gpt_image_1_edit_image::{
|
||||
gpt_image_1_edit_image, GptImage1EditImageInput,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{
|
||||
FalRequestCostCalculator, UsdCents,
|
||||
@@ -172,7 +172,7 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<R: IntoUrl>(
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let request = GptImage1NonByokEditImageInput {
|
||||
let request = GptImage1EditImageInput {
|
||||
prompt: req.prompt,
|
||||
image_urls: req.image_urls,
|
||||
mask_image_url: req.mask_image_url,
|
||||
@@ -184,7 +184,7 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<R: IntoUrl>(
|
||||
output_format,
|
||||
};
|
||||
|
||||
let result = gpt_image_1_non_byok_edit_image(request)
|
||||
let result = gpt_image_1_edit_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ use crate::creds::open_ai_api_key::OpenAiApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use crate::requests::http::image::text::http_gpt_image_1_text_to_image::{gpt_image_1_text_to_image, GptImage1TextToImageInput};
|
||||
use crate::requests::http::image::text::http_gpt_image_1_byok_text_to_image::{gpt_image_1_byok_text_to_image, GptImage1ByokTextToImageInput};
|
||||
use crate::requests::core_api::webhook_response::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
@@ -106,7 +106,7 @@ pub async fn enqueue_gpt_image_1_byok_text_to_image_webhook<V: IntoUrl>(
|
||||
GptTextToImageNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let request = GptImage1TextToImageInput {
|
||||
let request = GptImage1ByokTextToImageInput {
|
||||
prompt: req.prompt,
|
||||
image_size: image_size.to_string(),
|
||||
num_images,
|
||||
@@ -114,7 +114,7 @@ pub async fn enqueue_gpt_image_1_byok_text_to_image_webhook<V: IntoUrl>(
|
||||
openai_api_key: args.openai_api_key.0.to_string(),
|
||||
};
|
||||
|
||||
let result = gpt_image_1_text_to_image(request)
|
||||
let result = gpt_image_1_byok_text_to_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
+4
-4
@@ -2,8 +2,8 @@ use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::core_api::webhook_response::WebhookResponse;
|
||||
use crate::requests::http::image::text::http_gpt_image_1_non_byok_text_to_image::{
|
||||
gpt_image_1_non_byok_text_to_image, GptImage1NonByokTextToImageInput,
|
||||
use crate::requests::http::image::text::http_gpt_image_1_text_to_image::{
|
||||
gpt_image_1_text_to_image, GptImage1TextToImageInput,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{
|
||||
FalRequestCostCalculator, UsdCents,
|
||||
@@ -154,7 +154,7 @@ pub async fn enqueue_gpt_image_1_text_to_image_webhook<R: IntoUrl>(
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let request = GptImage1NonByokTextToImageInput {
|
||||
let request = GptImage1TextToImageInput {
|
||||
prompt: req.prompt,
|
||||
num_images: Some(num_images),
|
||||
image_size,
|
||||
@@ -163,7 +163,7 @@ pub async fn enqueue_gpt_image_1_text_to_image_webhook<R: IntoUrl>(
|
||||
output_format,
|
||||
};
|
||||
|
||||
let result = gpt_image_1_non_byok_text_to_image(request)
|
||||
let result = gpt_image_1_text_to_image(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user