fal redesign wip

This commit is contained in:
Brandon Thomas committed 2026-05-09 20:25:32 -04:00
1 parent 220cf66dfc
commit b8786e2e18
12 files changed
+348 -2

No files matched your search

+1
View File
@@ -1,6 +1,7 @@
[package]
name = "fal_client"
edition = "2021"
rust-version = "1.75" # MSRV: async traits
version = "0.0.1"
publish = false
@@ -0,0 +1,184 @@
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::api::image::angle::flux_2_lora_edit_image_angle::http::Flux2LoraEditImageAngleOutput;
use crate::requests::core_api::fal_request::FalRequest;
use crate::requests::core_api::webhook_response::WebhookResponse;
use crate::requests::http::image::angle::http_flux_2_lora_edit_image_angle::{http_flux_2_lora_edit_image_angle, Flux2LoraEditImageAngleInput};
use crate::requests::traits::fal_endpoint_trait::FalEndpoint;
use crate::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use reqwest::IntoUrl;
const ENDPOINT : &str = "fal-ai/flux-2-lora-gallery/multiple-angles";
#[derive(Clone, Debug)]
pub struct Flux2LoraEditImageAngleRequest {
// Request required
pub image_urls: Vec<String>,
// Camera parameters
pub horizontal_angle: Option<f64>,
pub vertical_angle: Option<f64>,
pub zoom: Option<f64>,
// Optional args
pub num_images: Option<Flux2LoraAngleNumImages>,
pub image_size: Option<Flux2LoraAngleImageSize>,
pub lora_scale: Option<f64>,
pub guidance_scale: Option<f64>,
pub num_inference_steps: Option<u32>,
}
#[derive(Copy, Clone, Debug)]
pub enum Flux2LoraAngleNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum Flux2LoraAngleImageSize {
Square,
SquareHd,
PortraitFourThree,
PortraitSixteenNine,
LandscapeFourThree,
LandscapeSixteenNine,
}
impl FalEndpoint for Flux2LoraEditImageAngleRequest {
type RawRequest = Flux2LoraEditImageAngleInput;
type RawResponse = Flux2LoraEditImageAngleOutput;
fn get_endpoint() -> &'static str {
ENDPOINT
}
fn to_request(&self) -> Result<FalRequest<Self::RawRequest, Self::RawResponse>, FalErrorPlus> {
let num_images = self.num_images
.map(|n| match n {
Flux2LoraAngleNumImages::One => 1,
Flux2LoraAngleNumImages::Two => 2,
Flux2LoraAngleNumImages::Three => 3,
Flux2LoraAngleNumImages::Four => 4,
});
let image_size = req.image_size
.map(|s| match s {
Flux2LoraAngleImageSize::Square => "square",
Flux2LoraAngleImageSize::SquareHd => "square_hd",
Flux2LoraAngleImageSize::PortraitFourThree => "portrait_4_3",
Flux2LoraAngleImageSize::PortraitSixteenNine => "portrait_16_9",
Flux2LoraAngleImageSize::LandscapeFourThree => "landscape_4_3",
Flux2LoraAngleImageSize::LandscapeSixteenNine => "landscape_16_9",
})
.map(|s| s.to_string());
let request = Flux2LoraEditImageAngleInput {
image_urls: req.image_urls,
horizontal_angle: req.horizontal_angle,
vertical_angle: req.vertical_angle,
zoom: req.zoom,
lora_scale: req.lora_scale,
image_size,
guidance_scale: req.guidance_scale,
num_inference_steps: req.num_inference_steps,
num_images,
// Constants
enable_safety_checker: Some(false),
output_format: Some("png".to_string()),
// Unused
seed: None,
};
let result = http_flux_2_lora_edit_image_angle(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
}
}
pub async fn enqueue_flux_2_lora_edit_image_angle_webhook<R: IntoUrl>(
args: EnqueueFlux2LoraEditImageAngleArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let req = args.request;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::angle::enqueue_flux_2_lora_edit_image_angle_webhook::{enqueue_flux_2_lora_edit_image_angle_webhook, EnqueueFlux2LoraAngleImageSize, EnqueueFlux2LoraAngleNumImages, EnqueueFlux2LoraEditImageAngleArgs, EnqueueFlux2LoraEditImageAngleRequest};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test_single() -> AnyhowResult<()> {
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueFlux2LoraEditImageAngleArgs {
request: EnqueueFlux2LoraEditImageAngleRequest {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(45.0),
vertical_angle: Some(15.0),
zoom: Some(5.0),
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let _result = enqueue_flux_2_lora_edit_image_angle_webhook(args).await?;
Ok(())
}
/// Enqueues 27 angle combinations: 3 horizontal x 3 vertical x 3 zoom levels.
#[tokio::test]
#[ignore]
async fn test_angle_grid() -> AnyhowResult<()> {
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let horizontal_angles: [f64; 3] = [-45.0, 0.0, 45.0];
let vertical_angles: [f64; 3] = [-30.0, 0.0, 30.0];
let zoom_levels: [f64; 3] = [0.0, 1.0, 3.0];
for &h in &horizontal_angles {
for &v in &vertical_angles {
for &z in &zoom_levels {
let args = EnqueueFlux2LoraEditImageAngleArgs {
request: EnqueueFlux2LoraEditImageAngleRequest {
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
horizontal_angle: Some(h),
vertical_angle: Some(v),
zoom: Some(z),
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
lora_scale: None,
guidance_scale: None,
num_inference_steps: None,
},
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let _result = enqueue_flux_2_lora_edit_image_angle_webhook(args).await?;
println!("Enqueued: h={}, v={}, z={}", h, v, z);
}
}
}
Ok(())
}
}
@@ -0,0 +1,18 @@
use crate::requests::api::image::angle::flux_2_lora_edit_image_angle::binding::{Flux2LoraAngleNumImages, Flux2LoraEditImageAngleRequest};
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
impl FalRequestCostCalculator for Flux2LoraEditImageAngleRequest {
fn calculate_cost_in_cents(&self) -> UsdCents {
// Pricing: $0.021 per megapixel.
// For a 1024x1024 image (~1 MP), that's ~2 cents per image.
let unit_cost = 2;
let cost = match self.num_images {
None => unit_cost,
Some(Flux2LoraAngleNumImages::One) => unit_cost,
Some(Flux2LoraAngleNumImages::Two) => unit_cost * 2,
Some(Flux2LoraAngleNumImages::Three) => unit_cost * 3,
Some(Flux2LoraAngleNumImages::Four) => unit_cost * 4,
};
cost as UsdCents
}
}
@@ -0,0 +1,82 @@
use crate::requests::core_api::fal_request::FalRequest;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Flux2LoraEditImageAngleInput {
/// The URL of the image to adjust camera angle for.
pub image_urls: Vec<String>,
/// Horizontal rotation angle around the object in degrees.
/// 0°=front view, 90°=right side, 180°=back view, 270°=left side.
/// Default: 0
#[serde(skip_serializing_if = "Option::is_none")]
pub horizontal_angle: Option<f64>,
/// Vertical camera angle in degrees.
/// 0°=eye-level shot, 30°=elevated shot, 60°=high-angle shot.
/// Default: 0
#[serde(skip_serializing_if = "Option::is_none")]
pub vertical_angle: Option<f64>,
/// Camera zoom/distance. 0=wide shot, 5=medium shot, 10=close-up.
/// Default: 5
#[serde(skip_serializing_if = "Option::is_none")]
pub zoom: Option<f64>,
/// The strength of the multiple angles effect.
/// Default: 1, range: 0-2
#[serde(skip_serializing_if = "Option::is_none")]
pub lora_scale: Option<f64>,
/// The size of the generated image.
/// Options: square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9
#[serde(skip_serializing_if = "Option::is_none")]
pub image_size: Option<String>,
/// The CFG (Classifier Free Guidance) scale.
/// Default: 2.5, range: 0-20
#[serde(skip_serializing_if = "Option::is_none")]
pub guidance_scale: Option<f64>,
/// The number of inference steps to perform.
/// Default: 40, range: 1-50
#[serde(skip_serializing_if = "Option::is_none")]
pub num_inference_steps: Option<u32>,
/// Random seed for reproducibility.
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
/// Number of images to generate.
/// Default: 1, max: 4
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// Whether to enable the safety checker.
/// Default: true
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_safety_checker: Option<bool>,
/// Output format: png, jpeg, webp.
/// Default: "png"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Flux2LoraEditImageAngleFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Flux2LoraEditImageAngleOutput {
pub images: Vec<Flux2LoraEditImageAngleFile>,
pub seed: u64,
pub prompt: Option<String>,
}
pub fn http_flux_2_lora_edit_image_angle(
params: Flux2LoraEditImageAngleInput,
) -> FalRequest<Flux2LoraEditImageAngleInput, Flux2LoraEditImageAngleOutput> {
FalRequest::new(, params)
}
@@ -0,0 +1,3 @@
pub mod binding;
mod http;
mod cost;
@@ -0,0 +1 @@
pub mod flux_2_lora_edit_image_angle;
@@ -0,0 +1 @@
pub mod angle;
@@ -0,0 +1 @@
pub mod image;
@@ -132,4 +132,29 @@ impl<Params: Serialize, Response: DeserializeOwned> FalRequest<Params, Response>
Ok(payload)
}
/// Submit the request to the Fal queue system.
pub async fn queue_request(self) -> Result<QueueResponse, FalError> {
let key = self
.api_key
.expect("No fal API key provided, and FAL_API_KEY environment variable is not set");
let response = self
.client
.post(format!("https://queue.fal.run/{}", self.endpoint))
.json(&self.params)
.header("Authorization", format!("Key {}", &key))
.header("Content-Type", "application/json")
.send()
.await?;
if response.status() != 200 {
let error = response.text().await?;
return Err(error.into());
}
let payload: QueueResponse = response.error_for_status()?.json().await?;
Ok(payload)
}
}
@@ -1,3 +1,4 @@
pub mod api;
pub mod core_api;
pub mod http;
pub mod traits;
@@ -1,5 +1,34 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::fal_error_plus::FalErrorPlus;
use crate::requests::core_api::fal_request::FalRequest;
use crate::requests::core_api::queue_response::QueueResponse;
use crate::requests::core_api::webhook_response::WebhookResponse;
use serde::de::DeserializeOwned;
use serde::Serialize;
pub trait FalEndpoint {
type RawRequest : Serialize;
type RawResponse : DeserializeOwned;
/// Return the endpoint, eg. `fal-ai/flux-2-lora-gallery/multiple-angles`
fn get_endpoint() -> &'static str;
/// Return the request-form of the typed request.
fn to_request(&self) -> Result<FalRequest<Self::RawRequest, Self::RawResponse>, FalErrorPlus>;
async fn send_webhook_request(&self, api_key: &FalApiKey, webhook_url: &str) -> Result<WebhookResponse, FalErrorPlus> {
let request = self.to_request()?;
let result = request.with_api_key(&api_key.0)
.queue_webhook(webhook_url)
.await?;
Ok(result)
}
async fn send_queue_request(&self, api_key: &FalApiKey) -> Result<QueueResponse, FalErrorPlus> {
let request = self.to_request()?;
let result = request.with_api_key(&api_key.0)
.queue_request()
.await?;
Ok(result)
}
}
@@ -343,10 +343,10 @@
"markdownDescription": "Denies the fetch_send command without any pre-configured scope."
},
{
"description": "This permission set configures what kind of\r\nfetch operations are available from the http plugin.\r\n\r\nThis enables all fetch operations but does not\r\nallow explicitly any origins to be fetched. This needs to\r\nbe manually configured before usage.\r\n\r\n#### Granted Permissions\r\n\r\nAll fetch operations are enabled.\r\n\r\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-read-body`\n- `allow-fetch-send`",
"description": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-read-body`\n- `allow-fetch-send`",
"type": "string",
"const": "default",
"markdownDescription": "This permission set configures what kind of\r\nfetch operations are available from the http plugin.\r\n\r\nThis enables all fetch operations but does not\r\nallow explicitly any origins to be fetched. This needs to\r\nbe manually configured before usage.\r\n\r\n#### Granted Permissions\r\n\r\nAll fetch operations are enabled.\r\n\r\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-read-body`\n- `allow-fetch-send`"
"markdownDescription": "This permission set configures what kind of\nfetch operations are available from the http plugin.\n\nThis enables all fetch operations but does not\nallow explicitly any origins to be fetched. This needs to\nbe manually configured before usage.\n\n#### Granted Permissions\n\nAll fetch operations are enabled.\n\n\n#### This default permission set includes:\n\n- `allow-fetch`\n- `allow-fetch-cancel`\n- `allow-fetch-read-body`\n- `allow-fetch-send`"
}
]
}