diff --git a/crates/api_clients/fal_client/Cargo.toml b/crates/api_clients/fal_client/Cargo.toml index 40e4e16761..32017398d8 100644 --- a/crates/api_clients/fal_client/Cargo.toml +++ b/crates/api_clients/fal_client/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "fal_client" edition = "2021" +rust-version = "1.75" # MSRV: async traits version = "0.0.1" publish = false diff --git a/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/binding.rs b/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/binding.rs new file mode 100644 index 0000000000..7277ed7dbf --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/binding.rs @@ -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, + + // Camera parameters + pub horizontal_angle: Option, + pub vertical_angle: Option, + pub zoom: Option, + + // Optional args + pub num_images: Option, + pub image_size: Option, + pub lora_scale: Option, + pub guidance_scale: Option, + pub num_inference_steps: Option, +} + +#[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, 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( + args: EnqueueFlux2LoraEditImageAngleArgs<'_, R> +) -> Result { + + 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(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/cost.rs b/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/cost.rs new file mode 100644 index 0000000000..d278373d4e --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/cost.rs @@ -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 + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/http.rs b/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/http.rs new file mode 100644 index 0000000000..a1b9df0476 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/http.rs @@ -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, + + /// 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, + + /// 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, + + /// Camera zoom/distance. 0=wide shot, 5=medium shot, 10=close-up. + /// Default: 5 + #[serde(skip_serializing_if = "Option::is_none")] + pub zoom: Option, + + /// The strength of the multiple angles effect. + /// Default: 1, range: 0-2 + #[serde(skip_serializing_if = "Option::is_none")] + pub lora_scale: Option, + + /// 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, + + /// The CFG (Classifier Free Guidance) scale. + /// Default: 2.5, range: 0-20 + #[serde(skip_serializing_if = "Option::is_none")] + pub guidance_scale: Option, + + /// The number of inference steps to perform. + /// Default: 40, range: 1-50 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_inference_steps: Option, + + /// Random seed for reproducibility. + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, + + /// Number of images to generate. + /// Default: 1, max: 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// Whether to enable the safety checker. + /// Default: true + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_safety_checker: Option, + + /// Output format: png, jpeg, webp. + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Flux2LoraEditImageAngleFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Flux2LoraEditImageAngleOutput { + pub images: Vec, + pub seed: u64, + pub prompt: Option, +} + +pub fn http_flux_2_lora_edit_image_angle( + params: Flux2LoraEditImageAngleInput, +) -> FalRequest { + FalRequest::new(, params) +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/mod.rs new file mode 100644 index 0000000000..2ca35b0115 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/angle/flux_2_lora_edit_image_angle/mod.rs @@ -0,0 +1,3 @@ +pub mod binding; +mod http; +mod cost; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/api/image/angle/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/angle/mod.rs new file mode 100644 index 0000000000..f9bb8ed4f8 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/angle/mod.rs @@ -0,0 +1 @@ +pub mod flux_2_lora_edit_image_angle; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/api/image/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/mod.rs new file mode 100644 index 0000000000..578dafcb55 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/mod.rs @@ -0,0 +1 @@ +pub mod angle; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/api/mod.rs b/crates/api_clients/fal_client/src/requests/api/mod.rs new file mode 100644 index 0000000000..b1b6b4f003 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/mod.rs @@ -0,0 +1 @@ +pub mod image; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/core_api/fal_request.rs b/crates/api_clients/fal_client/src/requests/core_api/fal_request.rs index df7dc8d6c5..8f4c087133 100644 --- a/crates/api_clients/fal_client/src/requests/core_api/fal_request.rs +++ b/crates/api_clients/fal_client/src/requests/core_api/fal_request.rs @@ -132,4 +132,29 @@ impl FalRequest Ok(payload) } + + /// Submit the request to the Fal queue system. + pub async fn queue_request(self) -> Result { + 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) + } } diff --git a/crates/api_clients/fal_client/src/requests/mod.rs b/crates/api_clients/fal_client/src/requests/mod.rs index 876d1dc69d..ce990e00a5 100644 --- a/crates/api_clients/fal_client/src/requests/mod.rs +++ b/crates/api_clients/fal_client/src/requests/mod.rs @@ -1,3 +1,4 @@ +pub mod api; pub mod core_api; pub mod http; pub mod traits; diff --git a/crates/api_clients/fal_client/src/requests/traits/fal_endpoint_trait.rs b/crates/api_clients/fal_client/src/requests/traits/fal_endpoint_trait.rs index 9c6f7c564d..c85c349ad4 100644 --- a/crates/api_clients/fal_client/src/requests/traits/fal_endpoint_trait.rs +++ b/crates/api_clients/fal_client/src/requests/traits/fal_endpoint_trait.rs @@ -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, FalErrorPlus>; + + async fn send_webhook_request(&self, api_key: &FalApiKey, webhook_url: &str) -> Result { + 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 { + let request = self.to_request()?; + let result = request.with_api_key(&api_key.0) + .queue_request() + .await?; + Ok(result) + } } diff --git a/crates/vendor/tauri-plugin-http/permissions/schemas/schema.json b/crates/vendor/tauri-plugin-http/permissions/schemas/schema.json index 31640bee16..ea774399d5 100644 --- a/crates/vendor/tauri-plugin-http/permissions/schemas/schema.json +++ b/crates/vendor/tauri-plugin-http/permissions/schemas/schema.json @@ -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`" } ] }