Compare commits

..
Author SHA1 Message Date
Brandon Thomas 98b0503d97 cleanup scripts 2026-01-04 16:07:29 -05:00
Brandon Thomas 5d3c168b5b billing -> support artcraft 2026-01-04 11:38:53 -05:00
Brandon Thomas e2a4e426dd missing providers in billing 2026-01-03 23:51:23 -05:00
Brandon Thomas fbad200dee fix soundregistry singleton issues from nx build 2026-01-03 20:06:01 -05:00
Brandon Thomas 9e0a5558a8 rust dev against production js build 2026-01-03 18:49:13 -05:00
Brandon Thomas 6c0b32aa44 worldlabs api url updates (#954) 2026-01-03 01:16:28 -05:00
Brandon Thomas 6f9b637da0 nuke dead tasks and dismiss them from view (#953) 2026-01-03 00:04:16 -05:00
Brandon Thomas 40dda6e5cd fix builds 2026-01-02 12:11:46 -05:00
Brandon Thomas 06fca6b54f generation count options (#952) 2026-01-02 11:46:43 -05:00
Brandon Thomas 7a04b131f7 rename nx module editor3d -> artcraft (#951) 2026-01-02 00:45:22 -05:00
Brandon Thomas 427269feec sort model selectors, default models (#950) 2026-01-01 22:48:44 -05:00
Brandon Thomas 2e2c433ffe flux and seedance aspect ratio options (#949) 2026-01-01 20:29:49 -05:00
Brandon Thomas c8228c5073 more model controls (#948)
still need to roll this out to everything, but this lands standardized
aspect ratio for several models
2025-12-31 07:03:34 -05:00
Brandon Thomas f89ce374cd animated discord icon 2025-12-31 03:31:08 -05:00
Brandon Thomas affcbac10f artcraft spin animation for discord 2025-12-31 03:26:08 -05:00
Brandon Thomas cbf91709dc nano banana frontend options, unified nano banana endpoint, new aspect ratio controls (#947) 2025-12-31 03:21:25 -05:00
BFlat eea31d3f94 billing blocker modal update (#946)
billing blocker update
2025-12-30 11:29:21 +07:00
Brandon Thomas f8f3434d70 random discord app icon 2025-12-29 22:06:02 -05:00
Brandon Thomas c6f3fd13b8 add assets for discord that will be deployed with website 2025-12-29 21:41:05 -05:00
Brandon Thomas fd8f9d037f fix stupid bug 2025-12-29 14:33:05 -05:00
bflatastic 9fa4080851 reverse capability for pre-ES2023 2025-12-29 23:25:23 +07:00
Brandon Thomas 73b16274f8 remove spurious billing tests 2025-12-29 03:49:29 -05:00
Brandon Thomas 835c9ef6b0 begin charging for video generations (#945) 2025-12-29 03:39:10 -05:00
Brandon Thomas 11bf5949fe select provider in frontend (#944) 2025-12-28 09:30:41 -05:00
Brandon Thomas 5415109a35 tauri command cleanup (#943)
this will aid in adding providers
2025-12-28 05:20:25 -05:00
bflatastic cb8c3b426f model selector to show provider submenu only when there's more than one, and commented out provider priority from settings modal 2025-12-28 11:31:10 +07:00
bflatastic 25ac4213af task queue buttons 2025-12-24 23:12:07 +07:00
bflatastic 2a316f6cc7 grid fixes, toast z-index on 3d world page, smaller onboarding helper and moved to above promptbox 2025-12-24 22:54:11 +07:00
bflatastic 2a1ae24917 spz file flip in editor, 3d viewer above grid and no event clickthrough 2025-12-23 00:37:33 +07:00
BFlat 68ff2b484b scrolling model list update (#942) 2025-12-22 16:59:53 +07:00
Brandon Thomas 19258c257c kling 2.5/2.6, veo3.1, sora2 in tauri + javascript (#941) 2025-12-22 04:46:56 -05:00
Brandon Thomas 4cd3bd8095 more video models (fal, server, client) - kling2.6/2.6, veo3.1, sora2 (#940) 2025-12-22 00:36:31 -05:00
Brandon Thomas e7dc0abb36 fix required rust never type behavior (#939) 2025-12-21 20:09:59 -05:00
bflatastic a36fe72832 lightbox 3d world button, nano banana name mappings with icon and fixed column width 2025-12-21 19:26:40 +07:00
bflatastic 4eeb915bef duplicated topbar on miniapps index page fix 2025-12-21 18:51:09 +07:00
bflatastic 19079ab7ec worldlabs icon and mapping 2025-12-21 18:38:34 +07:00
BFlat 9ffcfe83c2 bug fix: ImageTo3DWorldStore to use GaussianGenerationEvent (#938) 2025-12-21 18:28:16 +07:00
Brandon Thomas 3404935720 tauri hooks for gpt-image-1.5 and seedream4/4.5 (#937) 2025-12-20 16:06:25 -05:00
Brandon Thomas b490b92833 more models (again) - partial impl of gpt-image-1.5, seedream4/.5 (#936) 2025-12-20 14:12:24 -05:00
Brandon Thomas f151837c2b website links 2025-12-19 17:07:20 -05:00
Brandon Thomas 86a6d16441 refresh access token (#935) 2025-12-19 06:47:22 -05:00
Brandon Thomas b9397fb809 worldlabs improvements - login modal, refresh, etc. (#934) 2025-12-19 06:41:24 -05:00
Brandon Thomas 5688d8cef7 worldlabs tauri (#933)
not finished: need refresh tokens and thumbnails
2025-12-18 22:50:36 -05:00
BFlat 6815040cb1 3d world page and separate store (#932) 2025-12-18 00:29:25 +07:00
Brandon Thomas 754a4da019 worldlabs client library (#931) 2025-12-17 00:21:56 -05:00
bflatastic 3a600fcb9f remove background ui styling center update 2025-12-16 23:15:27 +07:00
Tanish Manku 510589b758 Splat persistence and preview support (#930)
Branch name is misleading as it was originally meant to upload splats to
the endpoint. It results in HTTP400 as of now.
2025-12-15 23:46:48 -08:00
BFlat 7ee5a6aa9a Feature/remove bg mini app (#929)
- remove background mini app
- button in lightbox to remove background leads to mini app as well
2025-12-16 05:45:35 +07:00
Tanish Manku f5cd65be9c Temporarily give splats meshes a name (#928) 2025-12-10 22:29:01 -08:00
Tanish Manku fba1a65e95 Fix bone controls not appearing since 3JS update (#927) 2025-12-10 22:14:26 -08:00
1451 changed files with 23336 additions and 3881 deletions
+2 -2
View File
@@ -200,10 +200,10 @@ jobs:
# Use npx to ensure we use the local nx version
echo "Building with npx nx..."
npx nx run editor3d:build
npx nx run artcraft:build
echo "Checking build output..."
ls -la apps/editor3d/dist/ | head -10
ls -la apps/artcraft/dist/ | head -10
# Update Tauri config to skip the build (since we already built)
cd ${{ github.workspace }}/crates/desktop/artcraft
@@ -75,10 +75,10 @@ jobs:
# Use npx to ensure we use the local nx version
echo "Building with npx nx..."
npx nx run editor3d:build
npx nx run artcraft:build
echo "Checking build output..."
dir apps/editor3d/dist/ | Select-Object -First 10
dir apps/artcraft/dist/ | Select-Object -First 10
# Update Tauri config to skip the build (since we already built)
cd ${{ github.workspace }}/crates/desktop/artcraft
@@ -104,4 +104,4 @@ jobs:
releaseBody: "Windows build - See the assets to download this version and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
args: ${{ matrix.args }}
+1 -1
View File
@@ -46,7 +46,7 @@ crates/lib/sqlite_queries/target/
node_modules/
# Frontend build outputs
frontend/apps/editor3d/bundle-report.html
frontend/apps/artcraft/bundle-report.html
# Vim noise
*.swo
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET task_status = \"cancelled_by_user\"\n WHERE task_status NOT IN (\n \"complete_success\",\n \"complete_failure\",\n \"dead\",\n \"cancelled_by_user\",\n \"cancelled_by_provider\",\n \"cancelled_by_us\"\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "30272128d83543d9fbfca930b7cdd4af30adfb4d90f2de044463a111c6340495"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET is_dismissed_by_user = 1\n WHERE is_dismissed_by_user != 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "f83018ec5493982659a56576296914f937bb4e75db3a522eb4bda486d5caa402"
}
Generated
+47
View File
@@ -698,6 +698,7 @@ dependencies = [
"url_utils",
"web_base64",
"workspace-build-acceleration",
"world_labs_client",
]
[[package]]
@@ -5151,6 +5152,14 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "http_headers"
version = "0.0.1"
dependencies = [
"errors",
"workspace-build-acceleration",
]
[[package]]
name = "http_server_common"
version = "0.0.1"
@@ -14165,6 +14174,44 @@ dependencies = [
"zstd-sys",
]
[[package]]
name = "world_labs_client"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.22.1",
"bstr",
"byteorder",
"chrono",
"cloudflare_errors",
"cloudflare_mitigation",
"cookie_store 0.0.1",
"env",
"env_logger",
"errors",
"filesys",
"futures 0.3.31",
"http_headers",
"indexmap 2.9.0",
"jwt_light",
"log",
"log4rs",
"once_cell",
"regex",
"scraper 0.24.0",
"serde",
"serde_json",
"sha2 0.11.0-rc.2",
"test_data",
"testing",
"tokio",
"url",
"uuid 1.18.1",
"workspace-build-acceleration",
"wreq",
"wreq-util",
]
[[package]]
name = "wreq"
version = "6.0.0-rc.20"
+4
View File
@@ -18,6 +18,7 @@ members = [
"crates/api_clients/openart_client",
"crates/api_clients/runwayml_client",
"crates/api_clients/thumbnail_generator",
"crates/api_clients/world_labs_client",
# CLI tools for operations, etc.
"crates/cli/devenv/dev-database-seed",
@@ -67,6 +68,7 @@ members = [
"crates/lib/files/media",
"crates/lib/files/mimetypes",
"crates/lib/files/videos",
"crates/lib/http_headers",
"crates/lib/idempotency",
"crates/lib/image_algorithms",
"crates/lib/jobs_common",
@@ -199,6 +201,7 @@ fal_client = { path = "crates/api_clients/fal_client" }
file_to_base64_url = { path = "crates/lib/files/file_to_base64_url" }
filesys = { path = "crates/lib/files/filesys" }
grok_client = { path = "crates/api_clients/grok_client" }
http_headers = { path = "crates/lib/http_headers" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
jwt_light = { path = "crates/lib/jwt_light" }
@@ -217,6 +220,7 @@ testing = { path = "crates/lib/testing" }
tokens = { path = "crates/schema/public/tokens" }
url_utils = { path = "crates/lib/url_utils" }
web_base64 = { path = "crates/lib/data/web_base64" }
world_labs_client = { path = "crates/api_clients/world_labs_client" }
# External libs
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
@@ -0,0 +1 @@
pub mod remove_image_background;
@@ -1,6 +1,6 @@
pub mod flux_pro_kontext_max_edit_image;
pub mod gpt_image_1_edit_image;
pub mod gemini_25_flash_edit_image;
pub mod gpt_image_1_edit_image;
pub mod qwen_edit_image;
pub mod seededit_3_edit_image;
@@ -1,9 +1,5 @@
pub mod bg_removal;
pub mod edit;
pub mod generate_flux_1_dev_text_to_image;
pub mod generate_flux_1_schnell_text_to_image;
pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod generate_gpt_image_1_text_to_image;
pub mod inpaint;
pub mod multi_function;
pub mod remove_image_background;
pub mod text;
@@ -0,0 +1,69 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const BYTEDANCE_SEEDREAM_V4_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/bytedance_seedream_4";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedreamV4MultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context.
/// If present, we're doing image editing (image-to-image / image-editing)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<BytedanceSeedreamV4MultiFunctionImageGenNumImages>,
pub max_images: Option<BytedanceSeedreamV4MultiFunctionImageGenMaxImages>,
pub image_size: Option<BytedanceSeedreamV4MultiFunctionImageGenImageSize>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4MultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4MultiFunctionImageGenMaxImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4MultiFunctionImageGenImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto,
Auto2k,
Auto4k,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedreamV4MultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,68 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const BYTEDANCE_SEEDREAM_V4P5_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/bytedance_seedream_4p5";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context.
/// If present, we're doing image editing (image-to-image / image-editing)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<BytedanceSeedreamV4p5MultiFunctionImageGenNumImages>,
pub max_images: Option<BytedanceSeedreamV4p5MultiFunctionImageGenMaxImages>,
pub image_size: Option<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4p5MultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4p5MultiFunctionImageGenMaxImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedreamV4p5MultiFunctionImageGenImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto2k,
Auto4k,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedreamV4p5MultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,95 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GPT_IMAGE_1P5_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/gpt_image_1p5";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GptImage1p5MultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context (**OR INPAINTING CONTEXT!**).
/// If present, we're doing image editing (image-to-image / image-editing / image-inpainting)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Only for image editing, which turns this into an inpainting task.
/// Text to image should not set this.
pub mask_image_token: Option<MediaFileToken>,
/// Number of images to generate. Default is one.
pub num_images: Option<GptImage1p5MultiFunctionImageGenNumImages>,
pub image_size: Option<GptImage1p5MultiFunctionImageGenSize>,
pub background: Option<GptImage1p5MultiFunctionImageGenBackground>,
pub quality: Option<GptImage1p5MultiFunctionImageGenQuality>,
/// Only for image editing.
pub input_fidelity: Option<GptImage1p5MultiFunctionImageGenInputFidelity>,
pub output_format: Option<GptImage1p5MultiFunctionImageGenOutputFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenSize {
/// 1024x1024
Square,
/// 1536x1024
Wide,
/// 1024x1536
Tall,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenBackground {
Auto,
Transparent,
Opaque,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenQuality {
Low,
Medium,
High,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenInputFidelity {
Low,
High,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GptImage1p5MultiFunctionImageGenOutputFormat {
Jpeg,
Png,
Webp,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GptImage1p5MultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1 +1,5 @@
pub mod bytedance_seedream_v4_multi_function_image_gen;
pub mod bytedance_seedream_v4p5_multi_function_image_gen;
pub mod gpt_image_1p5_multi_function_image_gen;
pub mod nano_banana_multi_function_image_gen;
pub mod nano_banana_pro_multi_function_image_gen;
@@ -0,0 +1,62 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const NANO_BANANA_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/nano_banana";
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct NanoBananaMultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context.
/// If present, we're doing image editing (image-to-image / image-editing)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<NanoBananaMultiFunctionImageGenNumImages>,
/// Aspect ratio of the images to generate. Default is "1:1"
pub aspect_ratio: Option<NanoBananaMultiFunctionImageGenAspectRatio>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaMultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaMultiFunctionImageGenAspectRatio {
// Auto (only for image editing)
Auto, // Default for image editing.
// Square
OneByOne, // Default for text-to-image
// Wide
FiveByFour,
FourByThree,
ThreeByTwo,
SixteenByNine,
TwentyOneByNine,
// Tall
FourByFive,
ThreeByFour,
TwoByThree,
NineBySixteen, // NB: No NineByTwentyOne ?
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct NanoBananaMultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -5,7 +5,7 @@ use utoipa::ToSchema;
pub const NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/nano_banana_pro";
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct NanoBananaProMultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
@@ -48,8 +48,10 @@ pub enum NanoBananaProMultiFunctionImageGenImageResolution {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaProMultiFunctionImageGenAspectRatio {
// Auto (only for image editing)
Auto, // Default for image editing.
// Square
OneByOne, // Default
OneByOne, // Default for text-to-image
// Wide
FiveByFour,
FourByThree,
@@ -0,0 +1,50 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GEMINI_25_FLASH_EDIT_IMAGE_PATH: &str = "/v1/generate/image/gemini_25_flash";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Gemini25FlashTextToImageImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
// TODO: OneOf for image_urls or image_media_tokens
/// Image media tokens to include in the editing context.
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<Gemini25FlashTextToImageImageNumImages>,
/// Quality of the image to generate. Default is High.
pub image_quality: Option<Gemini25FlashTextToImageImageImageQuality>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Gemini25FlashTextToImageImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Gemini25FlashTextToImageImageImageQuality {
Auto,
Low,
Medium,
High, // Default
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Gemini25FlashTextToImageImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -19,6 +19,7 @@ pub struct GenerateFluxPro11TextToImageRequest {
pub num_images: Option<GenerateFluxPro11TextToImageNumImages>,
}
// TODO(btw,2026-01-01): Fal.ai and Fal.rs client libraries seem to disagree about the options.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateFluxPro11TextToImageAspectRatio {
@@ -23,10 +23,14 @@ pub struct GenerateFluxPro11UltraTextToImageRequest {
#[serde(rename_all = "snake_case")]
pub enum GenerateFluxPro11UltraTextToImageAspectRatio {
Square, // 1:1
LandscapeThreeByTwo, // 3:2
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
LandscapeTwentyOneByNine, // 21:9
PortraitTwoByThree, // 2:3
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
PortraitNineByTwentyOne, // 9:21
//Custom { width: u32, height: u32 }, // TODO
}
@@ -0,0 +1,6 @@
pub mod gemini_25_flash_text_to_image;
pub mod generate_flux_1_dev_text_to_image;
pub mod generate_flux_1_schnell_text_to_image;
pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod generate_gpt_image_1_text_to_image;
@@ -21,11 +21,14 @@ pub struct GenerateSeedance10LiteImageToVideoRequest {
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: aspect ratio of the generated video.
/// Optional: resolution of the generated video.
pub resolution: Option<GenerateSeedance10LiteResolution>,
/// Optional: aspect ratio of the generated video.
/// Optional: duration of the generated video.
pub duration: Option<GenerateSeedance10LiteDuration>,
/// Optional: aspect ratio of the generated video.
pub aspect_ratio: Option<GenerateSeedance10LiteAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
@@ -42,6 +45,19 @@ pub enum GenerateSeedance10LiteDuration {
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateSeedance10LiteAspectRatio {
Auto,
TwentyOneByNine,
SixteenByNine,
FourByThree,
Square,
ThreeByFour,
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateSeedance10LiteImageToVideoResponse {
pub success: bool,
@@ -5,4 +5,5 @@ pub mod generate_seedance_1_0_lite_image_to_video;
pub mod generate_seedance_1_0_pro_image_to_video;
pub mod generate_veo_2_image_to_video;
pub mod generate_veo_3_fast_image_to_video;
pub mod generate_veo_3_image_to_video;
pub mod generate_veo_3_image_to_video;
pub mod multi_function;
@@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const KLING_2P5_TURBO_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH: &str = "/v1/generate/video/multi_function/kling_2p5_turbo_pro";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Kling2p5TurboProMultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// "Required".
/// Required downstream, but we'll coerce null to empty string.
/// Text prompt.
pub prompt: Option<String>,
/// Optional.
/// Only for text-to-video.
pub negative_prompt: Option<String>,
/// Optional.
/// Only for image-to-video
/// Source of the starting frame
/// If present, we're doing image-to-video
/// If absent, we're doing text-to-video
pub start_frame_image_media_token: Option<MediaFileToken>,
/// Optional.
/// Only for image-to-video
/// Source of the ending frame (if desired)
pub end_frame_image_media_token: Option<MediaFileToken>,
/// Optional
/// Duration of the video
pub duration: Option<Kling2p5TurboProMultiFunctionVideoGenDuration>,
/// Optional.
/// Only for text-to-video
pub aspect_ratio: Option<Kling2p5TurboProMultiFunctionVideoGenAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling2p5TurboProMultiFunctionVideoGenDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling2p5TurboProMultiFunctionVideoGenAspectRatio {
Square,
SixteenByNine,
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Kling2p5TurboProMultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,68 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const KLING_2P6_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH: &str = "/v1/generate/video/multi_function/kling_2p6_pro";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Kling2p6ProMultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// "Required".
/// Required downstream, but we'll coerce null to empty string.
/// Text prompt.
pub prompt: Option<String>,
/// Optional.
/// Only for text-to-video.
pub negative_prompt: Option<String>,
/// Optional.
/// Only for image-to-video
/// Source of the starting frame
/// If present, we're doing image-to-video
/// If absent, we're doing text-to-video
pub start_frame_image_media_token: Option<MediaFileToken>,
// /// Optional.
// /// Only for image-to-video
// /// Source of the ending frame (if desired)
// pub end_frame_image_media_token: Option<MediaFileToken>,
/// Optional
/// Duration of the video
pub duration: Option<Kling2p6ProMultiFunctionVideoGenDuration>,
/// Optional
/// Whether to generate audio.
pub generate_audio: Option<bool>,
/// Optional.
/// Only for text-to-video
pub aspect_ratio: Option<Kling2p6ProMultiFunctionVideoGenAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling2p6ProMultiFunctionVideoGenDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling2p6ProMultiFunctionVideoGenAspectRatio {
Square,
SixteenByNine,
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Kling2p6ProMultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,6 @@
pub mod kling_2_5_turbo_multi_function_video_gen;
pub mod kling_2_6_multi_function_video_gen;
pub mod sora_2_multi_function_video_gen;
pub mod sora_2_pro_multi_function_video_gen;
pub mod veo_3p1_fast_multi_function_video_gen;
pub mod veo_3p1_multi_function_video_gen;
@@ -0,0 +1,66 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const SORA_2_MULTI_FUNCTION_VIDEO_VIDEO_PATH: &str = "/v1/generate/video/multi_function/sora_2";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Sora2MultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// "Required".
/// Required downstream, but we'll coerce null to empty string.
/// Text prompt.
pub prompt: Option<String>,
/// Optional.
/// Only for image-to-video
/// Source of the starting frame
/// If present, we're doing image-to-video
/// If absent, we're doing text-to-video
pub image_media_token: Option<MediaFileToken>,
/// Optional.
pub resolution: Option<Sora2MultiFunctionVideoGenResolution>,
/// Optional
/// Duration of the video
/// (this is uniform against all modes)
pub duration: Option<Sora2MultiFunctionVideoGenDuration>,
/// Optional.
pub aspect_ratio: Option<Sora2MultiFunctionVideoGenAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2MultiFunctionVideoGenResolution {
Auto,
SevenTwentyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2MultiFunctionVideoGenDuration {
FourSeconds,
EightSeconds,
TwelveSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2MultiFunctionVideoGenAspectRatio {
Auto,
NineBySixteen,
SixteenByNine,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Sora2MultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,67 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const SORA_2_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH: &str = "/v1/generate/video/multi_function/sora_2_pro";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Sora2ProMultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// "Required".
/// Required downstream, but we'll coerce null to empty string.
/// Text prompt.
pub prompt: Option<String>,
/// Optional.
/// Only for image-to-video
/// Source of the starting frame
/// If present, we're doing image-to-video
/// If absent, we're doing text-to-video
pub image_media_token: Option<MediaFileToken>,
/// Optional.
pub resolution: Option<Sora2ProMultiFunctionVideoGenResolution>,
/// Optional
/// Duration of the video
/// (this is uniform against all modes)
pub duration: Option<Sora2ProMultiFunctionVideoGenDuration>,
/// Optional.
pub aspect_ratio: Option<Sora2ProMultiFunctionVideoGenAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2ProMultiFunctionVideoGenResolution {
Auto,
SevenTwentyP,
TenEightyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2ProMultiFunctionVideoGenDuration {
FourSeconds,
EightSeconds,
TwelveSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2ProMultiFunctionVideoGenAspectRatio {
Auto,
NineBySixteen,
SixteenByNine,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Sora2ProMultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,90 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const VEO_3P1_FAST_MULTI_FUNCTION_VIDEO_VIDEO_PATH: &str = "/v1/generate/video/multi_function/veo_3p1_fast";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Veo3p1FastMultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// "Required".
/// Required downstream, but we'll coerce null to empty string.
/// Text prompt.
pub prompt: Option<String>,
/// Optional.
/// Only for text-to-video.
pub negative_prompt: Option<String>,
/// Optional.
/// Only for image-to-video
/// Source of the starting frame
/// If present, we're doing image-to-video
/// If absent, we're doing text-to-video
pub start_frame_image_media_token: Option<MediaFileToken>,
/// Optional.
/// Only for image-to-video
/// Source of the ending frame (if desired)
pub end_frame_image_media_token: Option<MediaFileToken>,
/// Optional
/// Duration of the video
/// (this is uniform against all modes)
pub duration: Option<Veo3p1FastMultiFunctionVideoGenDuration>,
/// Optional.
pub aspect_ratio: Option<Veo3p1FastMultiFunctionVideoGenAspectRatio>,
/// Optional.
pub resolution: Option<Veo3p1FastMultiFunctionVideoGenResolution>,
/// Optional
/// Whether to generate audio.
pub generate_audio: Option<bool>,
/// Optional
/// Only for text-to-video
pub enhance_prompt: Option<bool>,
/// Optional
/// Only for text-to-video
pub seed: Option<i64>,
/// Optional
/// Only for text-to-video
pub auto_fix: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1FastMultiFunctionVideoGenDuration {
FourSeconds,
SixSeconds,
EightSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1FastMultiFunctionVideoGenAspectRatio {
Auto,
SixteenByNine,
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1FastMultiFunctionVideoGenResolution {
SevenTwentyP,
TenEightyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Veo3p1FastMultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,90 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const VEO_3P1_MULTI_FUNCTION_VIDEO_VIDEO_PATH: &str = "/v1/generate/video/multi_function/veo_3p1";
/// Both text-to-video and image-to-video in one request.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Veo3p1MultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// "Required".
/// Required downstream, but we'll coerce null to empty string.
/// Text prompt.
pub prompt: Option<String>,
/// Optional.
/// Only for text-to-video.
pub negative_prompt: Option<String>,
/// Optional.
/// Only for image-to-video
/// Source of the starting frame
/// If present, we're doing image-to-video
/// If absent, we're doing text-to-video
pub start_frame_image_media_token: Option<MediaFileToken>,
/// Optional.
/// Only for image-to-video
/// Source of the ending frame (if desired)
pub end_frame_image_media_token: Option<MediaFileToken>,
/// Optional
/// Duration of the video
/// (this is uniform against all modes)
pub duration: Option<Veo3p1MultiFunctionVideoGenDuration>,
/// Optional.
pub aspect_ratio: Option<Veo3p1MultiFunctionVideoGenAspectRatio>,
/// Optional.
pub resolution: Option<Veo3p1MultiFunctionVideoGenResolution>,
/// Optional
/// Whether to generate audio.
pub generate_audio: Option<bool>,
/// Optional
/// Only for text-to-video
pub enhance_prompt: Option<bool>,
/// Optional
/// Only for text-to-video
pub seed: Option<i64>,
/// Optional
/// Only for text-to-video
pub auto_fix: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1MultiFunctionVideoGenDuration {
FourSeconds,
SixSeconds,
EightSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1MultiFunctionVideoGenAspectRatio {
Auto,
SixteenByNine,
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1MultiFunctionVideoGenResolution {
SevenTwentyP,
TenEightyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Veo3p1MultiFunctionVideoGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1 @@
pub mod remove_image_background;
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::remove_image_background::{RemoveImageBackgroundRequest, RemoveImageBackgroundResponse, REMOVE_IMAGE_BACKGROUND_PATH};
use artcraft_api_defs::generate::image::bg_removal::remove_image_background::{RemoveImageBackgroundRequest, RemoveImageBackgroundResponse, REMOVE_IMAGE_BACKGROUND_PATH};
pub async fn remove_image_background(
api_host: &ApiHost,
@@ -1,9 +1,5 @@
pub mod bg_removal;
pub mod edit;
pub mod generate_flux_1_dev_text_to_image;
pub mod generate_flux_1_schnell_text_to_image;
pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod generate_gpt_image_1_text_to_image;
pub mod inpaint;
pub mod multi_function;
pub mod remove_image_background;
pub mod text;
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{BytedanceSeedreamV4MultiFunctionImageGenRequest, BytedanceSeedreamV4MultiFunctionImageGenResponse, BYTEDANCE_SEEDREAM_V4_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn bytedance_seedream_v4_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: BytedanceSeedreamV4MultiFunctionImageGenRequest,
) -> Result<BytedanceSeedreamV4MultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
BYTEDANCE_SEEDREAM_V4_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{BytedanceSeedreamV4p5MultiFunctionImageGenRequest, BytedanceSeedreamV4p5MultiFunctionImageGenResponse, BYTEDANCE_SEEDREAM_V4P5_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn bytedance_seedream_v4p5_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: BytedanceSeedreamV4p5MultiFunctionImageGenRequest,
) -> Result<BytedanceSeedreamV4p5MultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
BYTEDANCE_SEEDREAM_V4P5_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{GptImage1p5MultiFunctionImageGenRequest, GptImage1p5MultiFunctionImageGenResponse, GPT_IMAGE_1P5_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn gpt_image_1p5_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: GptImage1p5MultiFunctionImageGenRequest,
) -> Result<GptImage1p5MultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GPT_IMAGE_1P5_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -1 +1,5 @@
pub mod bytedance_seedream_v4_multi_function_image_gen_image;
pub mod bytedance_seedream_v4p5_multi_function_image_gen_image;
pub mod gpt_image_1p5_multi_function_image_gen_image;
pub mod nano_banana_multi_function_image_gen_image;
pub mod nano_banana_pro_multi_function_image_gen_image;
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{NanoBananaMultiFunctionImageGenRequest, NanoBananaMultiFunctionImageGenResponse, NANO_BANANA_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn nano_banana_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: NanoBananaMultiFunctionImageGenRequest,
) -> Result<NanoBananaMultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
NANO_BANANA_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse, GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH};
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{GenerateFlux1DevTextToImageRequest, GenerateFlux1DevTextToImageResponse, GENERATE_FLUX_1_DEV_TEXT_TO_IMAGE_PATH};
pub async fn generate_flux_1_dev_text_to_image(
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
pub async fn generate_flux_1_schnell_text_to_image(
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageRequest, GenerateFluxPro11TextToImageResponse, GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH};
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{GenerateFluxPro11TextToImageRequest, GenerateFluxPro11TextToImageResponse, GENERATE_FLUX_PRO_11_TEXT_TO_IMAGE_PATH};
pub async fn generate_flux_pro_11_text_to_image(
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageRequest, GenerateFluxPro11UltraTextToImageResponse, GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH};
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{GenerateFluxPro11UltraTextToImageRequest, GenerateFluxPro11UltraTextToImageResponse, GENERATE_FLUX_PRO_11_ULTRA_TEXT_TO_IMAGE_PATH};
pub async fn generate_flux_pro_11_ultra_text_to_image(
api_host: &ApiHost,
@@ -2,7 +2,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageRequest, GenerateGptImage1TextToImageResponse, GPT_IMAGE_1_TEXT_TO_IMAGE_PATH};
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::{GenerateGptImage1TextToImageRequest, GenerateGptImage1TextToImageResponse, GPT_IMAGE_1_TEXT_TO_IMAGE_PATH};
pub async fn generate_gpt_image_1_text_to_image(
@@ -0,0 +1,5 @@
pub mod generate_flux_1_dev_text_to_image;
pub mod generate_flux_1_schnell_text_to_image;
pub mod generate_flux_pro_11_text_to_image;
pub mod generate_flux_pro_11_ultra_text_to_image;
pub mod generate_gpt_image_1_text_to_image;
@@ -5,3 +5,4 @@ pub mod generate_seedance_1_0_lite_image_to_video;
pub mod generate_veo_2_image_to_video;
pub mod generate_veo_3_fast_image_to_video;
pub mod generate_veo_3_image_to_video;
pub mod multi_function;
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::kling_2_5_turbo_multi_function_video_gen::{Kling2p5TurboProMultiFunctionVideoGenRequest, Kling2p5TurboProMultiFunctionVideoGenResponse, KLING_2P5_TURBO_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH};
pub async fn kling_2p5_turbo_pro_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Kling2p5TurboProMultiFunctionVideoGenRequest,
) -> Result<Kling2p5TurboProMultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
KLING_2P5_TURBO_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::kling_2_6_multi_function_video_gen::{Kling2p6ProMultiFunctionVideoGenRequest, Kling2p6ProMultiFunctionVideoGenResponse, KLING_2P6_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH};
pub async fn kling_2p6_pro_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Kling2p6ProMultiFunctionVideoGenRequest,
) -> Result<Kling2p6ProMultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
KLING_2P6_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,6 @@
pub mod kling_2p5_turbo_pro_multi_function_video_gen;
pub mod kling_2p6_pro_multi_function_video_gen;
pub mod sora_2_multi_function_video_gen;
pub mod sora_2_pro_multi_function_video_gen;
pub mod veo_3p1_fast_multi_function_video_gen;
pub mod veo_3p1_multi_function_video_gen;
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::sora_2_multi_function_video_gen::{Sora2MultiFunctionVideoGenRequest, Sora2MultiFunctionVideoGenResponse, SORA_2_MULTI_FUNCTION_VIDEO_VIDEO_PATH};
pub async fn sora_2_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Sora2MultiFunctionVideoGenRequest,
) -> Result<Sora2MultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
SORA_2_MULTI_FUNCTION_VIDEO_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::sora_2_pro_multi_function_video_gen::{Sora2ProMultiFunctionVideoGenRequest, Sora2ProMultiFunctionVideoGenResponse, SORA_2_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH};
pub async fn sora_2_pro_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Sora2ProMultiFunctionVideoGenRequest,
) -> Result<Sora2ProMultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
SORA_2_PRO_MULTI_FUNCTION_VIDEO_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::veo_3p1_fast_multi_function_video_gen::{Veo3p1FastMultiFunctionVideoGenRequest, Veo3p1FastMultiFunctionVideoGenResponse, VEO_3P1_FAST_MULTI_FUNCTION_VIDEO_VIDEO_PATH};
pub async fn veo_3p1_fast_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Veo3p1FastMultiFunctionVideoGenRequest,
) -> Result<Veo3p1FastMultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
VEO_3P1_FAST_MULTI_FUNCTION_VIDEO_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,18 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::video::multi_function::veo_3p1_multi_function_video_gen::{Veo3p1MultiFunctionVideoGenRequest, Veo3p1MultiFunctionVideoGenResponse, VEO_3P1_MULTI_FUNCTION_VIDEO_VIDEO_PATH};
pub async fn veo_3p1_multi_function_video_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Veo3p1MultiFunctionVideoGenRequest,
) -> Result<Veo3p1MultiFunctionVideoGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
VEO_3P1_MULTI_FUNCTION_VIDEO_VIDEO_PATH,
maybe_creds,
request,
).await?)
}
@@ -0,0 +1,143 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::api_error::ApiError;
use crate::error::client_error::ClientError;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::http_get_anonymous::http_get_anonymous;
use chrono::{DateTime, Utc};
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::common::generation_provider::GenerationProvider;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
use idempotency::uuid::generate_random_uuid;
use log::debug;
use reqwest::multipart::Form;
use reqwest::Client;
use serde_derive::{Deserialize, Serialize};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::prompts::PromptToken;
use uuid::uuid;
pub struct LegacyUploadMediaFileFromFileArgs<'a, P: AsRef<Path>> {
pub api_host: &'a ApiHost,
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
// NB: Path needs to be owned for the request.
pub path: P,
// /// If true, we should hide the image from the user's gallery.
// pub is_intermediate_system_file: bool,
// /// If provided, this is the prompt that this image is associated with.
// /// NOTE: Cannot set `is_intermediate_system_file = true` if this is set.
// pub maybe_prompt_token: Option<&'a PromptToken>,
// /// If provided, this is the service provider that created the image.
// /// NOTE: Cannot set `is_intermediate_system_file = true` if this is set.
// pub maybe_generation_provider: Option<GenerationProvider>,
// /// If provided, this groups the file into a batch
// /// TODO: This shouldn't be set clientside without the backend generating the token
// /// and cryptographically securing it. But we need to go fast here.
// pub maybe_batch_token: Option<&'a BatchGenerationToken>,
}
/// Upload using the legacy endpoint (spz, etc.)
pub async fn legacy_upload_media_file_from_file<P: AsRef<Path>>(
args: LegacyUploadMediaFileFromFileArgs<'_, P>,
) -> Result<UploadImageMediaFileSuccessResponse, StorytellerError> {
validate_args(&args)?;
let url = get_route(args.api_host);
debug!("Requesting {:?}", &url);
//let mut file = File::open(path)?;
let client = Client::builder()
.gzip(true)
.build()
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
let mut form = Form::new()
.text("uuid_idempotency_token", generate_random_uuid())
.file("file", args.path)
.await
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
//if args.is_intermediate_system_file {
// form = form.text("is_intermediate_system_file", "true");
//}
//if let Some(prompt_token) = &args.maybe_prompt_token {
// form = form.text("maybe_prompt_token", prompt_token.to_string());
//}
//if let Some(batch_token) = &args.maybe_batch_token {
// form = form.text("maybe_batch_token", batch_token.to_string());
//}
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON);
if let Some(creds) = args.maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let response = request_builder
.multipart(form)
.send()
.await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let response = filter_bad_response(response).await?;
let response_body = &response.text().await
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
let media_file = serde_json::from_str(&response_body)
.map_err(|err| StorytellerError::Api(ApiError::from(err)))?;
Ok(media_file)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname_and_scheme = api_host.to_api_hostname_and_scheme();
format!("{}/v1/media_files/upload", api_hostname_and_scheme)
}
// TODO(bt,2025-04-22): Share API definitions between client and server in common crate.
#[derive(Deserialize, Debug)]
pub struct UploadImageMediaFileSuccessResponse {
pub success: bool,
pub media_file_token: MediaFileToken,
}
fn validate_args<P: AsRef<Path>>(
args: &LegacyUploadMediaFileFromFileArgs<'_, P>,
) -> Result<(), StorytellerError> {
// if args.is_intermediate_system_file && args.maybe_prompt_token.is_some() {
// return Err(StorytellerError::Client(ClientError::InvalidPreflightRequest(
// "Cannot set `is_intermediate_system_file` to true if `maybe_prompt_token` is provided."
// .to_string())));
// }
Ok(())
}
@@ -5,3 +5,4 @@ pub mod upload_image_media_file_from_bytes;
pub mod upload_image_media_file_from_file;
pub mod upload_new_engine_asset_from_file;
pub mod upload_video_media_file_from_file;
pub mod legacy_upload_media_file_from_file;
@@ -5,7 +5,6 @@ use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
use log::{debug, info};
use reqwest::Client;
use serde::de::DeserializeOwned;
@@ -5,7 +5,6 @@ use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
use log::debug;
use reqwest::Client;
use serde::de::DeserializeOwned;
@@ -5,7 +5,6 @@ use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
use log::{debug, info};
use reqwest::Client;
use serde::de::DeserializeOwned;
@@ -5,7 +5,6 @@ use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use artcraft_api_defs::generate::image::generate_flux_1_schnell_text_to_image::{GenerateFlux1SchnellTextToImageRequest, GenerateFlux1SchnellTextToImageResponse, GENERATE_FLUX_1_SCHNELL_TEXT_TO_IMAGE_PATH};
use errors::AnyhowResult;
use log::{debug, info};
use reqwest::Client;
@@ -1,2 +1,3 @@
pub mod queue;
pub mod webhook;
pub mod webhook;
pub mod traits;
@@ -0,0 +1,9 @@
/// Cost in pennies.
pub type UsdCents = u64;
pub trait FalRequestCostCalculator {
/// Calculate the cost of the request.
fn calculate_cost_in_cents(&self) -> UsdCents;
}
@@ -0,0 +1 @@
pub mod fal_request_cost_calculator_trait;
@@ -11,6 +11,9 @@ pub struct Gemini25FlashEditArgs<'a, R: IntoUrl> {
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub num_images: Gemini25FlashEditNumImages,
// Optional
pub aspect_ratio: Option<Gemini25FlashEditAspectRatio>,
// Fulfillment
pub webhook_url: R,
@@ -25,6 +28,26 @@ pub enum Gemini25FlashEditNumImages {
Four,
}
/// auto, 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16
/// Default is "auto"
#[derive(Copy, Clone, Debug)]
pub enum Gemini25FlashEditAspectRatio {
// Automatic (default)
Auto,
// Square
OneByOne,
// Wide
FiveByFour,
FourByThree,
ThreeByTwo,
SixteenByNine,
TwentyOneByNine,
// Tall
FourByFive,
ThreeByFour,
TwoByThree,
NineBySixteen, // NB: No NineByTwentyOne ?
}
pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
args: Gemini25FlashEditArgs<'_, R>
@@ -37,9 +60,30 @@ pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
Gemini25FlashEditNumImages::Four => 4,
};
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Auto
Gemini25FlashEditAspectRatio::Auto => "auto",
// Square
Gemini25FlashEditAspectRatio::OneByOne => "1:1",
// Wide
Gemini25FlashEditAspectRatio::FiveByFour => "5:4",
Gemini25FlashEditAspectRatio::FourByThree => "4:3",
Gemini25FlashEditAspectRatio::ThreeByTwo => "3:2",
Gemini25FlashEditAspectRatio::SixteenByNine => "16:9",
Gemini25FlashEditAspectRatio::TwentyOneByNine => "21:9",
// Tall
Gemini25FlashEditAspectRatio::FourByFive => "4:5",
Gemini25FlashEditAspectRatio::ThreeByFour => "3:4",
Gemini25FlashEditAspectRatio::TwoByThree => "2:3",
Gemini25FlashEditAspectRatio::NineBySixteen => "9:16",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let request = Gemini25FlashImageEditInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
aspect_ratio,
// Constants
num_images: Some(num_images),
output_format: Some("png".to_string()),
@@ -75,6 +119,7 @@ mod tests {
TREX_SKELETON_IMAGE_URL.to_string(),
],
num_images: Gemini25FlashEditNumImages::Two,
aspect_ratio: None,
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
api_key: &api_key,
webhook_url: "https://example.com/webhook",
@@ -0,0 +1,182 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::gpt_image::gpt_image_1p5_image_edit::{gpt_image_1p5_image_edit, GptImage1p5ImageEditInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueGptImage1p5EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub num_images: EnqueueGptImage1p5EditImageNumImages,
// Optional args
pub mask_image_url: Option<String>,
pub image_size: Option<EnqueueGptImage1p5EditImageSize>,
pub background: Option<EnqueueGptImage1p5EditImageBackground>,
pub quality: Option<EnqueueGptImage1p5EditImageQuality>,
pub input_fidelity: Option<EnqueueGptImage1p5EditImageInputFidelity>,
pub output_format: Option<EnqueueGptImage1p5EditImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageSize {
/// 1024x1024
Square,
/// 1536x1024
Wide,
/// 1024x1536
Tall,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageBackground {
Auto,
Transparent,
Opaque,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageQuality {
Low,
Medium,
High,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageInputFidelity {
Low,
High,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5EditImageOutputFormat {
Jpeg,
Png,
Webp,
}
pub async fn enqueue_gpt_image_1p5_image_edit_webhook<R: IntoUrl>(
args: EnqueueGptImage1p5EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
EnqueueGptImage1p5EditImageNumImages::One => 1,
EnqueueGptImage1p5EditImageNumImages::Two => 2,
EnqueueGptImage1p5EditImageNumImages::Three => 3,
EnqueueGptImage1p5EditImageNumImages::Four => 4,
};
let image_size = args.image_size
.map(|s| match s {
EnqueueGptImage1p5EditImageSize::Square => "1024x1024",
EnqueueGptImage1p5EditImageSize::Wide => "1536x1024",
EnqueueGptImage1p5EditImageSize::Tall => "1024x1536",
})
.map(|resolution| resolution.to_string());
let background = args.background
.map(|s| match s {
EnqueueGptImage1p5EditImageBackground::Auto => "auto",
EnqueueGptImage1p5EditImageBackground::Transparent => "transparent",
EnqueueGptImage1p5EditImageBackground::Opaque => "opaque",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let quality = args.quality
.map(|s| match s {
EnqueueGptImage1p5EditImageQuality::Low => "low",
EnqueueGptImage1p5EditImageQuality::Medium => "medium",
EnqueueGptImage1p5EditImageQuality::High => "high",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let input_fidelity = args.input_fidelity
.map(|s| match s {
EnqueueGptImage1p5EditImageInputFidelity::Low => "low",
EnqueueGptImage1p5EditImageInputFidelity::High => "high",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let output_format = args.output_format
.map(|s| match s {
EnqueueGptImage1p5EditImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage1p5EditImageOutputFormat::Png => "png",
EnqueueGptImage1p5EditImageOutputFormat::Webp => "webp",
})
.map(|aspect_ratio| aspect_ratio.to_string())
.unwrap_or_else(|| "png".to_string());
let request = GptImage1p5ImageEditInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
num_images: Some(num_images),
output_format: Some(output_format),
// Optionals
mask_image_url: args.mask_image_url,
image_size,
background,
quality,
input_fidelity,
};
let result = gpt_image_1p5_image_edit(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs, EnqueueGptImage1p5EditImageNumImages};
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]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueGptImage1p5EditImageArgs {
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",
num_images: EnqueueGptImage1p5EditImageNumImages::Two,
mask_image_url: None,
image_size: None,
background: None,
quality: None,
input_fidelity: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
output_format: None,
};
let result = enqueue_gpt_image_1p5_image_edit_webhook(args).await?;
Ok(())
}
}
@@ -30,15 +30,17 @@ pub enum EnqueueNanoBananaProEditImageNumImages {
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProEditImageResolution {
OneK,
OneK, // Default value "1K"
TwoK,
FourK,
}
/// 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16
/// Default is "1:1"
/// auto, 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16
/// Default is "auto"
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProEditImageAspectRatio {
// Automatic (default)
Auto,
// Square
OneByOne,
// Wide
@@ -75,6 +77,8 @@ pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Auto
EnqueueNanoBananaProEditImageAspectRatio::Auto => "auto",
// Square
EnqueueNanoBananaProEditImageAspectRatio::OneByOne => "1:1",
// Wide
@@ -3,6 +3,7 @@ pub mod enqueue_bytedance_seedream_v4p5_edit_image_webhook;
pub mod enqueue_flux_pro_kontext_max_edit_webhook;
pub mod enqueue_gemini_25_flash_edit_webhook;
pub mod enqueue_gpt_image_1_edit_image_webhook;
pub mod enqueue_gpt_image_1p5_edit_image_webhook;
pub mod enqueue_nano_banana_pro_edit_image_webhook;
pub mod enqueue_qwen_image_edit_webhook;
pub mod enqueue_seededit_v3_edit_webhook;
@@ -13,10 +13,11 @@ pub struct FluxPro11Args<'a, U: IntoUrl> {
pub num_images: FluxPro11NumImages,
}
// TODO(bt,2026-01-01): This seems to disagree between Fal.ai and Fal.rs client libraries.
#[derive(Copy, Clone, Debug)]
pub enum FluxPro11AspectRatio {
Square, // 1:1
SquareHd, // 1:1
SquareHd, // 1:1 (TODO: Is this in the API? I checked recently and don't see it.)
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
@@ -16,10 +16,14 @@ pub struct FluxPro11UltraArgs<'a, U: IntoUrl> {
#[derive(Copy, Clone, Debug)]
pub enum FluxPro11UltraAspectRatio {
Square, // 1:1
LandscapeThreeByTwo, // 3:2
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
LandscapeTwentyOneByNine, // 21:9
PortraitTwoByThree, // 2:3
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
PortraitNineByTwentyOne, // 9:21
//Custom { width: u32, height: u32 }, // TODO
}
@@ -44,10 +48,14 @@ pub async fn enqueue_flux_pro_11_ultra_text_to_image_webhook<U: IntoUrl>(
let aspect_ratio = match args.aspect_ratio {
FluxPro11UltraAspectRatio::Square => AspectRatioProperty::Property_1_1,
FluxPro11UltraAspectRatio::LandscapeThreeByTwo => AspectRatioProperty::Property_3_2,
FluxPro11UltraAspectRatio::LandscapeFourByThree => AspectRatioProperty::Property_4_3,
FluxPro11UltraAspectRatio::LandscapeSixteenByNine => AspectRatioProperty::Property_16_9,
FluxPro11UltraAspectRatio::LandscapeTwentyOneByNine => AspectRatioProperty::Property_21_9,
FluxPro11UltraAspectRatio::PortraitTwoByThree => AspectRatioProperty::Property_2_3,
FluxPro11UltraAspectRatio::PortraitThreeByFour => AspectRatioProperty::Property_3_4,
FluxPro11UltraAspectRatio::PortraitNineBySixteen => AspectRatioProperty::Property_9_16,
FluxPro11UltraAspectRatio::PortraitNineByTwentyOne => AspectRatioProperty::Property_9_21,
};
let request = FluxProUltraTextToImageInput {
@@ -0,0 +1,121 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::prelude::fal_ai::gemini_25_flash_image::text_to_image::gemini_25_flash_text_to_image;
use fal::prelude::fal_ai::gemini_25_flash_image::text_to_image::Gemini25FlashTextToImageInput;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Gemini25FlashTextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub num_images: Gemini25FlashTextToImageNumImages,
// Optional
pub aspect_ratio: Option<Gemini25FlashTextToImageAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum Gemini25FlashTextToImageNumImages {
One,
Two,
Three,
Four,
}
/// 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16
/// Default is "1:1"
#[derive(Copy, Clone, Debug)]
pub enum Gemini25FlashTextToImageAspectRatio {
// Square
OneByOne,
// Wide
FiveByFour,
FourByThree,
ThreeByTwo,
SixteenByNine,
TwentyOneByNine,
// Tall
FourByFive,
ThreeByFour,
TwoByThree,
NineBySixteen, // NB: No NineByTwentyOne ?
}
pub async fn enqueue_gemini_25_flash_text_to_image_webhook<R: IntoUrl>(
args: Gemini25FlashTextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
Gemini25FlashTextToImageNumImages::One => 1,
Gemini25FlashTextToImageNumImages::Two => 2,
Gemini25FlashTextToImageNumImages::Three => 3,
Gemini25FlashTextToImageNumImages::Four => 4,
};
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Square
Gemini25FlashTextToImageAspectRatio::OneByOne => "1:1",
// Wide
Gemini25FlashTextToImageAspectRatio::FiveByFour => "5:4",
Gemini25FlashTextToImageAspectRatio::FourByThree => "4:3",
Gemini25FlashTextToImageAspectRatio::ThreeByTwo => "3:2",
Gemini25FlashTextToImageAspectRatio::SixteenByNine => "16:9",
Gemini25FlashTextToImageAspectRatio::TwentyOneByNine => "21:9",
// Tall
Gemini25FlashTextToImageAspectRatio::FourByFive => "4:5",
Gemini25FlashTextToImageAspectRatio::ThreeByFour => "3:4",
Gemini25FlashTextToImageAspectRatio::TwoByThree => "2:3",
Gemini25FlashTextToImageAspectRatio::NineBySixteen => "9:16",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let request = Gemini25FlashTextToImageInput {
prompt: args.prompt.to_string(),
aspect_ratio,
// Constants
num_images: Some(num_images),
output_format: Some("png".to_string()),
};
let result = gemini_25_flash_text_to_image(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{enqueue_gemini_25_flash_text_to_image_webhook, Gemini25FlashTextToImageArgs, Gemini25FlashTextToImageNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Gemini25FlashTextToImageArgs {
prompt: "a warrior on the field of battle, final fantasy style, lots of hungry raptors rushing the heroes",
num_images: Gemini25FlashTextToImageNumImages::Two,
aspect_ratio: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_gemini_25_flash_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,164 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::gpt_image::gpt_image_1p5_text_to_image::{gpt_image_1p5_text_to_image, GptImage1p5TextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueGptImage1p5TextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub num_images: EnqueueGptImage1p5TextToImageNumImages,
// Optional args
pub image_size: Option<EnqueueGptImage1p5TextToImageSize>,
pub background: Option<EnqueueGptImage1p5TextToImageBackground>,
pub quality: Option<EnqueueGptImage1p5TextToImageQuality>,
pub output_format: Option<EnqueueGptImage1p5TextToImageOutputFormat>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageSize {
/// 1024x1024
Square,
/// 1536x1024
Wide,
/// 1024x1536
Tall,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageBackground {
Auto,
Transparent,
Opaque,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageQuality {
Low,
Medium,
High,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueGptImage1p5TextToImageOutputFormat {
Jpeg,
Png,
Webp,
}
pub async fn enqueue_gpt_image_1p5_text_to_image_webhook<R: IntoUrl>(
args: EnqueueGptImage1p5TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
EnqueueGptImage1p5TextToImageNumImages::One => 1,
EnqueueGptImage1p5TextToImageNumImages::Two => 2,
EnqueueGptImage1p5TextToImageNumImages::Three => 3,
EnqueueGptImage1p5TextToImageNumImages::Four => 4,
};
let image_size = args.image_size
.map(|s| match s {
EnqueueGptImage1p5TextToImageSize::Square => "1024x1024",
EnqueueGptImage1p5TextToImageSize::Wide => "1536x1024",
EnqueueGptImage1p5TextToImageSize::Tall => "1024x1536",
})
.map(|resolution| resolution.to_string());
let background = args.background
.map(|s| match s {
EnqueueGptImage1p5TextToImageBackground::Auto => "auto",
EnqueueGptImage1p5TextToImageBackground::Transparent => "transparent",
EnqueueGptImage1p5TextToImageBackground::Opaque => "opaque",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let quality = args.quality
.map(|s| match s {
EnqueueGptImage1p5TextToImageQuality::Low => "low",
EnqueueGptImage1p5TextToImageQuality::Medium => "medium",
EnqueueGptImage1p5TextToImageQuality::High => "high",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let output_format = args.output_format
.map(|s| match s {
EnqueueGptImage1p5TextToImageOutputFormat::Jpeg => "jpeg",
EnqueueGptImage1p5TextToImageOutputFormat::Png => "png",
EnqueueGptImage1p5TextToImageOutputFormat::Webp => "webp",
})
.map(|aspect_ratio| aspect_ratio.to_string())
.unwrap_or_else(|| "png".to_string());
let num_images = match args.num_images {
EnqueueGptImage1p5TextToImageNumImages::One => 1,
EnqueueGptImage1p5TextToImageNumImages::Two => 2,
EnqueueGptImage1p5TextToImageNumImages::Three => 3,
EnqueueGptImage1p5TextToImageNumImages::Four => 4,
};
let request = GptImage1p5TextToImageInput {
prompt: args.prompt.to_string(),
num_images: Some(num_images),
output_format: Some(output_format),
// Optionals
image_size,
background,
quality,
};
let result = gpt_image_1p5_text_to_image(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs, EnqueueGptImage1p5TextToImageNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueGptImage1p5TextToImageArgs {
prompt: "an anime girl riding on the back of a t-rex",
num_images: EnqueueGptImage1p5TextToImageNumImages::Two,
image_size: None,
background: None,
quality: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
output_format: None,
};
let result = enqueue_gpt_image_1p5_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -1,3 +1,5 @@
pub mod enqueue_bytedance_seedream_v4_text_to_image_webhook;
pub mod enqueue_bytedance_seedream_v4p5_text_to_image_webhook;
pub mod enqueue_gemini_25_flash_text_to_image_webhook;
pub mod enqueue_gpt_image_1p5_text_to_image_webhook;
pub mod enqueue_nano_banana_pro_text_to_image_webhook;
@@ -1,105 +0,0 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::bytedance::seedance::v1::lite::image_to_video::image_to_video;
use fal::endpoints::fal_ai::bytedance::seedance::v1::lite::image_to_video::ImageToVideoRequest;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Seedance1LiteArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub camera_fixed: bool,
pub duration: Seedance1LiteDuration,
pub resolution: Seedance1LiteResolution,
pub seed: Option<u32>,
}
#[derive(Copy, Clone, Debug)]
pub enum Seedance1LiteDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Seedance1LiteResolution {
FourEightyP, // 480p
SevenTwentyP, // 720p
}
pub async fn enqueue_seedance_1_lite_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Seedance1LiteArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Seedance1LiteDuration::FiveSeconds => Some("5".to_string()),
Seedance1LiteDuration::TenSeconds => Some("10".to_string()),
};
let resolution = match args.resolution {
Seedance1LiteResolution::FourEightyP => Some("480p".to_string()),
Seedance1LiteResolution::SevenTwentyP => Some("720p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let end_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = ImageToVideoRequest {
image_url,
end_image_url,
prompt: args.prompt.to_string(),
duration,
resolution,
// TODO: Add these later
camera_fixed: None,
seed: -1,
// Static
enable_safety_checker: Some(false),
};
let result = image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_seedance_1_lite_image_to_video_webhook::{enqueue_seedance_1_lite_image_to_video_webhook, Seedance1LiteArgs, Seedance1LiteDuration, Seedance1LiteResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Seedance1LiteArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "shiba in glasses runs to the lake and stands by the shore",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1LiteDuration::FiveSeconds,
resolution: Seedance1LiteResolution::SevenTwentyP,
seed: None,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_seedance_1_lite_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -1,115 +0,0 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::error::classify_fal_error::classify_fal_error;
use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::bytedance::seedance::v1::pro::image_to_video::{seedance_v1_pro_image_to_video, SeedanceImageToVideoProRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Seedance1ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub camera_fixed: bool,
pub duration: Seedance1ProDuration,
pub resolution: Seedance1ProResolution,
pub seed: Option<u32>,
}
#[derive(Copy, Clone, Debug)]
pub enum Seedance1ProDuration {
ThreeSeconds,
FourSeconds,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
NineSeconds,
TenSeconds,
ElevenSeconds,
TwelveSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Seedance1ProResolution {
FourEightyP, // 480p
SevenTwentyP, // 720p
TenEightyP, // 1080p
}
pub async fn enqueue_seedance_1_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Seedance1ProArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Seedance1ProDuration::ThreeSeconds => Some("3".to_string()),
Seedance1ProDuration::FourSeconds => Some("4".to_string()),
Seedance1ProDuration::FiveSeconds => Some("5".to_string()),
Seedance1ProDuration::SixSeconds => Some("6".to_string()),
Seedance1ProDuration::SevenSeconds => Some("7".to_string()),
Seedance1ProDuration::EightSeconds => Some("8".to_string()),
Seedance1ProDuration::NineSeconds => Some("9".to_string()),
Seedance1ProDuration::TenSeconds => Some("10".to_string()),
Seedance1ProDuration::ElevenSeconds => Some("11".to_string()),
Seedance1ProDuration::TwelveSeconds => Some("12".to_string()),
};
let resolution = match args.resolution {
Seedance1ProResolution::FourEightyP => Some("480p".to_string()),
Seedance1ProResolution::SevenTwentyP => Some("720p".to_string()),
Seedance1ProResolution::TenEightyP => Some("1080p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = SeedanceImageToVideoProRequest {
image_url,
prompt: args.prompt.to_string(),
duration,
resolution,
// TODO: Add these later
camera_fixed: None,
// Static
enable_safety_checker: Some(false),
};
let result = seedance_v1_pro_image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_seedance_1_pro_image_to_video_webhook::{enqueue_seedance_1_pro_image_to_video_webhook, Seedance1ProArgs, Seedance1ProDuration, Seedance1ProResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TALL_MOCHI_WITH_GLASSES_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Seedance1ProArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
prompt: "shiba in glasses runs to the lake and stands by the shore",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1ProDuration::FiveSeconds,
resolution: Seedance1ProResolution::SevenTwentyP,
seed: None,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_seedance_1_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_fast_pro_image_to_video::{hailuo_v2p3_fast_pro_image_to_video, HailuoV2p3FastProImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -18,6 +19,16 @@ pub struct EnqueueHailuoV2p3FastProImageToVideoArgs<'a, R: IntoUrl> {
pub api_key: &'a FalApiKey,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueHailuoV2p3FastProImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
/// "Your request will cost $0.33 per video."
33
}
}
/// Hailuo 2.3 Fast Pro Image-to-Video
/// https://fal.ai/models/fal-ai/minimax/hailuo-2.3-fast/pro/image-to-video
pub async fn enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueHailuoV2p3FastProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_pro_image_to_video::{hailuo_v2p3_pro_image_to_video, HailuoV2p3ProImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -18,6 +19,16 @@ pub struct EnqueueHailuoV2p3ProImageToVideoArgs<'a, R: IntoUrl> {
pub api_key: &'a FalApiKey,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueHailuoV2p3ProImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
/// "Your request will cost $0.49 per video generation."
49
}
}
/// Hailuo 2.3 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/minimax/hailuo-2.3/pro/image-to-video
pub async fn enqueue_hailuo_v2p3_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueHailuoV2p3ProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,47 +1,62 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::kling_video::v1_6::pro::image_to_video::{image_to_video, ProImageToVideoRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling16ProArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub struct Kling1p6ProArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Kling16Duration,
pub aspect_ratio: Kling16ProAspectRatio,
pub duration: Kling1p6ProDuration,
pub aspect_ratio: Kling1p6ProAspectRatio,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling16Duration {
pub enum Kling1p6ProDuration {
Default,
FiveSeconds,
TenSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling16ProAspectRatio {
pub enum Kling1p6ProAspectRatio {
Square, // 1:1
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling16ProArgs<'_, U, T, V>
impl <U: IntoUrl, T: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling1p6ProArgs<'_, U, T, V> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "Your request will cost $0.095 per second."
match self.duration {
Kling1p6ProDuration::Default => 48, // $0.095 * 5 = $0.475 (round up)
Kling1p6ProDuration::FiveSeconds => 48, // $0.095 * 5 = $0.475 (round up)
Kling1p6ProDuration::TenSeconds => 95, // $0.095 * 10 = $0.95
}
}
}
/// Kling 1.6 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v1.6/pro/image-to-video
pub async fn enqueue_kling_v1p6_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling1p6ProArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling16Duration::Default => None,
Kling16Duration::FiveSeconds => Some("5".to_string()), // Gross...
Kling16Duration::TenSeconds => Some("10".to_string()),
Kling1p6ProDuration::Default => None, // defaults to "5"
Kling1p6ProDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling1p6ProDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Kling16ProAspectRatio::Square => Some("1:1".to_string()),
Kling16ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling16ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
Kling1p6ProAspectRatio::Square => Some("1:1".to_string()),
Kling1p6ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling1p6ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
@@ -71,7 +86,7 @@ pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl,
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_kling_16_pro_image_to_video_webhook::{enqueue_kling_16_pro_image_to_video_webhook, Kling16Duration, Kling16ProArgs, Kling16ProAspectRatio};
use crate::requests::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::{enqueue_kling_v1p6_pro_image_to_video_webhook, Kling1p6ProArgs, Kling1p6ProAspectRatio, Kling1p6ProDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
@@ -84,17 +99,17 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Kling16ProArgs {
let args = Kling1p6ProArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "shiba in glasses runs to the lake and stands by the shore",
api_key: &api_key,
duration: Kling16Duration::Default,
aspect_ratio: Kling16ProAspectRatio::WideSixteenNine,
duration: Kling1p6ProDuration::Default,
aspect_ratio: Kling1p6ProAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_16_pro_image_to_video_webhook(args).await?;
let result = enqueue_kling_v1p6_pro_image_to_video_webhook(args).await?;
Ok(())
}
@@ -4,43 +4,59 @@ use crate::error::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::kling_video::v2_1::master::image_to_video::{image_to_video, MasterImageToVideoRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
pub struct Kling21MasterArgs<'a, U: IntoUrl, V: IntoUrl> {
pub struct Kling2p1MasterArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Kling21MasterDuration,
pub aspect_ratio: Kling21MasterAspectRatio,
pub duration: Kling2p1MasterDuration,
pub aspect_ratio: Kling2p1MasterAspectRatio,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling21MasterDuration {
pub enum Kling2p1MasterDuration {
Default,
FiveSeconds,
TenSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling21MasterAspectRatio {
pub enum Kling2p1MasterAspectRatio {
Square, // 1:1
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_21_master_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling21MasterArgs<'_, U, V>
impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling2p1MasterArgs<'_, U, V> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $1.40.
// For every additional second you will be charged $0.28."
match self.duration {
Kling2p1MasterDuration::Default => 140, // $1.40 for 5 seconds
Kling2p1MasterDuration::FiveSeconds => 140, // $1.40 for 5 seconds
Kling2p1MasterDuration::TenSeconds => 280, // $1.40 + (0.28 * 5) = $2.80
}
}
}
/// Kling 2.1 Master Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.1/master/image-to-video
pub async fn enqueue_kling_v2p1_master_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling2p1MasterArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling21MasterDuration::Default => None,
Kling21MasterDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling21MasterDuration::TenSeconds => Some("10".to_string()),
Kling2p1MasterDuration::Default => None, // defaults to "5"
Kling2p1MasterDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling2p1MasterDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Kling21MasterAspectRatio::Square => Some("1:1".to_string()),
Kling21MasterAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling21MasterAspectRatio::TallNineSixteen => Some("9:16".to_string()),
Kling2p1MasterAspectRatio::Square => Some("1:1".to_string()),
Kling2p1MasterAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling2p1MasterAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
@@ -68,7 +84,7 @@ pub async fn enqueue_kling_21_master_image_to_video_webhook<U: IntoUrl, V: IntoU
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_kling_21_master_image_to_video_webhook::{enqueue_kling_21_master_image_to_video_webhook, Kling21MasterArgs, Kling21MasterAspectRatio, Kling21MasterDuration};
use crate::requests::webhook::video::image::enqueue_kling_v2p1_master_image_to_video_webhook::{enqueue_kling_v2p1_master_image_to_video_webhook, Kling2p1MasterArgs, Kling2p1MasterAspectRatio, Kling2p1MasterDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -82,16 +98,16 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Kling21MasterArgs {
let args = Kling2p1MasterArgs {
image_url: image_url,
prompt: "a shot of the mountains, the camera rotates around to show the mountain range, the sun begins to set, and the moon becomes visible",
api_key: &api_key,
duration: Kling21MasterDuration::Default,
aspect_ratio: Kling21MasterAspectRatio::WideSixteenNine,
duration: Kling2p1MasterDuration::Default,
aspect_ratio: Kling2p1MasterAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_21_master_image_to_video_webhook(args).await?;
let result = enqueue_kling_v2p1_master_image_to_video_webhook(args).await?;
Ok(())
}
@@ -1,47 +1,62 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::kling_video::v2_1::pro::image_to_video::{image_to_video, ProImageToVideoRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling21ProArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub struct Kling2p1ProArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub duration: Kling21ProDuration,
pub aspect_ratio: Kling21ProAspectRatio,
pub duration: Kling2p1ProDuration,
pub aspect_ratio: Kling2p1ProAspectRatio,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling21ProDuration {
pub enum Kling2p1ProDuration {
Default,
FiveSeconds,
TenSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Kling21ProAspectRatio {
pub enum Kling2p1ProAspectRatio {
Square, // 1:1
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling21ProArgs<'_, U, T, V>
impl <U: IntoUrl, T: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Kling2p1ProArgs<'_, U, T, V> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $0.45.
// For every additional second you will be charged $0.09."
match self.duration {
Kling2p1ProDuration::Default => 45, // $0.45
Kling2p1ProDuration::FiveSeconds => 45, // $0.45
Kling2p1ProDuration::TenSeconds => 90, // $0.45 + (5 * 0.09) = $0.90
}
}
}
/// Kling 2.1 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.1/pro/image-to-video
pub async fn enqueue_kling_v2p1_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling2p1ProArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling21ProDuration::Default => None,
Kling21ProDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling21ProDuration::TenSeconds => Some("10".to_string()),
Kling2p1ProDuration::Default => None, // Defaults to 5 seconds
Kling2p1ProDuration::FiveSeconds => Some("5".to_string()), // Gross...
Kling2p1ProDuration::TenSeconds => Some("10".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Kling21ProAspectRatio::Square => Some("1:1".to_string()),
Kling21ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling21ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
Kling2p1ProAspectRatio::Square => Some("1:1".to_string()),
Kling2p1ProAspectRatio::WideSixteenNine => Some("16:9".to_string()),
Kling2p1ProAspectRatio::TallNineSixteen => Some("9:16".to_string()),
};
let image_url = args.image_url.as_str().to_string();
@@ -71,7 +86,7 @@ pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl,
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_kling_21_pro_image_to_video_webhook::{enqueue_kling_21_pro_image_to_video_webhook, Kling21ProArgs, Kling21ProAspectRatio, Kling21ProDuration};
use crate::requests::webhook::video::image::enqueue_kling_v2p1_pro_image_to_video_webhook::{enqueue_kling_v2p1_pro_image_to_video_webhook, Kling2p1ProArgs, Kling2p1ProAspectRatio, Kling2p1ProDuration};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL};
@@ -84,17 +99,17 @@ mod tests {
let api_key = FalApiKey::from_str(&secret);
let args = Kling21ProArgs {
let args = Kling2p1ProArgs {
image_url: SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "a shot of the mountains, the camera pulls back to show a corgi waiting to jump into the lake",
api_key: &api_key,
duration: Kling21ProDuration::Default,
aspect_ratio: Kling21ProAspectRatio::WideSixteenNine,
duration: Kling2p1ProDuration::Default,
aspect_ratio: Kling2p1ProAspectRatio::WideSixteenNine,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_21_pro_image_to_video_webhook(args).await?;
let result = enqueue_kling_v2p1_pro_image_to_video_webhook(args).await?;
Ok(())
}
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_pro_image_to_video::{kling_v2p5_turbo_pro_image_to_video, KlingV2p5TurboProImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -27,10 +28,26 @@ pub enum EnqueueKlingV2p5TurboProImageToVideoDurationSeconds {
Ten,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueKlingV2p5TurboProImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $0.35.
// For every additional second you will be charged $0.07."
match self.duration {
None => 35, // $0.35
Some(EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five) => 35, // $0.35
Some(EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Ten) => 70, // $0.35 + (5 * $0.07) = $0.70
}
}
}
/// Kling 2.5 Turbo Pro Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.5-turbo/pro/image-to-video
pub async fn enqueue_kling_v2p5_turbo_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueKlingV2p5TurboProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
// NB: Defaults to 5 seconds
let duration = args.duration
.map(|resolution| match resolution {
EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five => "5",
@@ -1,9 +1,10 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_standard_image_to_video::{kling_v2p5_turbo_standard_image_to_video, KlingV2p5TurboStandardImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_standard_image_to_video::{kling_v2p5_turbo_standard_image_to_video, KlingV2p5TurboStandardImageToVideoInput};
pub struct EnqueueKlingV2p5TurboStandardImageToVideoArgs<'a, R: IntoUrl> {
// Request required
@@ -25,10 +26,26 @@ pub enum EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds {
Ten,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueKlingV2p5TurboStandardImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $0.21.
// For every additional second you will be charged $0.042."
match self.duration {
None => 21, // $0.21
Some(EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Five) => 21, // $0.21
Some(EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Ten) => 42, // $0.21 + (5 * $0.042) = $0.42
}
}
}
/// Kling 2.5 Turbo Standard Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.5-turbo/standard/image-to-video
pub async fn enqueue_kling_v2p5_turbo_standard_image_to_video_webhook<R: IntoUrl>(
args: EnqueueKlingV2p5TurboStandardImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
// NB: Defaults to 5 seconds.
let duration = args.duration
.map(|resolution| match resolution {
EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Five => "5",
@@ -57,10 +74,10 @@ pub async fn enqueue_kling_v2p5_turbo_standard_image_to_video_webhook<R: IntoUrl
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_kling_v2p5_turbo_standard_image_to_video_webhook::{enqueue_kling_v2p5_turbo_standard_image_to_video_webhook, EnqueueKlingV2p5TurboStandardImageToVideoArgs, EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
use crate::requests::webhook::video::image::enqueue_kling_v2p5_turbo_standard_image_to_video_webhook::{enqueue_kling_v2p5_turbo_standard_image_to_video_webhook, EnqueueKlingV2p5TurboStandardImageToVideoArgs, EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds};
#[tokio::test]
#[ignore]
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::kling_video::v2_6::kling_v2p6_pro_image_to_video::{kling_v2p6_pro_image_to_video, KlingV2p6ProImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -11,8 +12,13 @@ pub struct EnqueueKlingV2p6ProImageToVideoArgs<'a, R: IntoUrl> {
pub image_url: String,
// Optional args
/// NB: Generate audio defaults to true, and it costs twice as much.
pub generate_audio: Option<bool>,
pub negative_prompt: Option<String>,
/// NB: Duration defaults to 5 seconds
pub duration: Option<EnqueueKlingV2p6ProImageToVideoDurationSeconds>,
// Fulfillment
@@ -26,6 +32,26 @@ pub enum EnqueueKlingV2p6ProImageToVideoDurationSeconds {
Ten,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueKlingV2p6ProImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For every second of video you generated, you will be
// charged $0.07 (audio off) or $0.14 (audio on).
// For example, a 5s video with audio on will cost $0.70"
let generate_audio = self.generate_audio.unwrap_or(true);
let duration = self.duration.unwrap_or(EnqueueKlingV2p6ProImageToVideoDurationSeconds::Five);
match (generate_audio, duration) {
(false, EnqueueKlingV2p6ProImageToVideoDurationSeconds::Five) => 35, // audio off: $0.07 * 5 = $0.35
(false, EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten) => 70, // audio off: $0.07 * 10 = $0.70
(true, EnqueueKlingV2p6ProImageToVideoDurationSeconds::Five) => 70, // audio on: $0.14 * 5 = $0.70
(true, EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten) => 140, // audio on: $0.14 * 10 = $1.40
}
}
}
/// Kling 2.6 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.6/pro/image-to-video
pub async fn enqueue_kling_v2p6_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueKlingV2p6ProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::pixverse::v5::pixverse_v5_image_to_video::{pixverse_v5_image_to_video, PixverseV5ImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -14,8 +15,11 @@ pub struct EnqueuePixverseV5ImageToVideoArgs<'a, R: IntoUrl> {
pub negative_prompt: Option<String>,
// NB: eight-second videos do not work with 1080P
// NB: Defaults to 5 seconds
pub duration: Option<EnqueuePixverseV5ImageToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueuePixverseV5ImageToVideoAspectRatio>,
// NB: Defaults to 720p
pub resolution: Option<EnqueuePixverseV5ImageToVideoResolution>,
pub style: Option<EnqueuePixverseV5ImageToVideoStyle>,
@@ -56,6 +60,33 @@ pub enum EnqueuePixverseV5ImageToVideoStyle {
Cyberpunk,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueuePixverseV5ImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost
// $0.15 for 360p and 540p,
// $0.2 for 720p and
// $0.4 for 1080p.
// For $1 you can run this model with approximately 2 times."
let duration = self.duration.unwrap_or(EnqueuePixverseV5ImageToVideoDurationSeconds::Five);
let resolution = self.resolution.unwrap_or(EnqueuePixverseV5ImageToVideoResolution::SevenTwentyP);
match (duration, resolution) {
// Five seconds
(EnqueuePixverseV5ImageToVideoDurationSeconds::Five, EnqueuePixverseV5ImageToVideoResolution::ThreeSixtyP) => 15, // $0.15
(EnqueuePixverseV5ImageToVideoDurationSeconds::Five, EnqueuePixverseV5ImageToVideoResolution::FiveFortyP) => 15, // $0.15
(EnqueuePixverseV5ImageToVideoDurationSeconds::Five, EnqueuePixverseV5ImageToVideoResolution::SevenTwentyP) => 20, // $0.20
(EnqueuePixverseV5ImageToVideoDurationSeconds::Five, EnqueuePixverseV5ImageToVideoResolution::TenEightyP) => 40, // $0.40
// Ten seconds
(EnqueuePixverseV5ImageToVideoDurationSeconds::Eight, EnqueuePixverseV5ImageToVideoResolution::ThreeSixtyP) => 30, // NB: Doubling since unclear
(EnqueuePixverseV5ImageToVideoDurationSeconds::Eight, EnqueuePixverseV5ImageToVideoResolution::FiveFortyP) => 30, // NB: Doubling since unclear
(EnqueuePixverseV5ImageToVideoDurationSeconds::Eight, EnqueuePixverseV5ImageToVideoResolution::SevenTwentyP) => 40, // NB: Doubling since unclear
(EnqueuePixverseV5ImageToVideoDurationSeconds::Eight, EnqueuePixverseV5ImageToVideoResolution::TenEightyP) => 80, // NB: Doubling since unclear
}
}
}
/// Pixverse 5 Image-to-Video
/// https://fal.ai/models/fal-ai/pixverse/v5/image-to-video
pub async fn enqueue_pixverse_v5_image_to_video_webhook<R: IntoUrl>(
args: EnqueuePixverseV5ImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -0,0 +1,220 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::bytedance::seedance::v1::lite::image_to_video::image_to_video;
use fal::endpoints::fal_ai::bytedance::seedance::v1::lite::image_to_video::ImageToVideoRequest;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Seedance1LiteArgs<'a, U: IntoUrl, T: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub end_frame_image_url: Option<T>,
pub prompt: &'a str,
pub duration: Seedance1LiteDuration,
pub resolution: Seedance1LiteResolution,
pub aspect_ratio: Option<Seedance1LiteAspectRatio>,
pub camera_fixed: bool,
pub seed: Option<u32>,
pub api_key: &'a FalApiKey,
pub webhook_url: V,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Seedance1LiteDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Seedance1LiteResolution {
FourEightyP, // 480p
SevenTwentyP, // 720p
TenEightyP, // 1080p
}
/// Possible enum values: 21:9, 16:9, 4:3, 1:1, 3:4, 9:16, auto
#[derive(Copy, Clone, Debug)]
pub enum Seedance1LiteAspectRatio {
Auto,
TwentyOneByNine,
SixteenByNine,
FourByThree,
Square,
ThreeByFour,
NineBySixteen,
}
impl <U: IntoUrl, T: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Seedance1LiteArgs<'_, U, T, V> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "Each 720p 5 second video costs $0.18.
// For other resolutions, 1 million video tokens costs $1.8.
// tokens(video) = (height x width x FPS x duration) / 1024."
if self.resolution == Seedance1LiteResolution::SevenTwentyP
&& self.duration == Seedance1LiteDuration::FiveSeconds
{
return 18;
}
// TODO: Only correct for some aspect ratios for now.
let (width, height) = match self.resolution {
Seedance1LiteResolution::FourEightyP => (640u32, 480u32), // NB: Only for 4:3 !
Seedance1LiteResolution::SevenTwentyP => (1280, 720), // NB: Only for 16:9 !
Seedance1LiteResolution::TenEightyP => (1920, 1080),
};
let duration = match self.duration {
Seedance1LiteDuration::FiveSeconds => 5.0,
Seedance1LiteDuration::TenSeconds => 10.0,
};
// TODO: Not sure if FPS is right.
// Inferred from https://help.scenario.com/en/articles/seedance-models-the-essentials/
const FPS : f64 = 30.0;
let tokens = (height as f64) * (width as f64) * FPS * duration;
let tokens = tokens / 1024.0;
let cost = tokens * 1.8 / 1_000_000.0;
let cost = cost * 100.0; // Dollars to cents.
let cost = cost.ceil(); // NB: This is probably what Fal does.
cost as UsdCents
}
}
/// Seedance 1.0 Lite Image-to-Video
/// https://fal.ai/models/fal-ai/bytedance/seedance/v1/lite/image-to-video
pub async fn enqueue_seedance_1_lite_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Seedance1LiteArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Seedance1LiteDuration::FiveSeconds => Some("5".to_string()),
Seedance1LiteDuration::TenSeconds => Some("10".to_string()),
};
let resolution = match args.resolution {
Seedance1LiteResolution::FourEightyP => Some("480p".to_string()),
Seedance1LiteResolution::SevenTwentyP => Some("720p".to_string()),
Seedance1LiteResolution::TenEightyP => Some("1080p".to_string()),
};
/// Possible enum values: 21:9, 16:9, 4:3, 1:1, 3:4, 9:16, auto
let aspect_ratio = args.aspect_ratio
.map(|r| match r {
Seedance1LiteAspectRatio::Auto => "auto",
Seedance1LiteAspectRatio::TwentyOneByNine => "21:9",
Seedance1LiteAspectRatio::SixteenByNine => "16:9",
Seedance1LiteAspectRatio::FourByThree => "4:3",
Seedance1LiteAspectRatio::Square => "1:1",
Seedance1LiteAspectRatio::ThreeByFour => "3:4",
Seedance1LiteAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let image_url = args.image_url.as_str().to_string();
let end_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = ImageToVideoRequest {
image_url,
end_image_url,
prompt: args.prompt.to_string(),
duration,
resolution,
aspect_ratio,
// TODO: Add these later
camera_fixed: None,
seed: -1,
// Static
enable_safety_checker: Some(false),
};
let result = image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{enqueue_seedance_1_lite_image_to_video_webhook, Seedance1LiteArgs, Seedance1LiteDuration, Seedance1LiteResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
use crate::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
#[test]
fn test_cost() {
let api_key = FalApiKey::from_str("");
let mut args = Seedance1LiteArgs {
image_url: "",
end_frame_image_url: Some(""),
prompt: "",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1LiteDuration::FiveSeconds,
resolution: Seedance1LiteResolution::SevenTwentyP,
aspect_ratio: None,
seed: None,
webhook_url: "https://example.com/webhook",
};
// NB: Constant value specified by FAL
args.duration = Seedance1LiteDuration::FiveSeconds;
args.resolution = Seedance1LiteResolution::SevenTwentyP;
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 18);
// NB: Calculations follow...
args.duration = Seedance1LiteDuration::FiveSeconds;
args.resolution = Seedance1LiteResolution::TenEightyP;
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 55);
args.duration = Seedance1LiteDuration::FiveSeconds;
args.resolution = Seedance1LiteResolution::FourEightyP;
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 9);
args.duration = Seedance1LiteDuration::TenSeconds;
args.resolution = Seedance1LiteResolution::TenEightyP;
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 110);
}
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Seedance1LiteArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
end_frame_image_url: Some(JUNO_AT_LAKE_IMAGE_URL.to_string()),
prompt: "shiba in glasses runs to the lake and stands by the shore",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1LiteDuration::FiveSeconds,
resolution: Seedance1LiteResolution::SevenTwentyP,
aspect_ratio: None,
seed: None,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_seedance_1_lite_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,205 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::bytedance::seedance::v1::pro::image_to_video::{seedance_v1_pro_image_to_video, SeedanceImageToVideoProRequest};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Seedance1ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub webhook_url: V,
pub prompt: &'a str,
pub api_key: &'a FalApiKey,
pub camera_fixed: bool,
pub duration: Seedance1ProDuration,
pub resolution: Seedance1ProResolution,
pub seed: Option<u32>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Seedance1ProDuration {
ThreeSeconds,
FourSeconds,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
NineSeconds,
TenSeconds,
ElevenSeconds,
TwelveSeconds,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Seedance1ProResolution {
FourEightyP, // 480p
SevenTwentyP, // 720p
TenEightyP, // 1080p
}
impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Seedance1ProArgs<'_, U, V> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "Each 1080p 5 second video costs roughly $0.62.
// For other resolutions, 1 million video tokens costs $2.5.
// tokens(video) = (height x width x FPS x duration) / 1024."
if self.resolution == Seedance1ProResolution::TenEightyP
&& self.duration == Seedance1ProDuration::FiveSeconds
{
return 62;
}
// TODO: Only correct for some aspect ratios for now.
let (width, height) = match self.resolution {
Seedance1ProResolution::FourEightyP => (640u32, 480u32), // NB: Only for 4:3 !
Seedance1ProResolution::SevenTwentyP => (1280, 720), // NB: Only for 16:9 !
Seedance1ProResolution::TenEightyP => (1920, 1080),
};
let duration = match self.duration {
Seedance1ProDuration::ThreeSeconds => 3.0,
Seedance1ProDuration::FourSeconds => 4.0,
Seedance1ProDuration::FiveSeconds => 5.0,
Seedance1ProDuration::SixSeconds => 6.0,
Seedance1ProDuration::SevenSeconds => 7.0,
Seedance1ProDuration::EightSeconds => 8.0,
Seedance1ProDuration::NineSeconds => 9.0,
Seedance1ProDuration::TenSeconds => 10.0,
Seedance1ProDuration::ElevenSeconds => 11.0,
Seedance1ProDuration::TwelveSeconds => 12.0,
};
// TODO: Not sure if FPS is right.
// Inferred from https://help.scenario.com/en/articles/seedance-models-the-essentials/
const FPS : f64 = 30.0;
let tokens = (height as f64) * (width as f64) * FPS * duration;
let tokens = tokens / 1024.0;
let cost = tokens * 2.5 / 1_000_000.0;
let cost = cost * 100.0; // Dollars to cents.
let cost = cost.ceil(); // NB: This is probably what Fal does.
cost as UsdCents
}
}
/// Seedance 1.0 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/bytedance/seedance/v1/pro/image-to-video
pub async fn enqueue_seedance_1_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Seedance1ProArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Seedance1ProDuration::ThreeSeconds => Some("3".to_string()),
Seedance1ProDuration::FourSeconds => Some("4".to_string()),
Seedance1ProDuration::FiveSeconds => Some("5".to_string()),
Seedance1ProDuration::SixSeconds => Some("6".to_string()),
Seedance1ProDuration::SevenSeconds => Some("7".to_string()),
Seedance1ProDuration::EightSeconds => Some("8".to_string()),
Seedance1ProDuration::NineSeconds => Some("9".to_string()),
Seedance1ProDuration::TenSeconds => Some("10".to_string()),
Seedance1ProDuration::ElevenSeconds => Some("11".to_string()),
Seedance1ProDuration::TwelveSeconds => Some("12".to_string()),
};
let resolution = match args.resolution {
Seedance1ProResolution::FourEightyP => Some("480p".to_string()),
Seedance1ProResolution::SevenTwentyP => Some("720p".to_string()),
Seedance1ProResolution::TenEightyP => Some("1080p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = SeedanceImageToVideoProRequest {
image_url,
prompt: args.prompt.to_string(),
duration,
resolution,
// TODO: Add these later
camera_fixed: None,
// Static
enable_safety_checker: Some(false),
};
let result = seedance_v1_pro_image_to_video(request)
.with_api_key(&args.api_key.0)
.queue_webhook(args.webhook_url)
.await;
result.map_err(|err| classify_fal_error(err))
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use crate::requests::webhook::video::image::enqueue_seedance_1_pro_image_to_video_webhook::{enqueue_seedance_1_pro_image_to_video_webhook, Seedance1ProArgs, Seedance1ProDuration, Seedance1ProResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TALL_MOCHI_WITH_GLASSES_IMAGE_URL;
#[test]
fn test_cost() {
let api_key = FalApiKey::from_str("");
let mut args = Seedance1ProArgs {
image_url: "",
prompt: "",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1ProDuration::FiveSeconds,
resolution: Seedance1ProResolution::TenEightyP,
seed: None,
webhook_url: "https://example.com/webhook",
};
// NB: Constant value specified by FAL
args.duration = Seedance1ProDuration::FiveSeconds;
args.resolution = Seedance1ProResolution::TenEightyP;
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 62);
// NB: Calculations follow...
args.duration = Seedance1ProDuration::FiveSeconds;
args.resolution = Seedance1ProResolution::SevenTwentyP;
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 34);
args.duration = Seedance1ProDuration::TenSeconds;
args.resolution = Seedance1ProResolution::SevenTwentyP;
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 68);
args.duration = Seedance1ProDuration::TenSeconds;
args.resolution = Seedance1ProResolution::TenEightyP;
let cost = args.calculate_cost_in_cents();
assert_eq!(cost, 152);
}
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = Seedance1ProArgs {
image_url: TALL_MOCHI_WITH_GLASSES_IMAGE_URL,
prompt: "shiba in glasses runs to the lake and stands by the shore",
api_key: &api_key,
camera_fixed: false,
duration: Seedance1ProDuration::FiveSeconds,
resolution: Seedance1ProResolution::SevenTwentyP,
seed: None,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_seedance_1_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::sora::sora2::sora_2_image_to_video::{sora_2_image_to_video, Sora2ImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -11,8 +12,11 @@ pub struct EnqueueSora2ImageToVideoArgs<'a, R: IntoUrl> {
pub image_url: String,
// Optional args
pub resolution: Option<EnqueueSora2ImageToVideoResolution>,
// NB: Duration defaults to four seconds
pub duration: Option<EnqueueSora2ImageToVideoDurationSeconds>,
pub resolution: Option<EnqueueSora2ImageToVideoResolution>,
pub aspect_ratio: Option<EnqueueSora2ImageToVideoAspectRatio>,
// Fulfillment
@@ -40,6 +44,20 @@ pub enum EnqueueSora2ImageToVideoAspectRatio {
SixteenByNine,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSora2ImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "The pricing is $0.1/s for Sora 2."
let duration = self.duration.unwrap_or(EnqueueSora2ImageToVideoDurationSeconds::Four);
match duration {
EnqueueSora2ImageToVideoDurationSeconds::Four => 40, // $0.10 * 4
EnqueueSora2ImageToVideoDurationSeconds::Eight => 80, // $0.10 * 8
EnqueueSora2ImageToVideoDurationSeconds::Twelve => 120 // $0.10 * 12
}
}
}
/// Sora 2 Image-to-Video
/// https://fal.ai/models/fal-ai/sora-2/image-to-video
pub async fn enqueue_sora_2_image_to_video_webhook<R: IntoUrl>(
args: EnqueueSora2ImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::sora::sora2::sora_2_pro_image_to_video::{sora_2_pro_image_to_video, Sora2ProImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -41,6 +42,33 @@ pub enum EnqueueSora2ProImageToVideoAspectRatio {
SixteenByNine,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSora2ProImageToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "If an OpenAI API key is provided, it will be directly charged by OpenAI.
// Otherwise, you will be charged fal credits.
// The pricing is $0.30/s for 720p and $0.50/s for 1080p."
// NB(bt): There's no way to provide an API key with this endpoint?
let duration = self.duration.unwrap_or(EnqueueSora2ProImageToVideoDurationSeconds::Four);
let resolution = self.resolution.unwrap_or(EnqueueSora2ProImageToVideoResolution::Auto); // WHAT IS THIS?!
match (duration, resolution) {
(EnqueueSora2ProImageToVideoDurationSeconds::Four, EnqueueSora2ProImageToVideoResolution::Auto) => 120, // TODO: Auto = ???
(EnqueueSora2ProImageToVideoDurationSeconds::Four, EnqueueSora2ProImageToVideoResolution::SevenTwentyP) => 120,
(EnqueueSora2ProImageToVideoDurationSeconds::Four, EnqueueSora2ProImageToVideoResolution::TenEightyP) => 200,
(EnqueueSora2ProImageToVideoDurationSeconds::Eight, EnqueueSora2ProImageToVideoResolution::Auto) => 240, // TODO: Auto = ???
(EnqueueSora2ProImageToVideoDurationSeconds::Eight, EnqueueSora2ProImageToVideoResolution::SevenTwentyP) => 240,
(EnqueueSora2ProImageToVideoDurationSeconds::Eight, EnqueueSora2ProImageToVideoResolution::TenEightyP) => 400,
(EnqueueSora2ProImageToVideoDurationSeconds::Twelve, EnqueueSora2ProImageToVideoResolution::Auto) => 360, // TODO: Auto = ???
(EnqueueSora2ProImageToVideoDurationSeconds::Twelve, EnqueueSora2ProImageToVideoResolution::SevenTwentyP) => 360,
(EnqueueSora2ProImageToVideoDurationSeconds::Twelve, EnqueueSora2ProImageToVideoResolution::TenEightyP) => 600,
}
}
}
/// Sora 2 Pro Image-to-Video
/// https://fal.ai/models/fal-ai/sora-2/image-to-video/pro
pub async fn enqueue_sora_2_pro_image_to_video_webhook<R: IntoUrl>(
args: EnqueueSora2ProImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::veo2::image_to_video::image_to_video;
use fal::endpoints::fal_ai::veo2::image_to_video::ImageToVideoInput;
use fal::webhook::WebhookResponse;
@@ -17,7 +18,7 @@ pub struct Veo2Args<'a, U: IntoUrl, V: IntoUrl> {
#[derive(Copy, Clone, Debug)]
pub enum Veo2Duration {
Default,
Default, // NB: Default is 5 seconds
FiveSeconds,
SixSeconds,
SevenSeconds,
@@ -32,11 +33,28 @@ pub enum Veo2AspectRatio {
TallNineSixteen, // 9:16
}
impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Veo2Args<'_, U, V> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $2.50.
// For every aditional second you will be charged $0.50."
match self.duration {
Veo2Duration::Default => 250,
Veo2Duration::FiveSeconds => 250,
Veo2Duration::SixSeconds => 300,
Veo2Duration::SevenSeconds => 350,
Veo2Duration::EightSeconds => 400,
}
}
}
/// Veo 2 Image-to-Video
/// https://fal.ai/models/fal-ai/veo2/image-to-video
pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Veo2Args<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Veo2Duration::Default => None,
Veo2Duration::Default => None, // NB: Default is 5 seconds.
Veo2Duration::FiveSeconds => Some("5s".to_string()),
Veo2Duration::SixSeconds => Some("6s".to_string()),
Veo2Duration::SevenSeconds => Some("7s".to_string()),
@@ -72,7 +90,7 @@ pub async fn enqueue_veo_2_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio, Veo2Duration};
use crate::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{enqueue_veo_2_image_to_video_webhook, Veo2Args, Veo2AspectRatio, Veo2Duration};
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::veo3::image_to_video_fast::{image_to_video_fast, ImageToVideoFastInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -17,7 +18,9 @@ pub struct Veo3FastArgs<'a, U: IntoUrl, V: IntoUrl> {
#[derive(Copy, Clone, Debug)]
pub enum Veo3FastDuration {
Default,
Default, // NB: Defaults to 8 seconds.
FourSeconds,
SixSeconds,
EightSeconds,
}
@@ -28,11 +31,34 @@ pub enum Veo3FastResolution {
TenEightyP,
}
impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Veo3FastArgs<'_, U, V> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For every second of video you generated, you will be charged
// $0.10 (audio off) or $0.15 (audio on).
// For example, a 5s video with audio on will cost $0.75."
match (self.duration, self.generate_audio) {
(Veo3FastDuration::FourSeconds, false) => 40,
(Veo3FastDuration::SixSeconds, false) => 60,
(Veo3FastDuration::EightSeconds, false) => 80,
(Veo3FastDuration::Default, false) => 80, // NB: Default is 8 seconds
(Veo3FastDuration::FourSeconds, true) => 60,
(Veo3FastDuration::SixSeconds, true) => 90,
(Veo3FastDuration::EightSeconds, true) => 120,
(Veo3FastDuration::Default, true) => 120, // NB: Default is 8 seconds
}
}
}
/// Veo 3 Fast Image-to-Video
/// https://fal.ai/models/fal-ai/veo3/fast/image-to-video
pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Veo3FastArgs<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Veo3FastDuration::Default => None,
Veo3FastDuration::Default => None, // NB: Defaults to 8.
Veo3FastDuration::FourSeconds => Some("4s".to_string()),
Veo3FastDuration::SixSeconds => Some("6s".to_string()),
Veo3FastDuration::EightSeconds => Some("8s".to_string()),
};
@@ -64,7 +90,7 @@ pub async fn enqueue_veo_3_fast_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastDuration, Veo3FastResolution};
use crate::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{enqueue_veo_3_fast_image_to_video_webhook, Veo3FastArgs, Veo3FastDuration, Veo3FastResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::ERNEST_GHOST_TREX_IMAGE_URL;
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::veo3::image_to_video::{image_to_video, ImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -18,7 +19,9 @@ pub struct Veo3Args<'a, U: IntoUrl, V: IntoUrl> {
#[derive(Copy, Clone, Debug)]
pub enum Veo3Duration {
Default,
Default, // Default is 8 seconds
FourSeconds,
SixSeconds,
EightSeconds,
}
@@ -37,11 +40,36 @@ pub enum Veo3Resolution {
TenEightyP,
}
impl <U: IntoUrl, V: IntoUrl> FalRequestCostCalculator for Veo3Args<'_, U, V> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For every second of video you generated, you will be charged
// $0.20 (audio off) or
// $0.40 (audio on).
// For example, a 5s video with audio on will cost $2."
match (self.duration, self.generate_audio) {
(Veo3Duration::FourSeconds, false) => 80,
(Veo3Duration::SixSeconds, false) => 120,
(Veo3Duration::EightSeconds, false) => 160,
(Veo3Duration::Default, false) => 160, // NB: Default is 8 seconds
(Veo3Duration::FourSeconds, true) => 160,
(Veo3Duration::SixSeconds, true) => 240,
(Veo3Duration::EightSeconds, true) => 320,
(Veo3Duration::Default, true) => 320, // NB: Default is 8 seconds320
}
}
}
/// Veo 3 Image-to-Video
/// https://fal.ai/models/fal-ai/veo3/image-to-video
pub async fn enqueue_veo_3_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Veo3Args<'_, U, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Veo3Duration::Default => None,
Veo3Duration::FourSeconds => Some("4s".to_string()),
Veo3Duration::SixSeconds => Some("6s".to_string()),
Veo3Duration::EightSeconds => Some("8s".to_string()),
};
@@ -81,10 +109,10 @@ pub async fn enqueue_veo_3_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3AspectRatio, Veo3Duration, Veo3Resolution};
use crate::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{enqueue_veo_3_image_to_video_webhook, Veo3Args, Veo3AspectRatio, Veo3Duration, Veo3Resolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_fast_first_last_frame_image_to_video::{veo_3p1_fast_first_last_frame_image_to_video, Veo3p1FastFirstLastFrameImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -15,10 +16,16 @@ pub struct EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs<'a, R: IntoUrl> {
// Ending frame
pub last_frame_url: String,
// Optional args
// Optional args follow
// NB: Defaults to 8 seconds
pub duration: Option<EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio>,
pub resolution: Option<EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution>,
// NB: Defaults to true. Costs more to generate audio.
pub generate_audio: Option<bool>,
// Fulfillment
@@ -36,7 +43,6 @@ pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds {
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio {
Auto,
Square,
SixteenByNine,
NineBySixteen,
}
@@ -47,6 +53,31 @@ pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution {
TenEightyP,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs<'_, R> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For every second of video you generated, you will be charged
// $0.10 (audio off) or
// $0.15 (audio on).
// For example, a 5s video with audio on will cost $0.75."
let duration = self.duration
.unwrap_or(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Eight);
let generate_audio = self.generate_audio.unwrap_or(true);
match (duration, generate_audio) {
(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Four, false) => 40,
(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six, false) => 60,
(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Eight, false) => 80,
(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Four, true) => 60,
(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six, true) => 90,
(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Eight, true) => 120,
}
}
}
/// Veo 3.1 Fast First Frame / Last Frame Image-to-Video
/// https://fal.ai/models/fal-ai/veo3.1/fast/first-last-frame-to-video
pub async fn enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook<R: IntoUrl>(
args: EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -62,7 +93,6 @@ pub async fn enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook<R: Int
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::Square => "1:1",
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::NineBySixteen => "9:16",
})
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_fast_first_frame_image_to_video::{veo_3p1_fast_first_frame_image_to_video, Veo3p1FastFirstFrameImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -33,7 +34,6 @@ pub enum EnqueueVeo3p1FastImageToVideoDurationSeconds {
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastImageToVideoAspectRatio {
Auto,
Square,
SixteenByNine,
NineBySixteen,
}
@@ -44,6 +44,32 @@ pub enum EnqueueVeo3p1FastImageToVideoResolution {
TenEightyP,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueVeo3p1FastImageToVideoArgs<'_, R> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For every second of video you generated, you will be charged
// $0.10 (audio off) or
// $0.15 (audio on).
// For example, a 5s video with audio on will cost $0.75."
let duration = self.duration
.unwrap_or(EnqueueVeo3p1FastImageToVideoDurationSeconds::Eight);
let generate_audio = self.generate_audio.unwrap_or(true);
match (duration, generate_audio) {
(EnqueueVeo3p1FastImageToVideoDurationSeconds::Four, false) => 40,
(EnqueueVeo3p1FastImageToVideoDurationSeconds::Six, false) => 60,
(EnqueueVeo3p1FastImageToVideoDurationSeconds::Eight, false) => 80,
(EnqueueVeo3p1FastImageToVideoDurationSeconds::Four, true) => 60,
(EnqueueVeo3p1FastImageToVideoDurationSeconds::Six, true) => 90,
(EnqueueVeo3p1FastImageToVideoDurationSeconds::Eight, true) => 120,
}
}
}
/// Veo 3.1 Fast Image-to-Video
/// https://fal.ai/models/fal-ai/veo3.1/fast/image-to-video
pub async fn enqueue_veo_3p1_fast_image_to_video_webhook<R: IntoUrl>(
args: EnqueueVeo3p1FastImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -59,7 +85,6 @@ pub async fn enqueue_veo_3p1_fast_image_to_video_webhook<R: IntoUrl>(
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1FastImageToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1FastImageToVideoAspectRatio::Square => "1:1",
EnqueueVeo3p1FastImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1FastImageToVideoAspectRatio::NineBySixteen => "9:16",
})
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_first_last_frame_image_to_video::{veo_3p1_first_last_frame_image_to_video, Veo3p1FirstLastFrameImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -36,7 +37,6 @@ pub enum EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds {
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio {
Auto,
Square,
SixteenByNine,
NineBySixteen,
}
@@ -47,6 +47,31 @@ pub enum EnqueueVeo3p1FirstLastFrameImageToVideoResolution {
TenEightyP,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueVeo3p1FirstLastFrameImageToVideoArgs<'_, R> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For every second of video you generated, you will be charged
// $0.20 (audio off) or
// $0.40 (audio on).
// For example, a 5s video with audio on will cost $2."
let duration = self.duration
.unwrap_or(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight);
let generate_audio = self.generate_audio.unwrap_or(true);
match (duration, generate_audio) {
(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Four, false) => 80,
(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Six, false) => 120,
(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight, false) => 160,
(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Four, true) => 160,
(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Six, true) => 240,
(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight, true) => 320,
}
}
}
/// Veo 3.1 First Frame / Last Frame Image-to-Video
/// https://fal.ai/models/fal-ai/veo3.1/first-last-frame-to-video
pub async fn enqueue_veo_3p1_first_last_frame_image_to_video_webhook<R: IntoUrl>(
args: EnqueueVeo3p1FirstLastFrameImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -62,7 +87,6 @@ pub async fn enqueue_veo_3p1_first_last_frame_image_to_video_webhook<R: IntoUrl>
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::Square => "1:1",
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::NineBySixteen => "9:16",
})
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_first_frame_image_to_video::{veo_3p1_first_frame_image_to_video, Veo3p1FirstFrameImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -33,7 +34,6 @@ pub enum EnqueueVeo3p1ImageToVideoDurationSeconds {
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1ImageToVideoAspectRatio {
Auto,
Square,
SixteenByNine,
NineBySixteen,
}
@@ -44,6 +44,31 @@ pub enum EnqueueVeo3p1ImageToVideoResolution {
TenEightyP,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueVeo3p1ImageToVideoArgs<'_, R> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For every second of video you generated, you will be charged
// $0.20 (audio off) or
// $0.40 (audio on).
// For example, a 5s video with audio on will cost $2."
let duration = self.duration
.unwrap_or(EnqueueVeo3p1ImageToVideoDurationSeconds::Eight);
let generate_audio = self.generate_audio.unwrap_or(true);
match (duration, generate_audio) {
(EnqueueVeo3p1ImageToVideoDurationSeconds::Four, false) => 80,
(EnqueueVeo3p1ImageToVideoDurationSeconds::Six, false) => 120,
(EnqueueVeo3p1ImageToVideoDurationSeconds::Eight, false) => 160,
(EnqueueVeo3p1ImageToVideoDurationSeconds::Four, true) => 160,
(EnqueueVeo3p1ImageToVideoDurationSeconds::Six, true) => 240,
(EnqueueVeo3p1ImageToVideoDurationSeconds::Eight, true) => 320,
}
}
}
/// Veo 3.1 Image-to-Video
/// https://fal.ai/models/fal-ai/veo3.1/image-to-video
pub async fn enqueue_veo_3p1_image_to_video_webhook<R: IntoUrl>(
args: EnqueueVeo3p1ImageToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -59,7 +84,6 @@ pub async fn enqueue_veo_3p1_image_to_video_webhook<R: IntoUrl>(
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1ImageToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1ImageToVideoAspectRatio::Square => "1:1",
EnqueueVeo3p1ImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1ImageToVideoAspectRatio::NineBySixteen => "9:16",
})
@@ -1,11 +1,19 @@
pub mod enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook;
pub mod enqueue_hailuo_v2p3_pro_image_to_video_webhook;
pub mod enqueue_kling_v1p6_pro_image_to_video_webhook;
pub mod enqueue_kling_v2p1_master_image_to_video_webhook;
pub mod enqueue_kling_v2p1_pro_image_to_video_webhook;
pub mod enqueue_kling_v2p5_turbo_pro_image_to_video_webhook;
pub mod enqueue_kling_v2p5_turbo_standard_image_to_video_webhook;
pub mod enqueue_kling_v2p6_pro_image_to_video_webhook;
pub mod enqueue_pixverse_v5_image_to_video_webhook;
pub mod enqueue_seedance_1_lite_image_to_video_webhook;
pub mod enqueue_seedance_1_pro_image_to_video_webhook;
pub mod enqueue_sora_2_image_to_video_webhook;
pub mod enqueue_sora_2_pro_image_to_video_webhook;
pub mod enqueue_veo_2_image_to_video_webhook;
pub mod enqueue_veo_3_fast_image_to_video_webhook;
pub mod enqueue_veo_3_image_to_video_webhook;
pub mod enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook;
pub mod enqueue_veo_3p1_fast_image_to_video_webhook;
pub mod enqueue_veo_3p1_first_last_frame_image_to_video_webhook;
@@ -1,10 +1,2 @@
pub mod enqueue_kling_16_pro_image_to_video_webhook;
pub mod enqueue_kling_21_master_image_to_video_webhook;
pub mod enqueue_kling_21_pro_image_to_video_webhook;
pub mod enqueue_seedance_1_lite_image_to_video_webhook;
pub mod enqueue_seedance_1_pro_image_to_video_webhook;
pub mod enqueue_veo_2_image_to_video_webhook;
pub mod enqueue_veo_3_fast_image_to_video_webhook;
pub mod enqueue_veo_3_image_to_video_webhook;
pub mod image;
pub mod text;
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_pro_text_to_video::{hailuo_v2p3_pro_text_to_video, HailuoV2p3ProTextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -17,6 +18,16 @@ pub struct EnqueueHailuoV2p3ProTextToVideoArgs<'a, R: IntoUrl> {
pub api_key: &'a FalApiKey,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueHailuoV2p3ProTextToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
/// "Your request will cost $0.49 per video generation."
49
}
}
/// Hailuo 2.3 Pro Text-to-Video
/// https://fal.ai/models/fal-ai/minimax/hailuo-2.3/pro/text-to-video
pub async fn enqueue_hailuo_v2p3_pro_text_to_video_webhook<R: IntoUrl>(
args: EnqueueHailuoV2p3ProTextToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_pro_text_to_video::{kling_v2p5_turbo_pro_text_to_video, KlingV2p5TurboProTextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -32,6 +33,22 @@ pub enum EnqueueKlingV2p5TurboProTextToVideoAspectRatio {
NineBySixteen,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueKlingV2p5TurboProTextToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For 5s video your request will cost $0.35.
// For every additional second you will be charged $0.07."
match self.duration {
None => 35, // $0.35
Some(EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Five) => 35, // $0.35
Some(EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Ten) => 70, // $0.35 + (5 * $0.07) = $0.70
}
}
}
/// Kling 2.5 Turbo Pro Text-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.5-turbo/pro/text-to-video
pub async fn enqueue_kling_v2p5_turbo_pro_text_to_video_webhook<R: IntoUrl>(
args: EnqueueKlingV2p5TurboProTextToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -1,6 +1,7 @@
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::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
use fal::endpoints::fal_ai::kling_video::v2_6::kling_v2p6_pro_text_to_video::{kling_v2p6_pro_text_to_video, KlingV2p6ProTextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
@@ -33,6 +34,26 @@ pub enum EnqueueKlingV2p6ProTextToVideoAspectRatio {
NineBySixteen,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueKlingV2p6ProTextToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "For every second of video you generated, you will be
// charged $0.07 (audio off) or $0.14 (audio on).
// For example, a 5s video with audio on will cost $0.70"
let generate_audio = self.generate_audio.unwrap_or(true);
let duration = self.duration.unwrap_or(EnqueueKlingV2p6ProTextToVideoDurationSeconds::Five);
match (generate_audio, duration) {
(false, EnqueueKlingV2p6ProTextToVideoDurationSeconds::Five) => 35, // audio off: $0.07 * 5 = $0.35
(false, EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten) => 70, // audio off: $0.07 * 10 = $0.70
(true, EnqueueKlingV2p6ProTextToVideoDurationSeconds::Five) => 70, // audio on: $0.14 * 5 = $0.70
(true, EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten) => 140, // audio on: $0.14 * 10 = $1.40
}
}
}
/// Kling 2.6 Pro Text-to-Video
/// https://fal.ai/models/fal-ai/kling-video/v2.6/pro/text-to-video
pub async fn enqueue_kling_v2p6_pro_text_to_video_webhook<R: IntoUrl>(
args: EnqueueKlingV2p6ProTextToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
@@ -74,9 +95,9 @@ pub async fn enqueue_kling_v2p6_pro_text_to_video_webhook<R: IntoUrl>(
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::webhook::video::text::enqueue_kling_v2p6_pro_text_to_video_webhook::{enqueue_kling_v2p6_pro_text_to_video_webhook, EnqueueKlingV2p6ProTextToVideoArgs, EnqueueKlingV2p6ProTextToVideoAspectRatio, EnqueueKlingV2p6ProTextToVideoDurationSeconds};
use errors::AnyhowResult;
use std::fs::read_to_string;
use crate::requests::webhook::video::text::enqueue_kling_v2p6_pro_text_to_video_webhook::{enqueue_kling_v2p6_pro_text_to_video_webhook, EnqueueKlingV2p6ProTextToVideoArgs, EnqueueKlingV2p6ProTextToVideoAspectRatio, EnqueueKlingV2p6ProTextToVideoDurationSeconds};
#[tokio::test]
#[ignore]

Some files were not shown because too many files have changed in this diff Show More