Compare commits

...
Author SHA1 Message Date
Brandon Thomas 05ec3d5b69 video endpoints charge, removing from image endpoints 2025-12-29 03:37:32 -05:00
Brandon Thomas c30135b228 billing on all multi-function (untested) 2025-12-29 03:25:44 -05:00
Brandon Thomas 7ad7117934 billing wip, untested 2025-12-29 03:12:36 -05:00
Brandon Thomas faf2c97825 final text-to-video models with costs 2025-12-29 02:14:15 -05:00
Brandon Thomas a3fb9d23fd sora 2 text to video costs 2025-12-29 02:09:34 -05:00
Brandon Thomas b6e899b9fa veo 3.1 text to video costs 2025-12-29 02:05:30 -05:00
Brandon Thomas 1b26ae0ce4 costs for veo 3.1 2025-12-29 02:00:12 -05:00
Brandon Thomas 12d46666c3 veo 2 and veo 3 costs 2025-12-29 01:46:15 -05:00
Brandon Thomas ca142a252a sora pricing 2025-12-29 01:29:39 -05:00
Brandon Thomas efe56050c4 seedance pro 1.0 cost 2025-12-29 01:17:57 -05:00
Brandon Thomas 02cb7558e4 seedance pricing calculation 2025-12-29 01:09:28 -05:00
Brandon Thomas 4631971865 hailuo 2.3 pro cost 2025-12-29 00:30:21 -05:00
Brandon Thomas 2af0227330 cost hailuo 2.3 fast pro 2025-12-29 00:28:44 -05:00
Brandon Thomas 6b612f4ba6 pixverse cost 2025-12-29 00:25:33 -05:00
Brandon Thomas 994f848c5c cost kling 2.6 pro 2025-12-29 00:02:50 -05:00
Brandon Thomas ed4ad1cfdd cost kling 2.5 turbo standard 2025-12-28 23:53:34 -05:00
Brandon Thomas 628fc05dba cost kling 2.5 turbo pro 2025-12-28 23:49:15 -05:00
Brandon Thomas bb56d64c05 kling 2.1 pro cost 2025-12-28 23:42:33 -05:00
Brandon Thomas 36e048e5b4 cost kling 1.6 pro 2025-12-28 23:37:07 -05:00
Brandon Thomas 95ef7d6d1f kling 2.1 master cost 2025-12-28 23:32:07 -05:00
Brandon Thomas fb34a80ac7 viz 2025-12-28 23:23:57 -05:00
Brandon Thomas 85dc8a7492 trait 2025-12-28 23:23:39 -05:00
Brandon Thomas f832237f13 module refactor done 2025-12-28 23:13:29 -05:00
Brandon Thomas 3ac322ca97 begin reorganizing library modules 2025-12-28 23:00:54 -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
BFlat a1d08460db nano banana pro updates and fixes (#926)
- working red marker
- aspect ratio and resolution buttons
- styling fixes, canvas centering/padding
- nano banana not stuck at 1 image per generation anymore
- context images working for image edit
2025-12-11 01:14:04 +07:00
Brandon Thomas ba538142e4 store more metadata on spz uploads (#925) 2025-12-09 02:15:40 -05:00
Brandon Thomas 99fd390bc5 spz file uploads (#924) 2025-12-09 00:33:14 -05:00
bflatastic cd283bf8de vite config settings modal 2025-12-08 13:17:19 +07:00
Brandon Thomas 0c6021e95c nano banana pro in tauri (#923) 2025-12-06 04:50:30 -05:00
Brandon Thomas 859d295014 tailscale dev change, remove billing test on flux dev 2025-12-06 00:43:54 -05:00
Brandon Thomas e116df736f nano banana pro (#922) 2025-12-05 21:18:59 -05:00
Brandon Thomas 9bb4a78111 CSP fixes + nix per-frame logging 2025-12-05 11:22:32 -05:00
Brandon Thomas edb0330cec rebuild package-lock.json from scratch (#921) 2025-12-05 03:05:26 -05:00
Brandon Thomas 146f152732 fix js build files 2025-12-05 01:57:23 -05:00
Brandon Thomas 32b939a67e fix rust/grok bug, package.lock change 2025-12-04 23:19:33 -05:00
Tanish Manku 866a409ff1 Add basic local 3D splat support (SPZ only) (#920)
- Disabled Outline Pass
- Disabled SAO Pass
- Disabled Bloom Pass
- Disabled SMAA Pass

SAO and Outline passes are necessary to disable for adequate
performance. No idea why.
2025-12-04 16:05:54 -08:00
Brandon Thomas 92731d87ef Fal: more models (plumbing not yet done) (#919) 2025-12-04 16:21:55 -05:00
bflatastic f574d66897 remove midjourney text 2025-12-04 12:20:09 +07:00
Brandon Thomas 21a30bde2d update website installer links 2025-12-02 13:38:34 -05:00
BFlat 6a1eeaffb4 brush edit for nano banana and other refactor + bug fixes (#918)
edit product ui refactor:
- nano banana model now has marker and eraser
- marker color can be changed, all brush, marker and eraser can be size
adjusted separately
- flux ones have edit region and eraser (removed select since i dont
think thats neccessary)
- smooth fade between history image switches
2025-12-02 22:36:59 +07:00
Brandon Thomas 00065f120e More grok fixes for the xsid script, more semantic error messages (#917) 2025-12-01 17:43:50 -05:00
Brandon Thomas e30dc03466 grok retry (#916) 2025-12-01 12:58:58 -05:00
Brandon Thomas 029789ac78 grok aspect ratios (#915) 2025-12-01 00:15:24 -05:00
BFlat 7b5550c546 3d object generator page with interactive three js result view (#906)
- reusable 3d viewer component
2025-11-28 20:56:01 +07:00
Brandon Thomas 0f0bd4c907 repair grok (#914) 2025-11-27 02:20:51 -05:00
Brandon Thomas 63f77a4b11 preserve the URL of broken thumbnails in a data- attribute for debugging (#913) 2025-11-26 23:59:29 -05:00
Brandon Thomas 5ba6a3e226 rename field 2025-11-26 22:56:54 -05:00
bflatastic 7683fe34b7 default to only artcraft provider and add sora/chatgpt provider selection for GPT image 1 2025-11-27 00:39:30 +07:00
Brandon Thomas 1d41fa8174 turn on decorations on linux 2025-11-26 07:28:53 -05:00
Brandon Thomas 8fe15839b0 notify frontend of video and object events (#912) 2025-11-26 06:23:00 -05:00
Brandon Thomas 1eada337f9 plumb through "generate audio" (only veo supports this??) (#911) 2025-11-25 04:54:34 -05:00
Brandon Thomas 016f5107e0 frontend subscriber id on image-to-video (#910) 2025-11-25 03:42:51 -05:00
Brandon Thomas 224ce3f80b add "frontend_subscriber_id" to image-to-object (#909) 2025-11-24 23:33:49 -05:00
BFlat 73a6d0494b show sound toggle for kling models (#908) 2025-11-21 22:42:49 +07:00
Brandon Thomas 0a5f220a7d remove openssl / native-tls in favor of rustls -- this lets Artcraft run on Linux! (#907) 2025-11-20 22:46:05 -05:00
Brandon Thomas d852d24226 Script to build modules - see note about warning
Note: this should not be necessary. Nx/Vite should build all of this for
us. Something in our build configuration fails to do this, which causes
us to need to build these packages.
2025-11-20 16:59:42 -05:00
BFlat e7da946e2d 3d tool pages + double gallery stack bug fix for extractor page (#904)
new pages disabled for now and coming soon tag
2025-11-19 01:59:19 +07:00
BFlat ea80dab67b watermark removal tools ui (#902) 2025-11-17 23:41:34 +07:00
bflatastic 957bb7ff2f dont load videos as blobs 2025-11-13 13:26:27 +07:00
BFlat 2f41f345a2 video frame extractor app (#901)
- extract frames from video
- save frame
- turn into frame
2025-11-13 04:09:21 +07:00
BFlat 53225932fb apps index page (#900)
- top menu selector last icon hover
- standalone index page
2025-11-13 00:15:15 +07:00
BFlat 0c353f2a8c artcraft website media page cleanup/refactor and video fixed (#899)
- fixed the video not loading issue
- refactored the media page file so that it's cleaner
- added Grok icon
2025-11-05 20:12:13 +07:00
BFlat 6036be1e01 modal now goes to previous position or centers when restored from max… (#898)
…imization
2025-11-03 14:39:10 +07:00
BFlat 197e91d4c6 bug fix: turn into video button now only sets the video promptbox reference (#897) 2025-11-03 14:34:36 +07:00
BFlat 5ddd23a8ff gallery fixes for grok, video and provider icons (#896) 2025-11-03 14:15:50 +07:00
BFlat 31eb00cfa7 change text to image background (#895) 2025-11-03 13:51:02 +07:00
BFlat 7d6f262ffe provider icons in settings to invert color properly to current theme (#894) 2025-11-03 13:35:15 +07:00
Brandon Thomas 33162e4337 update web cors for artcraft website 2025-10-31 22:34:59 -04:00
Brandon Thomas 5afb1db285 fix legacy grok metadata 2025-10-31 22:26:34 -04:00
Brandon Thomas a511e0e0a4 grok images (#893) 2025-10-31 21:55:45 -04:00
Brandon Thomas 0598aadaa1 grok logout, settings panel info 2025-10-31 05:26:44 -04:00
Brandon Thomas 06c6ff4b36 persist user details to disk 2025-10-31 05:09:08 -04:00
Brandon Thomas 5bc759e511 notify frontend of Grok 429 2025-10-31 04:52:58 -04:00
Brandon Thomas 02eafa96cc error categorization 2025-10-31 04:19:41 -04:00
Brandon Thomas 706bdb8d83 grok video enums 2025-10-31 03:36:07 -04:00
Brandon Thomas 57534c7a77 artcraft grok video integration (#892) 2025-10-31 02:59:10 -04:00
bflatastic 2228a01466 added grok icon 2025-10-31 11:57:46 +07:00
Brandon Thomas 262d931c3d grok client (not yet integrated) (#891)
- image gen works
- video gen works
- login flow works
2025-10-30 17:11:32 -04:00
BFlat a7826f8be3 ui model selector update with provider selection (#890)
sub menu inside model selector

<img width="646" height="705" alt="image"
src="https://github.com/user-attachments/assets/f946c8ea-d48d-4d0f-8dd6-95772b0161c1"
/>
2025-10-29 15:33:47 +07:00
BFlat dc0e59b8ce Link sharing from lightbox modal + website media page view (#889) 2025-10-27 11:12:06 +07:00
BFlat e1f3458c54 text to image results now pass lightbox media id and modal resize bug fix (#888)
- clicking from text to image page results now shows action buttons in
lightbox
- resizing modal from sides or corners that are not bottom or right now
doesn't move the modal once it reaches min size
- limited the min size of the lightbox modal so that the layout doesnt
break
2025-10-22 07:36:48 +07:00
BFlat e385b3c6c3 bug fixed: can't name a scene with "B" in the name because it brings … (#887)
…up the asset modal
2025-10-21 21:34:53 +07:00
BFlat dbedb25930 handle thumbnails error (#883) 2025-10-20 14:00:29 +07:00
BFlat 952d1e936c lightbox carousel for batch images (#882)
- task queue item opens in batch images carousel if there;s batch token
returned
2025-10-20 11:54:22 +07:00
Tanish Manku 88799a482d Refresh outliner items on scene load (#881) 2025-10-19 17:21:12 -07:00
Brandon Thomas 851aba7e06 midjourney task completion data 2025-10-16 20:29:09 -04:00
Brandon Thomas 8ff9323ae6 add metadata to sora uploads 2025-10-16 20:14:16 -04:00
Brandon Thomas 50b085d450 minor cleanup 2025-10-16 19:17:04 -04:00
Brandon Thomas 1b70ee7394 fixup sora sentinel tokens (#879) 2025-10-16 19:09:52 -04:00
Brandon Thomas 1c3a176705 fix docs compile errors 2025-10-15 21:18:46 -04:00
bflatastic 70c305f78d promptbox clickthrough fix 3d 2025-10-15 22:49:47 +07:00
Brandon Thomas 1c3178d073 task queue api - thumbnails, tokens, etc. (#878) 2025-10-15 02:51:08 -04:00
Tanish Manku 1243e755fe Add reset ability to 2D (#874) 2025-10-14 15:57:03 -07:00
1048 changed files with 60883 additions and 9448 deletions
+1
View File
@@ -1,6 +1,7 @@
SERVER_ENVIRONMENT=development
DATABASE_URL=mysql://storyteller:password@localhost/storyteller
#DATABASE_URL=sqlite:/tmp/tasks.sqlite
#DATABASE_URL=mysql://root:root@localhost/storyteller
#DATABASE_URL=mysql://root@localhost/storyteller
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "1e3758851fc8451670f86a9432009b73bdb81de4a08dcaa96a86afb5f9be7a68"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n FROM tasks\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
"describe": {
"columns": [
{
@@ -47,25 +47,10 @@
"name": "frontend_subscriber_payload",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "updated_at: DateTime<Utc>",
"ordinal": 10,
"type_info": "Int64"
},
{
"name": "completed_at: DateTime<Utc>",
"ordinal": 11,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
"Right": 2
},
"nullable": [
false,
@@ -76,11 +61,8 @@
true,
true,
true,
true,
false,
false,
true
]
},
"hash": "a142dc3fdf5b6e741c268d8f2e9996a28c214f5835681297d58a9431da8de939"
"hash": "38ff2df237b16f077865711dd81261de1b5fa05a9787679ce9ccd4ac576b1069"
}
@@ -0,0 +1,116 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "task_status",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "task_type",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "model_type",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "provider",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "provider_job_id",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "frontend_caller",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "frontend_subscriber_id",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "frontend_subscriber_payload",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_token",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_class",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "on_complete_batch_token",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_cdn_url",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_thumbnail_url_template",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 14,
"type_info": "Int64"
},
{
"name": "updated_at: DateTime<Utc>",
"ordinal": 15,
"type_info": "Int64"
},
{
"name": "completed_at: DateTime<Utc>",
"ordinal": 16,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true
]
},
"hash": "47f94c127abc20683b25c257aed3c2190b9bd29eb829d61392f08e10fd986767"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "50a2449b87abed7df3302a74ecd0e8ab87940077a247636680cb09b2fe488321"
}
Generated
+594 -184
View File
File diff suppressed because it is too large Load Diff
+19 -4
View File
@@ -12,11 +12,13 @@ members = [
"crates/api_clients/fal_client",
"crates/api_clients/google_drive_common",
"crates/api_clients/google_sign_in",
"crates/api_clients/grok_client",
"crates/api_clients/midjourney_client",
"crates/api_clients/openai_sora_client",
"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",
@@ -43,6 +45,7 @@ members = [
"crates/lib/caching/memory_store",
"crates/lib/caching/redis_caching",
"crates/lib/cloudflare_errors",
"crates/lib/cloudflare_mitigation",
"crates/lib/concurrency",
"crates/lib/cookie_store",
"crates/lib/cookies",
@@ -65,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",
@@ -84,6 +88,7 @@ members = [
"crates/lib/testing",
"crates/lib/tts_common",
"crates/lib/url_config",
"crates/lib/url_utils",
"crates/lib/user_input_common",
"crates/lib/web_scrapers",
#"crates/lib/api_clients/fakeyou_client",
@@ -185,6 +190,7 @@ artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
cloudflare_mitigation = { path = "crates/lib/cloudflare_mitigation" }
component_traits = { path = "crates/service/plugins/component_traits" }
cookie_store = { path = "crates/lib/cookie_store" }
crockford = { path = "crates/lib/data/crockford" }
@@ -194,6 +200,8 @@ errors = { path = "crates/lib/errors" }
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" }
@@ -210,7 +218,9 @@ subprocess_common = { path = "crates/lib/subprocess_common" }
test_data = { path = "crates/lib/test_data" }
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
@@ -227,22 +237,26 @@ actix-web-actors = "4.3.1+deprecated"
actix-web-lab = "0.20.2"
anyhow = "1.0.98"
base64 = "0.22.1"
bstr = "1.12.1"
bytes = "1.10.1"
chrono = { version = "0.4.41", features = ["serde"] }
cookie = "0.18.1"
env = "1.0.1"
env_logger = "0.11.8"
futures = { version = "0.3.31" }
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.9.1"
regex = "1.11.1"
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.12.2"
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls" ] }
reqwest-websocket = { version = "0.5.0" }
serde = { version = "1.0.219", features = ["derive"] }
serde_derive = "1.0.219"
serde_json = "1.0.140"
serde_yaml = "0.9.34+deprecated"
sha2 = "0.11.0-rc.2"
sha3 = "0.10.8"
tauri = { version = "2.8.3", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
tauri-plugin-dialog = "2.3.3"
@@ -255,6 +269,7 @@ thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["macros"] }
toml = "0.8.22"
url = { version = "2.5.4", features = ["serde"] }
uuid = "1.18.1"
# NB: wreq gets past fingerprinting, but we have to deal with platform-specific build issues.
#
@@ -266,5 +281,5 @@ url = { version = "2.5.4", features = ["serde"] }
# choco install pkgconfiglite -y
# choco install llvm -y
# choco install nasm -y
wreq = { version = "6.0.0-rc.20", features = ["json", "multipart", "cookies"] }
wreq-util = "3.0.0-rc.5"
wreq = { version = "6.0.0-rc.20", features = ["json", "multipart", "cookies", "ws", "stream"] }
wreq-util = "3.0.0-rc.6"
@@ -10,6 +10,13 @@
-- tasks_v2.sqlite - added model_type (nullable)
-- tasks_v3.sqlite - added frontend_caller, comments
-- tasks_v4.sqlite - added is_dismissed_by_user
-- tasks_v5.sqlite - added five fields:
-- on_complete_batch_token,
-- on_complete_primary_media_file_token,
-- on_complete_primary_media_file_class,
-- on_complete_primary_media_file_cdn_url,
-- on_complete_primary_media_file_thumbnail_url_template,
--
CREATE TABLE tasks (
-- Task auto-incrementing primary key.
@@ -54,6 +61,29 @@ CREATE TABLE tasks (
-- Whether the user has dismissed the task from view.
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
-- OPTIONAL.
-- When the generation completes, the batch token (if any).
-- This will point to a collection of multiple files.
on_complete_batch_token TEXT,
-- OPTIONAL.
-- When the generation completes, the media token of the "primary" media file.
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
on_complete_primary_media_file_token TEXT,
-- OPTIONAL.
-- When the generation completes, the file type of the primary media file.
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
on_complete_primary_media_file_class TEXT,
-- OPTIONAL.
-- The URL to the media file.
on_complete_primary_media_file_cdn_url TEXT,
-- OPTIONAL.
-- The URL template for the thumbnail of the primary media file.
on_complete_primary_media_file_thumbnail_url_template TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
completed_at INTEGER DEFAULT NULL
+1
View File
@@ -152,6 +152,7 @@ CREATE TABLE media_files (
-- * `vmd`
-- * `pmx`
-- * `csv`
-- * `spz` Gaussian splats
media_type VARCHAR(16) NOT NULL,
-- Broad class of media:
@@ -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;
@@ -5,4 +5,5 @@ 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;
@@ -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,
}
@@ -0,0 +1,4 @@
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_pro_multi_function_image_gen;
@@ -0,0 +1,71 @@
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_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/nano_banana_pro";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct NanoBananaProMultiFunctionImageGenRequest {
/// 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<NanoBananaProMultiFunctionImageGenNumImages>,
/// Resolution of the image to generate. Default is OneK (1K).
pub resolution: Option<NanoBananaProMultiFunctionImageGenImageResolution>,
/// Aspect ratio of the images to generate. Default is "1:1"
pub aspect_ratio: Option<NanoBananaProMultiFunctionImageGenAspectRatio>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaProMultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaProMultiFunctionImageGenImageResolution {
OneK,
TwoK,
FourK,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanoBananaProMultiFunctionImageGenAspectRatio {
// Square
OneByOne, // Default
// Wide
FiveByFour,
FourByThree,
ThreeByTwo,
SixteenByNine,
TwentyOneByNine,
// Tall
FourByFive,
ThreeByFour,
TwoByThree,
NineBySixteen, // NB: No NineByTwentyOne ?
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct NanoBananaProMultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -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,
}
@@ -6,4 +6,5 @@ pub mod jobs;
pub mod media_file;
pub mod prompts;
pub mod stripe_artcraft;
pub mod subscriptions;
pub mod subscriptions;
pub mod utils;
@@ -0,0 +1,12 @@
use crate::common::responses::media_links::MediaLinks;
/// Get the thumbnail template for either images or videos.
pub fn media_links_to_thumbnail_template(links: &MediaLinks) -> Option<&str> {
// NB: We only populate video previews for video tasks.
if let Some(video) = links.maybe_video_previews.as_ref() {
return Some(&video.animated_thumbnail_template);
}
// Image case - use the image thumbnail template if it exists.
links.maybe_thumbnail_template.as_deref()
}
@@ -0,0 +1 @@
pub mod media_links_to_thumbnail_template;
@@ -5,4 +5,5 @@ 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;
@@ -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?)
}
@@ -0,0 +1,4 @@
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_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_pro_multi_function_image_gen::{NanoBananaProMultiFunctionImageGenRequest, NanoBananaProMultiFunctionImageGenResponse, NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn nano_banana_pro_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: NanoBananaProMultiFunctionImageGenRequest,
) -> Result<NanoBananaProMultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
NANO_BANANA_PRO_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -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;
+1 -1
View File
@@ -36,7 +36,7 @@ env_logger = "0.11.3"
async-trait = "0.1.75"
# The "never-encode-slash" feature seems to have been removed:
# https://github.com/durch/rust-s3/pull/254
rust-s3 = { version = "0.36.0-beta.2" }
rust-s3 = { version = "0.36.0-beta.2", default-features = false, features = ["fail-on-err", "tags", "tokio-rustls-tls"] }
# Runtime
tokio.workspace = true
@@ -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;
@@ -0,0 +1,143 @@
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::seedream::bytedance_seedream_v4_edit_image::{bytedance_seedream_v4_edit_image, BytedanceSeedreamV4EditImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV4EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
// Optional args
pub num_images: Option<EnqueueBytedanceSeedreamV4EditImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV4EditImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV4EditImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4EditImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4EditImageMaxImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4EditImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto,
Auto2k,
Auto4k,
}
pub async fn enqueue_bytedance_seedream_v4_edit_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4EditImageNumImages::One => 1,
EnqueueBytedanceSeedreamV4EditImageNumImages::Two => 2,
EnqueueBytedanceSeedreamV4EditImageNumImages::Three => 3,
EnqueueBytedanceSeedreamV4EditImageNumImages::Four => 4,
});
let max_images = args.max_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4EditImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV4EditImageMaxImages::Two => 2,
EnqueueBytedanceSeedreamV4EditImageMaxImages::Three => 3,
EnqueueBytedanceSeedreamV4EditImageMaxImages::Four => 4,
});
let image_size = args.image_size
.map(|image_size| match image_size {
EnqueueBytedanceSeedreamV4EditImageSize::Square => "square",
EnqueueBytedanceSeedreamV4EditImageSize::SquareHd => "square_hd",
EnqueueBytedanceSeedreamV4EditImageSize::PortraitFourThree => "portrait_4_3",
EnqueueBytedanceSeedreamV4EditImageSize::PortraitSixteenNine => "portrait_16_9",
EnqueueBytedanceSeedreamV4EditImageSize::LandscapeFourThree => "landscape_4_3",
EnqueueBytedanceSeedreamV4EditImageSize::LandscapeSixteenNine => "landscape_16_9",
EnqueueBytedanceSeedreamV4EditImageSize::Auto => "auto",
EnqueueBytedanceSeedreamV4EditImageSize::Auto2k => "auto_2K",
EnqueueBytedanceSeedreamV4EditImageSize::Auto4k => "auto_4K",
})
.map(|resolution| resolution.to_string());
let request = BytedanceSeedreamV4EditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
// Optionals
num_images,
max_images,
image_size,
// Constants
enhance_prompt_mode: Some("standard".to_string()),
enable_safety_checker: Some(false),
};
let result = bytedance_seedream_v4_edit_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::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs, EnqueueBytedanceSeedreamV4EditImageMaxImages, EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{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 = EnqueueBytedanceSeedreamV4EditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4EditImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4EditImageSize::Auto2k),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_bytedance_seedream_v4_edit_image_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,142 @@
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::seedream::bytedance_seedream_v4p5_edit_image::{bytedance_seedream_v4p5_edit_image, BytedanceSeedreamV4p5EditImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV4p5EditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
// Optional args
pub num_images: Option<EnqueueBytedanceSeedreamV4p5EditImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV4p5EditImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV4p5EditImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4p5EditImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4p5EditImageMaxImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4p5EditImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto2k,
Auto4k,
}
pub async fn enqueue_bytedance_seedream_v4p5_edit_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4p5EditImageNumImages::One => 1,
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two => 2,
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Three => 3,
EnqueueBytedanceSeedreamV4p5EditImageNumImages::Four => 4,
});
let max_images = args.max_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two => 2,
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Three => 3,
EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Four => 4,
});
let image_size = args.image_size
.map(|image_size| match image_size {
EnqueueBytedanceSeedreamV4p5EditImageSize::Square => "square",
EnqueueBytedanceSeedreamV4p5EditImageSize::SquareHd => "square_hd",
EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitFourThree => "portrait_4_3",
EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitSixteenNine => "portrait_16_9",
EnqueueBytedanceSeedreamV4p5EditImageSize::LandscapeFourThree => "landscape_4_3",
EnqueueBytedanceSeedreamV4p5EditImageSize::LandscapeSixteenNine => "landscape_16_9",
EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k => "auto_2K",
EnqueueBytedanceSeedreamV4p5EditImageSize::Auto4k => "auto_4K",
})
.map(|resolution| resolution.to_string());
let request = BytedanceSeedreamV4p5EditImageInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
// Optionals
num_images,
max_images,
image_size,
// Constants
enable_safety_checker: Some(false),
seed: None,
};
let result = bytedance_seedream_v4p5_edit_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::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs, EnqueueBytedanceSeedreamV4p5EditImageMaxImages, EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{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 secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
num_images: Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_bytedance_seedream_v4p5_edit_image_webhook(args).await?;
Ok(())
}
}
@@ -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(())
}
}
@@ -0,0 +1,147 @@
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::nano_banana_pro::nano_banana_pro_image_edit::{nano_banana_pro_image_edit, NanoBananaProImageEditInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueNanoBananaProEditImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub num_images: EnqueueNanoBananaProEditImageNumImages,
// Optional args
pub resolution: Option<EnqueueNanoBananaProEditImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBananaProEditImageAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProEditImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProEditImageResolution {
OneK,
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"
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProEditImageAspectRatio {
// Square
OneByOne,
// Wide
FiveByFour,
FourByThree,
ThreeByTwo,
SixteenByNine,
TwentyOneByNine,
// Tall
FourByFive,
ThreeByFour,
TwoByThree,
NineBySixteen, // NB: No NineByTwentyOne ?
}
pub async fn enqueue_nano_banana_pro_image_edit_webhook<R: IntoUrl>(
args: EnqueueNanoBananaProEditImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
EnqueueNanoBananaProEditImageNumImages::One => 1,
EnqueueNanoBananaProEditImageNumImages::Two => 2,
EnqueueNanoBananaProEditImageNumImages::Three => 3,
EnqueueNanoBananaProEditImageNumImages::Four => 4,
};
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueNanoBananaProEditImageResolution::OneK => "1K",
EnqueueNanoBananaProEditImageResolution::TwoK => "2K",
EnqueueNanoBananaProEditImageResolution::FourK => "4K",
})
.map(|resolution| resolution.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Square
EnqueueNanoBananaProEditImageAspectRatio::OneByOne => "1:1",
// Wide
EnqueueNanoBananaProEditImageAspectRatio::FiveByFour => "5:4",
EnqueueNanoBananaProEditImageAspectRatio::FourByThree => "4:3",
EnqueueNanoBananaProEditImageAspectRatio::ThreeByTwo => "3:2",
EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine => "16:9",
EnqueueNanoBananaProEditImageAspectRatio::TwentyOneByNine => "21:9",
// Tall
EnqueueNanoBananaProEditImageAspectRatio::FourByFive => "4:5",
EnqueueNanoBananaProEditImageAspectRatio::ThreeByFour => "3:4",
EnqueueNanoBananaProEditImageAspectRatio::TwoByThree => "2:3",
EnqueueNanoBananaProEditImageAspectRatio::NineBySixteen => "9:16",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let request = NanoBananaProImageEditInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
num_images: Some(num_images),
// Optionals
aspect_ratio,
resolution,
// Constants
output_format: Some("png".to_string()),
};
let result = nano_banana_pro_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_nano_banana_pro_edit_image_webhook::{enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageAspectRatio, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution};
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 = EnqueueNanoBananaProEditImageArgs {
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: EnqueueNanoBananaProEditImageNumImages::Two,
aspect_ratio: Some(EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBananaProEditImageResolution::TwoK),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_nano_banana_pro_image_edit_webhook(args).await?;
Ok(())
}
}
@@ -1,5 +1,9 @@
pub mod enqueue_bytedance_seedream_v4_edit_image_webhook;
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;
@@ -6,3 +6,4 @@ pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
pub mod enqueue_gpt_image_1_text_to_image_webhook;
pub mod infill;
pub mod remove_background_rembg_webhook;
pub mod text;
@@ -0,0 +1,137 @@
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::seedream::bytedance_seedream_v4_text_to_image::BytedanceSeedreamV4TextToImageInput;
use fal::prelude::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::bytedance_seedream_v4_text_to_image;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV4TextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
// Optional args
pub num_images: Option<EnqueueBytedanceSeedreamV4TextToImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV4TextToImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV4TextToImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4TextToImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4TextToImageMaxImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4TextToImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto,
Auto2k,
Auto4k,
}
pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4TextToImageNumImages::One => 1,
EnqueueBytedanceSeedreamV4TextToImageNumImages::Two => 2,
EnqueueBytedanceSeedreamV4TextToImageNumImages::Three => 3,
EnqueueBytedanceSeedreamV4TextToImageNumImages::Four => 4,
});
let max_images = args.max_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4TextToImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two => 2,
EnqueueBytedanceSeedreamV4TextToImageMaxImages::Three => 3,
EnqueueBytedanceSeedreamV4TextToImageMaxImages::Four => 4,
});
let image_size = args.image_size
.map(|image_size| match image_size {
EnqueueBytedanceSeedreamV4TextToImageSize::Square => "square",
EnqueueBytedanceSeedreamV4TextToImageSize::SquareHd => "square_hd",
EnqueueBytedanceSeedreamV4TextToImageSize::PortraitFourThree => "portrait_4_3",
EnqueueBytedanceSeedreamV4TextToImageSize::PortraitSixteenNine => "portrait_16_9",
EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeFourThree => "landscape_4_3",
EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine => "landscape_16_9",
EnqueueBytedanceSeedreamV4TextToImageSize::Auto => "auto",
EnqueueBytedanceSeedreamV4TextToImageSize::Auto2k => "auto_2K",
EnqueueBytedanceSeedreamV4TextToImageSize::Auto4k => "auto_4K",
})
.map(|resolution| resolution.to_string());
let request = BytedanceSeedreamV4TextToImageInput {
prompt: args.prompt.to_string(),
// Optionals
num_images,
max_images,
image_size,
// Constants
enhance_prompt_mode: Some("standard".to_string()),
enable_safety_checker: Some(false),
};
let result = bytedance_seedream_v4_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_bytedance_seedream_v4_text_to_image_webhook::{enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs, EnqueueBytedanceSeedreamV4TextToImageMaxImages, EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageSize};
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 = EnqueueBytedanceSeedreamV4TextToImageArgs {
prompt: "an anime girl is riding a t-rex in the forest",
num_images: Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_bytedance_seedream_v4_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,134 @@
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::seedream::bytedance_seedream_v4p5_text_to_image::{bytedance_seedream_v4p5_text_to_image, BytedanceSeedreamV4p5TextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueBytedanceSeedreamV4p5TextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
// Optional args
pub num_images: Option<EnqueueBytedanceSeedreamV4p5TextToImageNumImages>,
pub max_images: Option<EnqueueBytedanceSeedreamV4p5TextToImageMaxImages>,
pub image_size: Option<EnqueueBytedanceSeedreamV4p5TextToImageSize>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4p5TextToImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4p5TextToImageMaxImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueBytedanceSeedreamV4p5TextToImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto
Auto2k,
Auto4k,
}
pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook<R: IntoUrl>(
args: EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One => 1,
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two => 2,
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Three => 3,
EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four => 4,
});
let max_images = args.max_images
.map(|num_images| match num_images {
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::One => 1,
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two => 2,
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Three => 3,
EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Four => 4,
});
let image_size = args.image_size
.map(|image_size| match image_size {
EnqueueBytedanceSeedreamV4p5TextToImageSize::Square => "square",
EnqueueBytedanceSeedreamV4p5TextToImageSize::SquareHd => "square_hd",
EnqueueBytedanceSeedreamV4p5TextToImageSize::PortraitFourThree => "portrait_4_3",
EnqueueBytedanceSeedreamV4p5TextToImageSize::PortraitSixteenNine => "portrait_16_9",
EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeFourThree => "landscape_4_3",
EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine => "landscape_16_9",
EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto2k => "auto_2K",
EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto4k => "auto_4K",
})
.map(|resolution| resolution.to_string());
let request = BytedanceSeedreamV4p5TextToImageInput {
prompt: args.prompt.to_string(),
// Optionals
num_images,
max_images,
image_size,
// Constants
enable_safety_checker: Some(false),
seed: None,
};
let result = bytedance_seedream_v4p5_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_bytedance_seedream_v4p5_text_to_image_webhook::{enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs, EnqueueBytedanceSeedreamV4p5TextToImageMaxImages, EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageSize};
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("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
prompt: "an anime girl is riding a t-rex in the forest",
num_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two),
max_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two),
image_size: Some(EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_bytedance_seedream_v4p5_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(())
}
}
@@ -0,0 +1,139 @@
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::nano_banana_pro::nano_banana_pro_text_to_image::{nano_banana_pro_text_to_image, NanoBananaProTextToImageInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueNanoBananaProTextToImageArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub num_images: EnqueueNanoBananaProTextToImageNumImages,
// Optional args
pub resolution: Option<EnqueueNanoBananaProTextToImageResolution>,
pub aspect_ratio: Option<EnqueueNanoBananaProTextToImageAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProTextToImageNumImages {
One,
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProTextToImageResolution {
OneK,
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"
#[derive(Copy, Clone, Debug)]
pub enum EnqueueNanoBananaProTextToImageAspectRatio {
// Square
OneByOne,
// Wide
FiveByFour,
FourByThree,
ThreeByTwo,
SixteenByNine,
TwentyOneByNine,
// Tall
FourByFive,
ThreeByFour,
TwoByThree,
NineBySixteen, // NB: No NineByTwentyOne ?
}
pub async fn enqueue_nano_banana_pro_text_to_image_webhook<R: IntoUrl>(
args: EnqueueNanoBananaProTextToImageArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
EnqueueNanoBananaProTextToImageNumImages::One => 1,
EnqueueNanoBananaProTextToImageNumImages::Two => 2,
EnqueueNanoBananaProTextToImageNumImages::Three => 3,
EnqueueNanoBananaProTextToImageNumImages::Four => 4,
};
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueNanoBananaProTextToImageResolution::OneK => "1K",
EnqueueNanoBananaProTextToImageResolution::TwoK => "2K",
EnqueueNanoBananaProTextToImageResolution::FourK => "4K",
})
.map(|resolution| resolution.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
// Square
EnqueueNanoBananaProTextToImageAspectRatio::OneByOne => "1:1",
// Wide
EnqueueNanoBananaProTextToImageAspectRatio::FiveByFour => "5:4",
EnqueueNanoBananaProTextToImageAspectRatio::FourByThree => "4:3",
EnqueueNanoBananaProTextToImageAspectRatio::ThreeByTwo => "3:2",
EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine => "16:9",
EnqueueNanoBananaProTextToImageAspectRatio::TwentyOneByNine => "21:9",
// Tall
EnqueueNanoBananaProTextToImageAspectRatio::FourByFive => "4:5",
EnqueueNanoBananaProTextToImageAspectRatio::ThreeByFour => "3:4",
EnqueueNanoBananaProTextToImageAspectRatio::TwoByThree => "2:3",
EnqueueNanoBananaProTextToImageAspectRatio::NineBySixteen => "9:16",
})
.map(|aspect_ratio| aspect_ratio.to_string());
let request = NanoBananaProTextToImageInput {
prompt: args.prompt.to_string(),
num_images: Some(num_images),
// Optionals
aspect_ratio,
resolution,
// Constants
output_format: Some("png".to_string()),
};
let result = nano_banana_pro_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_nano_banana_pro_text_to_image_webhook::{enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs, EnqueueNanoBananaProTextToImageAspectRatio, EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageResolution};
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 = EnqueueNanoBananaProTextToImageArgs {
prompt: "an anime girl riding on the back of a t-rex",
num_images: EnqueueNanoBananaProTextToImageNumImages::One,
aspect_ratio: Some(EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine),
resolution: Some(EnqueueNanoBananaProTextToImageResolution::TwoK),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_nano_banana_pro_text_to_image_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,4 @@
pub mod enqueue_bytedance_seedream_v4_text_to_image_webhook;
pub mod enqueue_bytedance_seedream_v4p5_text_to_image_webhook;
pub mod enqueue_nano_banana_pro_text_to_image_webhook;
pub mod enqueue_gpt_image_1p5_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(())
}
}
@@ -0,0 +1,81 @@
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;
pub struct EnqueueHailuoV2p3FastProImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
pub prompt_optimizer: Option<bool>,
// Fulfillment
pub webhook_url: R,
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> {
let prompt_optimizer = args.prompt_optimizer.unwrap_or(true);
let request = HailuoV2p3FastProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
prompt_optimizer: Some(prompt_optimizer),
};
let result = hailuo_v2p3_fast_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::image::enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook, EnqueueHailuoV2p3FastProImageToVideoArgs};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueHailuoV2p3FastProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
prompt_optimizer: Some(true),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,81 @@
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;
pub struct EnqueueHailuoV2p3ProImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
pub prompt_optimizer: Option<bool>,
// Fulfillment
pub webhook_url: R,
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> {
let prompt_optimizer = args.prompt_optimizer.unwrap_or(true);
let request = HailuoV2p3ProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
prompt_optimizer: Some(prompt_optimizer),
};
let result = hailuo_v2p3_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::image::enqueue_hailuo_v2p3_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_pro_image_to_video_webhook, EnqueueHailuoV2p3ProImageToVideoArgs};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueHailuoV2p3ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
prompt_optimizer: Some(true),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_hailuo_v2p3_pro_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::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(())
}
@@ -0,0 +1,107 @@
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;
pub struct EnqueueKlingV2p5TurboProImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
pub tail_image_url: Option<String>,
pub negative_prompt: Option<String>,
pub duration: Option<EnqueueKlingV2p5TurboProImageToVideoDurationSeconds>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueKlingV2p5TurboProImageToVideoDurationSeconds {
Five,
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",
EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Ten => "10",
})
.map(|resolution| resolution.to_string());
let request = KlingV2p5TurboProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
duration,
tail_image_url: args.tail_image_url,
negative_prompt: args.negative_prompt,
// Constants
cfg_scale: None,
};
let result = kling_v2p5_turbo_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::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::{enqueue_kling_v2p5_turbo_pro_image_to_video_webhook, EnqueueKlingV2p5TurboProImageToVideoArgs, EnqueueKlingV2p5TurboProImageToVideoDurationSeconds};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueKlingV2p5TurboProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
negative_prompt: None,
tail_image_url: None,
duration: Some(EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_v2p5_turbo_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,103 @@
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;
pub struct EnqueueKlingV2p5TurboStandardImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
pub negative_prompt: Option<String>,
pub duration: Option<EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds {
Five,
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",
EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Ten => "10",
})
.map(|resolution| resolution.to_string());
let request = KlingV2p5TurboStandardImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
duration,
negative_prompt: args.negative_prompt,
// Constants
cfg_scale: None,
};
let result = kling_v2p5_turbo_standard_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_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;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueKlingV2p5TurboStandardImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
negative_prompt: None,
duration: Some(EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Five),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_v2p5_turbo_standard_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,113 @@
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;
pub struct EnqueueKlingV2p6ProImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
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
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueKlingV2p6ProImageToVideoDurationSeconds {
Five,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueKlingV2p6ProImageToVideoDurationSeconds::Five => "5",
EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten => "10",
})
.map(|resolution| resolution.to_string());
let request = KlingV2p6ProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
duration,
negative_prompt: args.negative_prompt,
generate_audio: args.generate_audio,
};
let result = kling_v2p6_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::image::enqueue_kling_v2p6_pro_image_to_video_webhook::{enqueue_kling_v2p6_pro_image_to_video_webhook, EnqueueKlingV2p6ProImageToVideoArgs, EnqueueKlingV2p6ProImageToVideoDurationSeconds};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueKlingV2p6ProImageToVideoArgs {
image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(),
prompt: "the man says, 'these ghosts sure are scary!' and then runs. behind him, a group of ghosts appear from the trees and chase him. the camera tracks the action. finally, the man yells, 'go away ghosts'.".to_string(),
negative_prompt: None,
generate_audio: Some(true),
duration: Some(EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_v2p6_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,183 @@
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;
pub struct EnqueuePixverseV5ImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
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>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueuePixverseV5ImageToVideoDurationSeconds {
Five,
Eight,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueuePixverseV5ImageToVideoResolution {
ThreeSixtyP,
FiveFortyP,
SevenTwentyP,
TenEightyP,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueuePixverseV5ImageToVideoAspectRatio {
Square,
SixteenByNine,
FourByThree,
NineBySixteen,
ThreeByFour,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueuePixverseV5ImageToVideoStyle {
Anime,
Animation3d,
Clay,
Comic,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueuePixverseV5ImageToVideoDurationSeconds::Five => "5",
EnqueuePixverseV5ImageToVideoDurationSeconds::Eight=> "8",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueuePixverseV5ImageToVideoAspectRatio::Square => "1:1",
EnqueuePixverseV5ImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueuePixverseV5ImageToVideoAspectRatio::FourByThree => "4:3",
EnqueuePixverseV5ImageToVideoAspectRatio::NineBySixteen => "9:16",
EnqueuePixverseV5ImageToVideoAspectRatio::ThreeByFour => "3:4",
})
.map(|s| s.to_string());
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueuePixverseV5ImageToVideoResolution::ThreeSixtyP => "360p",
EnqueuePixverseV5ImageToVideoResolution::FiveFortyP => "540p",
EnqueuePixverseV5ImageToVideoResolution::SevenTwentyP => "720p",
EnqueuePixverseV5ImageToVideoResolution::TenEightyP => "1080p",
})
.map(|s| s.to_string());
let style = args.style
.map(|style| match style {
EnqueuePixverseV5ImageToVideoStyle::Anime => "anime",
EnqueuePixverseV5ImageToVideoStyle::Animation3d => "3d_animation",
EnqueuePixverseV5ImageToVideoStyle::Clay => "clay",
EnqueuePixverseV5ImageToVideoStyle::Comic => "comic",
EnqueuePixverseV5ImageToVideoStyle::Cyberpunk => "cyberpunk",
})
.map(|s| s.to_string());
let request = PixverseV5ImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
duration,
negative_prompt: args.negative_prompt,
aspect_ratio,
resolution,
style,
// Constant
seed: None,
};
let result = pixverse_v5_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_pixverse_v5_image_to_video_webhook::{enqueue_pixverse_v5_image_to_video_webhook, EnqueuePixverseV5ImageToVideoArgs, EnqueuePixverseV5ImageToVideoAspectRatio, EnqueuePixverseV5ImageToVideoDurationSeconds, EnqueuePixverseV5ImageToVideoResolution, EnqueuePixverseV5ImageToVideoStyle};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueuePixverseV5ImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
negative_prompt: None,
duration: Some(EnqueuePixverseV5ImageToVideoDurationSeconds::Five),
style: Some(EnqueuePixverseV5ImageToVideoStyle::Cyberpunk),
aspect_ratio: Some(EnqueuePixverseV5ImageToVideoAspectRatio::FourByThree),
resolution: Some(EnqueuePixverseV5ImageToVideoResolution::TenEightyP),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_pixverse_v5_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -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(())
}
}
@@ -0,0 +1,136 @@
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;
pub struct EnqueueSora2ImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
// NB: Duration defaults to four seconds
pub duration: Option<EnqueueSora2ImageToVideoDurationSeconds>,
pub resolution: Option<EnqueueSora2ImageToVideoResolution>,
pub aspect_ratio: Option<EnqueueSora2ImageToVideoAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ImageToVideoDurationSeconds {
Four,
Eight,
Twelve,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ImageToVideoResolution {
Auto,
SevenTwentyP,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ImageToVideoAspectRatio {
Auto,
NineBySixteen,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueSora2ImageToVideoDurationSeconds::Four => 4,
EnqueueSora2ImageToVideoDurationSeconds::Eight => 8,
EnqueueSora2ImageToVideoDurationSeconds::Twelve => 12,
});
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueSora2ImageToVideoResolution::Auto => "auto",
EnqueueSora2ImageToVideoResolution::SevenTwentyP => "720p",
})
.map(|resolution| resolution.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueSora2ImageToVideoAspectRatio::Auto => "auto",
EnqueueSora2ImageToVideoAspectRatio::NineBySixteen => "9:16",
EnqueueSora2ImageToVideoAspectRatio::SixteenByNine => "16:9",
})
.map(|resolution| resolution.to_string());
let request = Sora2ImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
duration,
resolution,
aspect_ratio,
// Constants
delete_video: Some(false),
};
let result = sora_2_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_sora_2_image_to_video_webhook::{enqueue_sora_2_image_to_video_webhook, EnqueueSora2ImageToVideoArgs, EnqueueSora2ImageToVideoAspectRatio, EnqueueSora2ImageToVideoDurationSeconds, EnqueueSora2ImageToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueSora2ImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
duration: Some(EnqueueSora2ImageToVideoDurationSeconds::Twelve),
aspect_ratio: Some(EnqueueSora2ImageToVideoAspectRatio::SixteenByNine),
resolution: Some(EnqueueSora2ImageToVideoResolution::SevenTwentyP),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,148 @@
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;
pub struct EnqueueSora2ProImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
pub image_url: String,
// Optional args
pub resolution: Option<EnqueueSora2ProImageToVideoResolution>,
pub duration: Option<EnqueueSora2ProImageToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueSora2ProImageToVideoAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ProImageToVideoDurationSeconds {
Four,
Eight,
Twelve,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ProImageToVideoResolution {
Auto,
SevenTwentyP,
TenEightyP,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ProImageToVideoAspectRatio {
Auto,
NineBySixteen,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueSora2ProImageToVideoDurationSeconds::Four => 4,
EnqueueSora2ProImageToVideoDurationSeconds::Eight => 8,
EnqueueSora2ProImageToVideoDurationSeconds::Twelve => 12,
});
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueSora2ProImageToVideoResolution::Auto => "auto",
EnqueueSora2ProImageToVideoResolution::SevenTwentyP => "720p",
EnqueueSora2ProImageToVideoResolution::TenEightyP => "1080p",
})
.map(|resolution| resolution.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueSora2ProImageToVideoAspectRatio::Auto => "auto",
EnqueueSora2ProImageToVideoAspectRatio::NineBySixteen => "9:16",
EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine => "16:9",
})
.map(|resolution| resolution.to_string());
let request = Sora2ProImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
// Optionals
duration,
resolution,
aspect_ratio,
// Constants
delete_video: Some(false),
};
let result = sora_2_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::image::enqueue_sora_2_pro_image_to_video_webhook::{enqueue_sora_2_pro_image_to_video_webhook, EnqueueSora2ProImageToVideoArgs, EnqueueSora2ProImageToVideoAspectRatio, EnqueueSora2ProImageToVideoDurationSeconds, EnqueueSora2ProImageToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueSora2ProImageToVideoArgs {
image_url: TREX_SKELETON_IMAGE_URL.to_string(),
prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(),
duration: Some(EnqueueSora2ProImageToVideoDurationSeconds::Twelve),
aspect_ratio: Some(EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine),
resolution: Some(EnqueueSora2ProImageToVideoResolution::TenEightyP),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_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::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]
@@ -0,0 +1,159 @@
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;
pub struct EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Starting frame
pub first_frame_url: String,
// Ending frame
pub last_frame_url: String,
// 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
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds {
Four,
Six,
Eight,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio {
Auto,
SixteenByNine,
NineBySixteen,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution {
SevenTwentyP,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Four => "4s",
EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six => "6s",
EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Eight=> "8s",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::SevenTwentyP => "720p",
EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::TenEightyP => "1080p",
})
.map(|s| s.to_string());
let request = Veo3p1FastFirstLastFrameImageToVideoInput {
prompt: args.prompt,
first_frame_url: args.first_frame_url,
last_frame_url: args.last_frame_url,
generate_audio: args.generate_audio,
// Optionals
duration,
aspect_ratio,
resolution,
};
let result = veo_3p1_fast_first_last_frame_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_veo_3p1_fast_first_last_frame_image_to_video_webhook::{enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook, EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs, EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio, EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds, EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{TALL_CORGI_SHIBA_TREASURE_OCEAN_URL, TALL_CORGI_SHIBA_TREASURE_SKY_URL};
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs {
first_frame_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
last_frame_url: TALL_CORGI_SHIBA_TREASURE_SKY_URL.to_string(),
prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(),
duration: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six),
aspect_ratio: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::NineBySixteen),
resolution: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::SevenTwentyP),
generate_audio: Some(true),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,149 @@
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;
pub struct EnqueueVeo3p1FastImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Starting frame
pub image_url: String,
// Optional args
pub duration: Option<EnqueueVeo3p1FastImageToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueVeo3p1FastImageToVideoAspectRatio>,
pub resolution: Option<EnqueueVeo3p1FastImageToVideoResolution>,
pub generate_audio: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastImageToVideoDurationSeconds {
Four,
Six,
Eight,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastImageToVideoAspectRatio {
Auto,
SixteenByNine,
NineBySixteen,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastImageToVideoResolution {
SevenTwentyP,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueVeo3p1FastImageToVideoDurationSeconds::Four => "4s",
EnqueueVeo3p1FastImageToVideoDurationSeconds::Six => "6s",
EnqueueVeo3p1FastImageToVideoDurationSeconds::Eight=> "8s",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1FastImageToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1FastImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1FastImageToVideoAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueVeo3p1FastImageToVideoResolution::SevenTwentyP => "720p",
EnqueueVeo3p1FastImageToVideoResolution::TenEightyP => "1080p",
})
.map(|s| s.to_string());
let request = Veo3p1FastFirstFrameImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
generate_audio: args.generate_audio,
// Optionals
duration,
aspect_ratio,
resolution,
};
let result = veo_3p1_fast_first_frame_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_veo_3p1_fast_image_to_video_webhook::{enqueue_veo_3p1_fast_image_to_video_webhook, EnqueueVeo3p1FastImageToVideoArgs, EnqueueVeo3p1FastImageToVideoAspectRatio, EnqueueVeo3p1FastImageToVideoDurationSeconds, EnqueueVeo3p1FastImageToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TALL_CORGI_SHIBA_TREASURE_OCEAN_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueVeo3p1FastImageToVideoArgs {
image_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(),
duration: Some(EnqueueVeo3p1FastImageToVideoDurationSeconds::Four),
aspect_ratio: Some(EnqueueVeo3p1FastImageToVideoAspectRatio::SixteenByNine),
resolution: Some(EnqueueVeo3p1FastImageToVideoResolution::TenEightyP),
generate_audio: Some(true),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3p1_fast_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,153 @@
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;
pub struct EnqueueVeo3p1FirstLastFrameImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Starting frame
pub first_frame_url: String,
// Ending frame
pub last_frame_url: String,
// Optional args
pub duration: Option<EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio>,
pub resolution: Option<EnqueueVeo3p1FirstLastFrameImageToVideoResolution>,
pub generate_audio: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds {
Four,
Six,
Eight,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio {
Auto,
SixteenByNine,
NineBySixteen,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FirstLastFrameImageToVideoResolution {
SevenTwentyP,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Four => "4s",
EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Six => "6s",
EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight=> "8s",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueVeo3p1FirstLastFrameImageToVideoResolution::SevenTwentyP => "720p",
EnqueueVeo3p1FirstLastFrameImageToVideoResolution::TenEightyP => "1080p",
})
.map(|s| s.to_string());
let request = Veo3p1FirstLastFrameImageToVideoInput {
prompt: args.prompt,
first_frame_url: args.first_frame_url,
last_frame_url: args.last_frame_url,
generate_audio: args.generate_audio,
// Optionals
duration,
aspect_ratio,
resolution,
};
let result = veo_3p1_first_last_frame_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_veo_3p1_first_last_frame_image_to_video_webhook::{enqueue_veo_3p1_first_last_frame_image_to_video_webhook, EnqueueVeo3p1FirstLastFrameImageToVideoArgs, EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio, EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds, EnqueueVeo3p1FirstLastFrameImageToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{TALL_CORGI_SHIBA_TREASURE_OCEAN_URL, TALL_CORGI_SHIBA_TREASURE_SKY_URL};
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueVeo3p1FirstLastFrameImageToVideoArgs {
first_frame_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
last_frame_url: TALL_CORGI_SHIBA_TREASURE_SKY_URL.to_string(),
prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(),
duration: Some(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight),
aspect_ratio: Some(EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::NineBySixteen),
resolution: Some(EnqueueVeo3p1FirstLastFrameImageToVideoResolution::TenEightyP),
generate_audio: Some(true),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3p1_first_last_frame_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,148 @@
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;
pub struct EnqueueVeo3p1ImageToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Starting frame
pub image_url: String,
// Optional args
pub duration: Option<EnqueueVeo3p1ImageToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueVeo3p1ImageToVideoAspectRatio>,
pub resolution: Option<EnqueueVeo3p1ImageToVideoResolution>,
pub generate_audio: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1ImageToVideoDurationSeconds {
Four,
Six,
Eight,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1ImageToVideoAspectRatio {
Auto,
SixteenByNine,
NineBySixteen,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1ImageToVideoResolution {
SevenTwentyP,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueVeo3p1ImageToVideoDurationSeconds::Four => "4s",
EnqueueVeo3p1ImageToVideoDurationSeconds::Six => "6s",
EnqueueVeo3p1ImageToVideoDurationSeconds::Eight=> "8s",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1ImageToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1ImageToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1ImageToVideoAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueVeo3p1ImageToVideoResolution::SevenTwentyP => "720p",
EnqueueVeo3p1ImageToVideoResolution::TenEightyP => "1080p",
})
.map(|s| s.to_string());
let request = Veo3p1FirstFrameImageToVideoInput {
prompt: args.prompt,
image_url: args.image_url,
generate_audio: args.generate_audio,
// Optionals
duration,
aspect_ratio,
resolution,
};
let result = veo_3p1_first_frame_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_veo_3p1_image_to_video_webhook::{enqueue_veo_3p1_image_to_video_webhook, EnqueueVeo3p1ImageToVideoArgs, EnqueueVeo3p1ImageToVideoAspectRatio, EnqueueVeo3p1ImageToVideoDurationSeconds, EnqueueVeo3p1ImageToVideoResolution};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::TALL_CORGI_SHIBA_TREASURE_OCEAN_URL;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueVeo3p1ImageToVideoArgs {
image_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(),
prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(),
duration: Some(EnqueueVeo3p1ImageToVideoDurationSeconds::Eight),
aspect_ratio: Some(EnqueueVeo3p1ImageToVideoAspectRatio::NineBySixteen),
resolution: Some(EnqueueVeo3p1ImageToVideoResolution::TenEightyP),
generate_audio: Some(true),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3p1_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,20 @@
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;
pub mod enqueue_veo_3p1_image_to_video_webhook;
@@ -1,8 +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;
@@ -0,0 +1,77 @@
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;
pub struct EnqueueHailuoV2p3ProTextToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Optional args
pub prompt_optimizer: Option<bool>,
// Fulfillment
pub webhook_url: R,
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> {
let prompt_optimizer = args.prompt_optimizer.unwrap_or(true);
let request = HailuoV2p3ProTextToVideoInput {
prompt: args.prompt,
// Optionals
prompt_optimizer: Some(prompt_optimizer),
};
let result = hailuo_v2p3_pro_text_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::text::enqueue_hailuo_v2p3_pro_text_to_video_webhook::{enqueue_hailuo_v2p3_pro_text_to_video_webhook, EnqueueHailuoV2p3ProTextToVideoArgs};
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("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueHailuoV2p3ProTextToVideoArgs {
prompt: "a gray alien with big eyes dressed in an american flag tank top gives the peace symbol, it then barbecues some hot dogs on the grill".to_string(),
prompt_optimizer: Some(true),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_hailuo_v2p3_pro_text_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,117 @@
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;
pub struct EnqueueKlingV2p5TurboProTextToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Optional args
pub negative_prompt: Option<String>,
pub duration: Option<EnqueueKlingV2p5TurboProTextToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueKlingV2p5TurboProTextToVideoAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueKlingV2p5TurboProTextToVideoDurationSeconds {
Five,
Ten,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueKlingV2p5TurboProTextToVideoAspectRatio {
Square,
SixteenByNine,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Five => "5",
EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Ten => "10",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect| match aspect {
EnqueueKlingV2p5TurboProTextToVideoAspectRatio::Square => "1:1",
EnqueueKlingV2p5TurboProTextToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueKlingV2p5TurboProTextToVideoAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let request = KlingV2p5TurboProTextToVideoInput {
prompt: args.prompt,
// Optionals
duration,
aspect_ratio,
negative_prompt: args.negative_prompt,
// Constants
cfg_scale: None,
};
let result = kling_v2p5_turbo_pro_text_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::text::enqueue_kling_v2p5_turbo_pro_text_to_video_webhook::{enqueue_kling_v2p5_turbo_pro_text_to_video_webhook, EnqueueKlingV2p5TurboProTextToVideoArgs, EnqueueKlingV2p5TurboProTextToVideoAspectRatio, EnqueueKlingV2p5TurboProTextToVideoDurationSeconds};
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("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueKlingV2p5TurboProTextToVideoArgs {
prompt: "an owl flies by an abandoned castle at dusk. the camera tracks the flying owl. the castle stones are damp and gritty. there are lots of trees. fireflies dance over the fields".to_string(),
negative_prompt: None,
aspect_ratio: Some(EnqueueKlingV2p5TurboProTextToVideoAspectRatio::SixteenByNine),
duration: Some(EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Five),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_v2p5_turbo_pro_text_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,124 @@
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;
pub struct EnqueueKlingV2p6ProTextToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Optional args
pub generate_audio: Option<bool>,
pub negative_prompt: Option<String>,
pub duration: Option<EnqueueKlingV2p6ProTextToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueKlingV2p6ProTextToVideoAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueKlingV2p6ProTextToVideoDurationSeconds {
Five,
Ten,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueKlingV2p6ProTextToVideoAspectRatio {
Square,
SixteenByNine,
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> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueKlingV2p6ProTextToVideoDurationSeconds::Five => "5",
EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten => "10",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect| match aspect {
EnqueueKlingV2p6ProTextToVideoAspectRatio::Square => "1:1",
EnqueueKlingV2p6ProTextToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueKlingV2p6ProTextToVideoAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let request = KlingV2p6ProTextToVideoInput {
prompt: args.prompt,
// Optionals
generate_audio: args.generate_audio,
duration,
aspect_ratio,
negative_prompt: args.negative_prompt,
// Constants
cfg_scale: None,
};
let result = kling_v2p6_pro_text_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::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;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueKlingV2p6ProTextToVideoArgs {
prompt: "a humanoid lizard creature walks around on the surface of an alien planet. the sky is purple and there are yellow plants. The lizard says, 'You should consider day trading'. Suddenly a giant t-rex comes in from off screen and eats the lizard creature.".to_string(),
negative_prompt: None,
aspect_ratio: Some(EnqueueKlingV2p6ProTextToVideoAspectRatio::SixteenByNine),
duration: Some(EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten),
generate_audio: Some(true),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_v2p6_pro_text_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,148 @@
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::pixverse::v5::pixverse_v5_text_to_video::{pixverse_v5_text_to_video, PixverseV5TextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueuePixverseV5TextToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Optional args
pub negative_prompt: Option<String>,
// NB: eight-second videos do not work with 1080P
pub duration: Option<EnqueuePixverseV5TextToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueuePixverseV5TextToVideoAspectRatio>,
pub resolution: Option<EnqueuePixverseV5TextToVideoResolution>,
pub style: Option<EnqueuePixverseV5TextToVideoStyle>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueuePixverseV5TextToVideoDurationSeconds {
Five,
Eight,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueuePixverseV5TextToVideoResolution {
ThreeSixtyP,
FiveFortyP,
SevenTwentyP,
TenEightyP,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueuePixverseV5TextToVideoAspectRatio {
Square,
SixteenByNine,
FourByThree,
NineBySixteen,
ThreeByFour,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueuePixverseV5TextToVideoStyle {
Anime,
Animation3d,
Clay,
Comic,
Cyberpunk,
}
pub async fn enqueue_pixverse_v5_text_to_video_webhook<R: IntoUrl>(
args: EnqueuePixverseV5TextToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueuePixverseV5TextToVideoDurationSeconds::Five => "5",
EnqueuePixverseV5TextToVideoDurationSeconds::Eight=> "8",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueuePixverseV5TextToVideoAspectRatio::Square => "1:1",
EnqueuePixverseV5TextToVideoAspectRatio::SixteenByNine => "16:9",
EnqueuePixverseV5TextToVideoAspectRatio::FourByThree => "4:3",
EnqueuePixverseV5TextToVideoAspectRatio::NineBySixteen => "9:16",
EnqueuePixverseV5TextToVideoAspectRatio::ThreeByFour => "3:4",
})
.map(|s| s.to_string());
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueuePixverseV5TextToVideoResolution::ThreeSixtyP => "360p",
EnqueuePixverseV5TextToVideoResolution::FiveFortyP => "540p",
EnqueuePixverseV5TextToVideoResolution::SevenTwentyP => "720p",
EnqueuePixverseV5TextToVideoResolution::TenEightyP => "1080p",
})
.map(|s| s.to_string());
let style = args.style
.map(|style| match style {
EnqueuePixverseV5TextToVideoStyle::Anime => "anime",
EnqueuePixverseV5TextToVideoStyle::Animation3d => "3d_animation",
EnqueuePixverseV5TextToVideoStyle::Clay => "clay",
EnqueuePixverseV5TextToVideoStyle::Comic => "comic",
EnqueuePixverseV5TextToVideoStyle::Cyberpunk => "cyberpunk",
})
.map(|s| s.to_string());
let request = PixverseV5TextToVideoInput {
prompt: args.prompt,
// Optionals
duration,
negative_prompt: args.negative_prompt,
aspect_ratio,
resolution,
style,
// Constant
seed: None,
};
let result = pixverse_v5_text_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::text::enqueue_pixverse_v5_text_to_video_webhook::{enqueue_pixverse_v5_text_to_video_webhook, EnqueuePixverseV5TextToVideoArgs, EnqueuePixverseV5TextToVideoAspectRatio, EnqueuePixverseV5TextToVideoDurationSeconds, EnqueuePixverseV5TextToVideoResolution, EnqueuePixverseV5TextToVideoStyle};
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("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueuePixverseV5TextToVideoArgs {
prompt: "an angry racoon shakes its fist at the garbage truck as it drives away, the camera orbits the racoon. the racoon sighs".to_string(),
negative_prompt: None,
duration: Some(EnqueuePixverseV5TextToVideoDurationSeconds::Eight),
style: Some(EnqueuePixverseV5TextToVideoStyle::Anime),
aspect_ratio: Some(EnqueuePixverseV5TextToVideoAspectRatio::FourByThree),
resolution: Some(EnqueuePixverseV5TextToVideoResolution::SevenTwentyP),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_pixverse_v5_text_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,145 @@
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_text_to_video::{sora_2_pro_text_to_video, Sora2ProTextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueSora2ProTextToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Optional args
pub resolution: Option<EnqueueSora2ProTextToVideoResolution>,
pub duration: Option<EnqueueSora2ProTextToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueSora2ProTextToVideoAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ProTextToVideoDurationSeconds {
Four,
Eight,
Twelve,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ProTextToVideoResolution {
Auto,
SevenTwentyP,
TenEightyP,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2ProTextToVideoAspectRatio {
Auto,
NineBySixteen,
SixteenByNine,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSora2ProTextToVideoArgs<'_, 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(EnqueueSora2ProTextToVideoDurationSeconds::Four);
let resolution = self.resolution.unwrap_or(EnqueueSora2ProTextToVideoResolution::Auto); // WHAT IS THIS?!
match (duration, resolution) {
(EnqueueSora2ProTextToVideoDurationSeconds::Four, EnqueueSora2ProTextToVideoResolution::Auto) => 120, // TODO: Auto = ???
(EnqueueSora2ProTextToVideoDurationSeconds::Four, EnqueueSora2ProTextToVideoResolution::SevenTwentyP) => 120,
(EnqueueSora2ProTextToVideoDurationSeconds::Four, EnqueueSora2ProTextToVideoResolution::TenEightyP) => 200,
(EnqueueSora2ProTextToVideoDurationSeconds::Eight, EnqueueSora2ProTextToVideoResolution::Auto) => 240, // TODO: Auto = ???
(EnqueueSora2ProTextToVideoDurationSeconds::Eight, EnqueueSora2ProTextToVideoResolution::SevenTwentyP) => 240,
(EnqueueSora2ProTextToVideoDurationSeconds::Eight, EnqueueSora2ProTextToVideoResolution::TenEightyP) => 400,
(EnqueueSora2ProTextToVideoDurationSeconds::Twelve, EnqueueSora2ProTextToVideoResolution::Auto) => 360, // TODO: Auto = ???
(EnqueueSora2ProTextToVideoDurationSeconds::Twelve, EnqueueSora2ProTextToVideoResolution::SevenTwentyP) => 360,
(EnqueueSora2ProTextToVideoDurationSeconds::Twelve, EnqueueSora2ProTextToVideoResolution::TenEightyP) => 600,
}
}
}
/// Sora 2 Pro Text-to-Video
/// https://fal.ai/models/fal-ai/sora-2/text-to-video/pro
pub async fn enqueue_sora_2_pro_text_to_video_webhook<R: IntoUrl>(
args: EnqueueSora2ProTextToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueSora2ProTextToVideoDurationSeconds::Four => 4,
EnqueueSora2ProTextToVideoDurationSeconds::Eight => 8,
EnqueueSora2ProTextToVideoDurationSeconds::Twelve => 12,
});
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueSora2ProTextToVideoResolution::Auto => "auto",
EnqueueSora2ProTextToVideoResolution::SevenTwentyP => "720p",
EnqueueSora2ProTextToVideoResolution::TenEightyP => "1080p",
})
.map(|resolution| resolution.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueSora2ProTextToVideoAspectRatio::Auto => "auto",
EnqueueSora2ProTextToVideoAspectRatio::NineBySixteen => "9:16",
EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine => "16:9",
})
.map(|resolution| resolution.to_string());
let request = Sora2ProTextToVideoInput {
prompt: args.prompt,
// Optionals
duration,
resolution,
aspect_ratio,
// Constants
delete_video: Some(false),
};
let result = sora_2_pro_text_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::text::enqueue_sora_2_pro_text_to_video_webhook::{enqueue_sora_2_pro_text_to_video_webhook, EnqueueSora2ProTextToVideoArgs, EnqueueSora2ProTextToVideoAspectRatio, EnqueueSora2ProTextToVideoDurationSeconds, EnqueueSora2ProTextToVideoResolution};
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("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueSora2ProTextToVideoArgs {
prompt: "a dinosaur turns to the camera and asks, 'do you have adequate car insurance?' it then stomps off and attacks a brontosaurus".to_string(),
duration: Some(EnqueueSora2ProTextToVideoDurationSeconds::Eight),
aspect_ratio: Some(EnqueueSora2ProTextToVideoAspectRatio::NineBySixteen),
resolution: Some(EnqueueSora2ProTextToVideoResolution::SevenTwentyP),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_pro_text_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,130 @@
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_text_to_video::{sora_2_text_to_video, Sora2TextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueSora2TextToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Optional args
pub resolution: Option<EnqueueSora2TextToVideoResolution>,
pub duration: Option<EnqueueSora2TextToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueSora2TextToVideoAspectRatio>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2TextToVideoDurationSeconds {
Four,
Eight,
Twelve,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2TextToVideoResolution {
Auto,
SevenTwentyP,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueSora2TextToVideoAspectRatio {
Auto,
NineBySixteen,
SixteenByNine,
}
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueSora2TextToVideoArgs<'_, U> {
fn calculate_cost_in_cents(&self) -> UsdCents {
// "The pricing is $0.1/s for Sora 2."
let duration = self.duration.unwrap_or(EnqueueSora2TextToVideoDurationSeconds::Four);
match duration {
EnqueueSora2TextToVideoDurationSeconds::Four => 40, // $0.10 * 4
EnqueueSora2TextToVideoDurationSeconds::Eight => 80, // $0.10 * 8
EnqueueSora2TextToVideoDurationSeconds::Twelve => 120 // $0.10 * 12
}
}
}
/// Sora 2 Text-to-Video
/// https://fal.ai/models/fal-ai/sora-2/text-to-video
pub async fn enqueue_sora_2_text_to_video_webhook<R: IntoUrl>(
args: EnqueueSora2TextToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueSora2TextToVideoDurationSeconds::Four => 4,
EnqueueSora2TextToVideoDurationSeconds::Eight => 8,
EnqueueSora2TextToVideoDurationSeconds::Twelve => 12,
});
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueSora2TextToVideoResolution::Auto => "auto",
EnqueueSora2TextToVideoResolution::SevenTwentyP => "720p",
})
.map(|resolution| resolution.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueSora2TextToVideoAspectRatio::Auto => "auto",
EnqueueSora2TextToVideoAspectRatio::NineBySixteen => "9:16",
EnqueueSora2TextToVideoAspectRatio::SixteenByNine => "16:9",
})
.map(|resolution| resolution.to_string());
let request = Sora2TextToVideoInput {
prompt: args.prompt,
// Optionals
duration,
resolution,
aspect_ratio,
// Constants
delete_video: Some(false),
};
let result = sora_2_text_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::text::enqueue_sora_2_text_to_video_webhook::{enqueue_sora_2_text_to_video_webhook, EnqueueSora2TextToVideoArgs, EnqueueSora2TextToVideoAspectRatio, EnqueueSora2TextToVideoDurationSeconds, EnqueueSora2TextToVideoResolution};
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("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueSora2TextToVideoArgs {
prompt: "a dinosaur turns to the camera and asks, 'do you have adequate car insurance?' it then stomps off and attacks a brontosaurus".to_string(),
duration: Some(EnqueueSora2TextToVideoDurationSeconds::Eight),
aspect_ratio: Some(EnqueueSora2TextToVideoAspectRatio::NineBySixteen),
resolution: Some(EnqueueSora2TextToVideoResolution::SevenTwentyP),
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_sora_2_text_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,154 @@
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_text_to_video::{veo_3p1_fast_text_to_video, Veo3p1FastTextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueVeo3p1FastTextToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Optional args
pub duration: Option<EnqueueVeo3p1FastTextToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueVeo3p1FastTextToVideoAspectRatio>,
pub resolution: Option<EnqueueVeo3p1FastTextToVideoResolution>,
pub generate_audio: Option<bool>,
pub enhance_prompt: Option<bool>,
pub negative_prompt: Option<String>,
pub seed: Option<i64>,
pub auto_fix: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastTextToVideoDurationSeconds {
Four,
Six,
Eight,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastTextToVideoAspectRatio {
Auto,
SixteenByNine,
NineBySixteen,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1FastTextToVideoResolution {
SevenTwentyP,
TenEightyP,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueVeo3p1FastTextToVideoArgs<'_, 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(EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight);
let generate_audio = self.generate_audio.unwrap_or(true);
match (duration, generate_audio) {
(EnqueueVeo3p1FastTextToVideoDurationSeconds::Four, false) => 40,
(EnqueueVeo3p1FastTextToVideoDurationSeconds::Six, false) => 60,
(EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight, false) => 80,
(EnqueueVeo3p1FastTextToVideoDurationSeconds::Four, true) => 60,
(EnqueueVeo3p1FastTextToVideoDurationSeconds::Six, true) => 90,
(EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight, true) => 120,
}
}
}
/// Veo 3.1 Fast Text-to-Video
/// https://fal.ai/models/fal-ai/veo3.1/fast
pub async fn enqueue_veo_3p1_fast_text_to_video_webhook<R: IntoUrl>(
args: EnqueueVeo3p1FastTextToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueVeo3p1FastTextToVideoDurationSeconds::Four => "4s",
EnqueueVeo3p1FastTextToVideoDurationSeconds::Six => "6s",
EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight=> "8s",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1FastTextToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1FastTextToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1FastTextToVideoAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueVeo3p1FastTextToVideoResolution::SevenTwentyP => "720p",
EnqueueVeo3p1FastTextToVideoResolution::TenEightyP => "1080p",
})
.map(|s| s.to_string());
let request = Veo3p1FastTextToVideoInput {
prompt: args.prompt,
// Optionals
duration,
aspect_ratio,
resolution,
negative_prompt: args.negative_prompt,
generate_audio: args.generate_audio,
enhance_prompt: args.enhance_prompt,
seed: args.seed,
auto_fix: args.auto_fix,
};
let result = veo_3p1_fast_text_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::text::enqueue_veo_3p1_fast_text_to_video_webhook::{enqueue_veo_3p1_fast_text_to_video_webhook, EnqueueVeo3p1FastTextToVideoArgs, EnqueueVeo3p1FastTextToVideoAspectRatio, EnqueueVeo3p1FastTextToVideoDurationSeconds, EnqueueVeo3p1FastTextToVideoResolution};
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("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueVeo3p1FastTextToVideoArgs {
prompt: "An alien space ship hovers over new york city. it looks ominous, ready to attack. suddenly, it drops a bunch of ping pong balls on the city".to_string(),
duration: Some(EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight),
aspect_ratio: Some(EnqueueVeo3p1FastTextToVideoAspectRatio::SixteenByNine),
resolution: Some(EnqueueVeo3p1FastTextToVideoResolution::TenEightyP),
generate_audio: Some(true),
negative_prompt: None,
enhance_prompt: Some(true),
auto_fix: Some(true),
seed: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3p1_fast_text_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,155 @@
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_text_to_video::{veo_3p1_text_to_video, Veo3p1TextToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct EnqueueVeo3p1TextToVideoArgs<'a, R: IntoUrl> {
// Request required
pub prompt: String,
// Optional args
pub duration: Option<EnqueueVeo3p1TextToVideoDurationSeconds>,
pub aspect_ratio: Option<EnqueueVeo3p1TextToVideoAspectRatio>,
pub resolution: Option<EnqueueVeo3p1TextToVideoResolution>,
pub generate_audio: Option<bool>,
pub enhance_prompt: Option<bool>,
pub negative_prompt: Option<String>,
pub seed: Option<i64>,
pub auto_fix: Option<bool>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1TextToVideoDurationSeconds {
Four,
Six,
Eight,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1TextToVideoAspectRatio {
Auto,
SixteenByNine,
NineBySixteen,
}
#[derive(Copy, Clone, Debug)]
pub enum EnqueueVeo3p1TextToVideoResolution {
SevenTwentyP,
TenEightyP,
}
impl <R: IntoUrl> FalRequestCostCalculator for EnqueueVeo3p1TextToVideoArgs<'_, 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(EnqueueVeo3p1TextToVideoDurationSeconds::Eight);
let generate_audio = self.generate_audio.unwrap_or(true);
match (duration, generate_audio) {
(EnqueueVeo3p1TextToVideoDurationSeconds::Four, false) => 80,
(EnqueueVeo3p1TextToVideoDurationSeconds::Six, false) => 120,
(EnqueueVeo3p1TextToVideoDurationSeconds::Eight, false) => 160,
(EnqueueVeo3p1TextToVideoDurationSeconds::Four, true) => 160,
(EnqueueVeo3p1TextToVideoDurationSeconds::Six, true) => 240,
(EnqueueVeo3p1TextToVideoDurationSeconds::Eight, true) => 320,
}
}
}
/// Veo 3.1 Text-to-Video
/// https://fal.ai/models/fal-ai/veo3.1
pub async fn enqueue_veo_3p1_text_to_video_webhook<R: IntoUrl>(
args: EnqueueVeo3p1TextToVideoArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = args.duration
.map(|resolution| match resolution {
EnqueueVeo3p1TextToVideoDurationSeconds::Four => "4s",
EnqueueVeo3p1TextToVideoDurationSeconds::Six => "6s",
EnqueueVeo3p1TextToVideoDurationSeconds::Eight=> "8s",
})
.map(|s| s.to_string());
let aspect_ratio = args.aspect_ratio
.map(|aspect_ratio| match aspect_ratio {
EnqueueVeo3p1TextToVideoAspectRatio::Auto => "auto",
EnqueueVeo3p1TextToVideoAspectRatio::SixteenByNine => "16:9",
EnqueueVeo3p1TextToVideoAspectRatio::NineBySixteen => "9:16",
})
.map(|s| s.to_string());
let resolution = args.resolution
.map(|resolution| match resolution {
EnqueueVeo3p1TextToVideoResolution::SevenTwentyP => "720p",
EnqueueVeo3p1TextToVideoResolution::TenEightyP => "1080p",
})
.map(|s| s.to_string());
let request = Veo3p1TextToVideoInput {
prompt: args.prompt,
// Optionals
duration,
aspect_ratio,
resolution,
negative_prompt: args.negative_prompt,
generate_audio: args.generate_audio,
enhance_prompt: args.enhance_prompt,
seed: args.seed,
auto_fix: args.auto_fix,
};
let result = veo_3p1_text_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::text::enqueue_veo_3p1_text_to_video_webhook::{enqueue_veo_3p1_text_to_video_webhook, EnqueueVeo3p1TextToVideoArgs, EnqueueVeo3p1TextToVideoAspectRatio, EnqueueVeo3p1TextToVideoDurationSeconds, EnqueueVeo3p1TextToVideoResolution};
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("/home/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&secret);
let args = EnqueueVeo3p1TextToVideoArgs {
prompt: "An alien space ship hovers over new york city. it looks ominous, ready to attack. suddenly, it drops a bunch of ping pong balls on the city".to_string(),
duration: Some(EnqueueVeo3p1TextToVideoDurationSeconds::Eight),
aspect_ratio: Some(EnqueueVeo3p1TextToVideoAspectRatio::SixteenByNine),
resolution: Some(EnqueueVeo3p1TextToVideoResolution::TenEightyP),
generate_audio: Some(true),
negative_prompt: None,
enhance_prompt: Some(true),
auto_fix: Some(true),
seed: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3p1_text_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,8 @@
pub mod enqueue_hailuo_v2p3_pro_text_to_video_webhook;
pub mod enqueue_kling_v2p5_turbo_pro_text_to_video_webhook;
pub mod enqueue_kling_v2p6_pro_text_to_video_webhook;
pub mod enqueue_pixverse_v5_text_to_video_webhook;
pub mod enqueue_sora_2_pro_text_to_video_webhook;
pub mod enqueue_sora_2_text_to_video_webhook;
pub mod enqueue_veo_3p1_fast_text_to_video_webhook;
pub mod enqueue_veo_3p1_text_to_video_webhook;
+52
View File
@@ -0,0 +1,52 @@
[package]
name = "grok_client"
edition = "2024"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "grok_client"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration.workspace = true
# Internal
cloudflare_errors.workspace = true
cloudflare_mitigation.workspace = true
cookie_store.workspace = true
errors.workspace = true
# External
anyhow.workspace = true
base64.workspace = true
bstr.workspace = true
byteorder = "1.5.0"
chrono.workspace = true
futures.workspace = true
log.workspace = true
once_cell.workspace = true
regex.workspace = true
scraper = "0.24.0"
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
tokio = { version = "1", features = ["full"] }
url.workspace = true
uuid.workspace = true
wreq-util.workspace = true
wreq.workspace = true
[dev-dependencies]
filesys.workspace = true
test_data.workspace = true
testing.workspace = true
log4rs = "1.4.0"
env_logger.workspace = true
env.workspace = true
chrono.workspace = true
@@ -0,0 +1,10 @@
/// Firefox 139 (2025) on macOS, which matches wreq's Emulation::Firefox139
pub const FIREFOX_139_MAC_USER_AGENT : &str =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0";
pub const FIREFOX_143_MAC_USER_AGENT : &str =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0";
pub const FIREFOX_144_MAC_USER_AGENT_UNSUPPORTED_TLS_FINGERPRINTING : &str =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0";
@@ -0,0 +1 @@
pub mod browser_user_agents;
@@ -0,0 +1,517 @@
#!/usr/bin/env python3
# From https://github.com/kbtit25/grok2api/blob/73c917b15438e12699531ccf9411d3a7d1110cbf/xStatsigIDGenerator.py
# -*- coding: utf-8 -*-
import base64
import struct
import hashlib
import time
import secrets
import requests
import re
import json
from typing import Optional, Dict, Any
class XStatsigIDGenerator:
"""x-statsig-id 生成器"""
def __init__(self):
self.base_timestamp = int(time.time()) # 使用当前系统时间
self.grok_url = "https://grok.com"
def get_grok_meta_content(self) -> bytes:
"""
从 grok.com 获取 meta 标签中的 grok-site-verification 内容
使用多种方法彻底解决403问题
Returns:
48字节的meta内容
"""
print("🌐 正在请求 grok.com...")
# 定义多种绕过策略
strategies = [
#self._try_curl_with_proxy,
self._try_curl_with_different_ua,
#self._try_requests_with_session,
#self._try_curl_cffi_advanced,
#self._try_alternative_endpoints,
#self._try_cached_content
]
for i, strategy in enumerate(strategies):
try:
print(f" 尝试策略 {i+1}: {strategy.__name__}")
result = strategy()
if result:
return result
except Exception as e:
print(f" 策略 {i+1} 失败: {e}")
continue
# 所有策略都失败,使用备用内容
print(" ❌ 所有策略都失败,使用备用meta内容")
fallback = b"backup-grok-meta-content-when-request-fails-ok"
return fallback + b'\x00' * (48 - len(fallback))
def _try_curl_with_proxy(self) -> bytes:
"""策略1: 使用curl + 代理"""
import subprocess
# 尝试多个公共代理
proxies = [
None, # 无代理
"socks5://127.0.0.1:1080", # 本地代理
"http://127.0.0.1:8080", # 本地HTTP代理
]
for proxy in proxies:
try:
curl_command = [
'curl', '-s', '-L', '--max-time', '15',
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'--header', 'Accept-Language: en-US,en;q=0.5',
'--header', 'Accept-Encoding: gzip, deflate',
'--header', 'Connection: keep-alive',
'--header', 'Upgrade-Insecure-Requests: 1',
'--compressed'
]
if proxy:
curl_command.extend(['--proxy', proxy])
curl_command.append(self.grok_url)
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=15)
if result.returncode == 0 and len(result.stdout) > 1000:
print(f" ✅ curl成功 (代理: {proxy or '无'})")
return self._extract_meta_from_html(result.stdout)
except Exception:
continue
return None
def _try_curl_with_different_ua(self) -> bytes:
"""策略2: 使用不同的User-Agent"""
import subprocess
user_agents = [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0',
#'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
#'Mozilla/5.0 (Android 13; Mobile; rv:109.0) Gecko/109.0 Firefox/119.0',
#'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
#'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15'
]
for ua in user_agents:
try:
curl_command = [
'curl-impersonate-ff', '-s', '-L', '--max-time', '10',
'--user-agent', ua,
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'--header', 'Accept-Language: en-US,en;q=0.9',
'--header', 'Cache-Control: no-cache',
'--header', 'Pragma: no-cache',
'--compressed',
self.grok_url
]
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=10)
if result.returncode == 0 and len(result.stdout) > 1000:
print(f" ✅ 不同UA成功")
return self._extract_meta_from_html(result.stdout)
except Exception:
continue
return None
def _try_requests_with_session(self) -> bytes:
"""策略3: 使用requests session模拟真实浏览器行为"""
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
# 配置重试策略
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
# 模拟真实浏览器行为
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Cache-Control': 'max-age=0'
}
try:
# 先访问主页建立session
session.get('https://x.com', headers=headers, timeout=5)
# 再访问grok
response = session.get(self.grok_url, headers=headers, timeout=10)
if response.status_code == 200 and len(response.text) > 1000:
print(f" ✅ session请求成功")
return self._extract_meta_from_html(response.text)
except Exception:
pass
return None
def _try_curl_cffi_advanced(self) -> bytes:
"""策略4: 使用curl_cffi高级模拟"""
try:
from curl_cffi import requests as curl_requests
# 尝试不同的浏览器模拟
impersonations = ['chrome120', 'chrome119', 'safari17', 'firefox119']
for imp in impersonations:
try:
response = curl_requests.get(
self.grok_url,
impersonate=imp,
timeout=10,
headers={
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache'
}
)
if response.status_code == 200 and len(response.text) > 1000:
print(f" ✅ curl_cffi成功 ({imp})")
return self._extract_meta_from_html(response.text)
except Exception:
continue
except ImportError:
pass
return None
def _try_alternative_endpoints(self) -> bytes:
"""策略5: 尝试替代端点"""
import subprocess
# 尝试不同的URL路径
alternative_urls = [
'https://grok.com/',
'https://grok.com/login',
'https://grok.com/home',
'https://x.com/i/grok'
]
for url in alternative_urls:
try:
curl_command = [
'curl', '-s', '-L', '--max-time', '8',
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'--header', 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'--compressed',
url
]
result = subprocess.run(curl_command, capture_output=True, text=True, timeout=8)
if result.returncode == 0 and len(result.stdout) > 500:
print(f" ✅ 替代端点成功: {url}")
meta_result = self._extract_meta_from_html(result.stdout)
if meta_result:
return meta_result
except Exception:
continue
return None
def _try_cached_content(self) -> bytes:
"""策略6: 使用缓存或预设内容"""
# 如果有真实的grok meta内容,可以在这里硬编码
known_meta_contents = [
"grok-site-verification-content-2024-production-v1",
"x-grok-verification-meta-content-stable-release",
"grok-meta-verification-string-for-api-access"
]
for content in known_meta_contents:
try:
meta_bytes = content.encode('utf-8')
if len(meta_bytes) < 48:
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
elif len(meta_bytes) > 48:
meta_bytes = meta_bytes[:48]
print(f" ✅ 使用预设内容: {content[:30]}...")
return meta_bytes
except Exception:
continue
return None
def _extract_meta_from_html(self, html_content: str) -> bytes:
"""从HTML中提取meta内容"""
patterns = [
r'<meta\s+name=["\']grok-site-verification["\']\s+content=["\']([^"\']+)["\']',
r'<meta\s+content=["\']([^"\']+)["\']\s+name=["\']grok-site-verification["\']',
r'grok-site-verification["\']?\s*(?:content|value)\s*=\s*["\']([^"\']+)["\']',
# 扩展模式,查找其他可能的meta标签
r'<meta\s+name=["\']verification["\']\s+content=["\']([^"\']+)["\']',
r'<meta\s+name=["\']site-verification["\']\s+content=["\']([^"\']+)["\']'
]
for pattern in patterns:
match = re.search(pattern, html_content, re.IGNORECASE)
if match:
verification_content = match.group(1)
print(f" ✅ 找到meta内容: {verification_content[:50]}...")
# 转换为48字节
meta_bytes = verification_content.encode('utf-8')
if len(meta_bytes) < 48:
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
elif len(meta_bytes) > 48:
meta_bytes = meta_bytes[:48]
return meta_bytes
# 如果没找到特定的meta标签,尝试从HTML中提取其他有用信息
if 'grok' in html_content.lower() and len(html_content) > 1000:
# 使用HTML内容的哈希作为meta内容
import hashlib
content_hash = hashlib.sha256(html_content.encode()).hexdigest()[:48]
meta_bytes = content_hash.encode('utf-8')
if len(meta_bytes) < 48:
meta_bytes = meta_bytes + b'\x00' * (48 - len(meta_bytes))
print(f" ✅ 使用内容哈希作为meta: {content_hash[:32]}...")
return meta_bytes
return None
def generate_browser_fingerprint(self) -> str:
"""
生成浏览器指纹信息 (结合方法1和方法3)
Returns:
指纹字符串
"""
print("🔍 生成浏览器指纹...")
# 模拟浏览器指纹信息
fingerprint_data = {
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
"language": "en",
"languages": ["en", "en-US"],
"platform": "MacIntel",
"cookieEnabled": True,
"doNotTrack": None,
"screenWidth": 450,
"screenHeight": 654,
"screenColorDepth": 24,
"screenPixelDepth": 24,
"screenAvailWidth": 450,
"screenAvailHeight": 654,
"innerWidth": 450,
"innerHeight": 654,
"outerWidth": 1920,
"outerHeight": 1055,
"timezone": "America/New York",
"timezoneOffset": -400,
"hardwareConcurrency": 14,
"deviceMemory": 16,
"maxTouchPoints": 0
}
# 方法3: 生成指纹哈希
fingerprint_string = json.dumps(fingerprint_data, sort_keys=True, separators=(',', ':'))
fingerprint_hash = hashlib.sha256(fingerprint_string.encode('utf-8')).hexdigest()
print(f" 指纹数据长度: {len(fingerprint_string)} 字符")
print(f" 指纹哈希: {fingerprint_hash[:32]}...")
return fingerprint_hash
def generate_x_statsig_id(self, method: str = "GET", pathname: str = "/") -> str:
"""
生成完整的 x-statsig-id
Args:
method: 请求方式 (GET/POST)
pathname: 请求路径
Returns:
生成的 x-statsig-id 字符串
"""
print("=" * 60)
print("🚀 开始生成 x-statsig-id")
print("=" * 60)
print(f"📋 生成参数:")
print(f" Method: {method}")
print(f" Pathname: {pathname}")
# 1. 获取 grok.com 的 meta content
meta_content = self.get_grok_meta_content()
# 2. 生成浏览器指纹
fingerprint = self.generate_browser_fingerprint()
# 3. 生成当前时间戳
current_timestamp = int(time.time())
relative_timestamp = current_timestamp - self.base_timestamp
print(f"⏰ 时间信息:")
print(f" 当前时间戳: {current_timestamp}")
print(f" 基准时间戳: {self.base_timestamp}")
print(f" 相对时间戳: {relative_timestamp}")
# 4. 生成时间戳字节 (小端序)
timestamp_bytes = struct.pack('<I', relative_timestamp)
print(f" 时间戳字节: {timestamp_bytes.hex()}")
# 5. 生成SHA256
sha_input = f"{method}!{pathname}!{relative_timestamp}{fingerprint}"
sha256_hash = hashlib.sha256(sha_input.encode('utf-8')).digest()
sha256_16bytes = sha256_hash[:16]
print(f"🔐 SHA256信息:")
print(f" 输入字符串: {sha_input[:100]}...")
print(f" SHA256前16字节: {sha256_16bytes.hex()}")
# 6. 固定值
fixed_byte = b'\x03'
# 7. 组合payload数据
payload_data = meta_content + timestamp_bytes + sha256_16bytes + fixed_byte
print(f"📦 Payload长度: {len(payload_data)} 字节")
# 8. 生成异或key并加密
xor_key = secrets.randbits(8)
encrypted_payload = bytes([b ^ xor_key for b in payload_data])
print(f"🔑 异或信息:")
print(f" 异或key: 0x{xor_key:02x} ({xor_key})")
# 9. 组合最终数据
final_data = bytes([xor_key]) + encrypted_payload
print(f" 最终数据长度: {len(final_data)} 字节")
# 10. Base64编码
result = base64.b64encode(final_data).decode('utf-8')
print(f"✅ 生成结果:")
print(f" x-statsig-id: {result}")
print(f" 长度: {len(result)} 字符")
return result
def verify_generated_id(self, statsig_id: str) -> bool:
"""
验证生成的ID结构是否正确
Args:
statsig_id: 要验证的ID
Returns:
验证是否通过
"""
print("\n" + "=" * 60)
print("🔍 验证生成的ID结构")
print("=" * 60)
try:
# Base64解码
decoded_bytes = base64.b64decode(statsig_id)
print(f"✅ Base64解码成功,长度: {len(decoded_bytes)} 字节")
# 提取异或key
xor_key = decoded_bytes[0]
print(f"✅ 异或key: 0x{xor_key:02x} ({xor_key})")
# 异或解密
decrypted = bytearray()
for i in range(1, len(decoded_bytes)):
decrypted.append(decoded_bytes[i] ^ xor_key)
print(f"✅ 解密后长度: {len(decrypted)} 字节")
# 验证数据结构
expected_length = 48 + 4 + 16 + 1 # meta + timestamp + sha256 + fixed
if len(decrypted) == expected_length:
print(f"✅ 数据长度正确: {len(decrypted)}/{expected_length}")
else:
print(f"❌ 数据长度错误: {len(decrypted)}/{expected_length}")
return False
# 检查固定值
fixed_val = decrypted[-1]
if fixed_val == 3:
print(f"✅ 固定值正确: {fixed_val}")
else:
print(f"❌ 固定值错误: {fixed_val} (期望: 3)")
return False
# 解析时间戳
timestamp_bytes = decrypted[48:52]
timestamp = struct.unpack('<I', timestamp_bytes)[0]
actual_time = self.base_timestamp + timestamp
print(f"✅ 时间戳解析:")
print(f" 相对时间: {timestamp} 秒")
print(f" 绝对时间: {actual_time}")
print(f" 时间差: {abs(time.time() - actual_time):.1f} 秒")
print("🎉 ID结构验证通过!")
return True
except Exception as e:
print(f"❌ 验证失败: {e}")
return False
def main():
"""主函数 - 演示完整流程"""
generator = XStatsigIDGenerator()
# 生成 x-statsig-id
method = "POST"
pathname = "/rest/app-chat/conversations/new"
statsig_id = generator.generate_x_statsig_id(method, pathname)
print("Generated id", statsig_id)
# 验证生成的ID
generator.verify_generated_id(statsig_id)
print(f"\n" + "=" * 60)
print("📋 使用说明")
print("=" * 60)
print("在HTTP请求中使用:")
print(f"Headers: {{'x-statsig-id': '{statsig_id}'}}")
if __name__ == "__main__":
main()
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# From https://github.com/realasfngl/Grok-Api/blob/d8f7c6754d95ad3707b214f93930ee454724035d/core/xctid.py
from math import floor, copysign, pi, cos, sin
from base64 import b64decode, b64encode
from re import findall, sub
from typing import List, Dict
from random import random
from hashlib import sha256
from struct import pack
from time import time
class Signature:
@staticmethod
def _h(x: float, _param: float, c: float, e: bool):
f = ((x * (c - _param)) / 255.0) + _param
if e:
return floor(f)
rounded = round(float(f), 2)
if rounded == 0.0:
return 0.0
return rounded
@staticmethod
def cubicBezierEased(t: float, x1: float, y1: float, x2: float, y2: float) -> float:
def bezier(u: float):
omu = 1.0 - u
b1 = 3.0 * omu * omu * u
b2 = 3.0 * omu * u * u
b3 = u * u * u
x = b1 * x1 + b2 * x2 + b3
y = b1 * y1 + b2 * y2 + b3
return x, y
lo, hi = 0.0, 1.0
for _ in range(80):
mid = 0.5 * (lo + hi)
if bezier(mid)[0] < t:
lo = mid
else:
hi = mid
u = 0.5 * (lo + hi)
return bezier(u)[1]
@staticmethod
def xa(svg: str) -> List[List[int]]:
s = (svg)
substr = s[9:]
parts = substr.split("C")
out = []
for part in parts:
cleaned = sub(r"[^\d]+", " ", part).strip()
if cleaned == "":
nums = [0]
else:
nums = [int(tok) for tok in cleaned.split() if tok != ""]
out.append(nums)
return out
@staticmethod
def tohex(num: float) -> str:
rounded = round(float(num), 2)
if rounded == 0.0:
return "0"
sign = "-" if copysign(1.0, rounded) < 0 else ""
absval = abs(rounded)
intpart = int(floor(absval))
frac = absval - intpart
if frac == 0.0:
return sign + format(intpart, "x")
frac_digits = []
f = frac
for _ in range(20):
f *= 16
digit = int(floor(f + 1e-12))
frac_digits.append(format(digit, "x"))
f -= digit
if abs(f) < 1e-12:
break
frac_str = "".join(frac_digits).rstrip("0")
if frac_str == "":
return sign + format(intpart, "x")
return sign + format(intpart, "x") + "." + frac_str
@staticmethod
def simulateStyle(values: List[int], c: int) -> Dict[str,str]:
duration = 4096
currentTime = round(c / 10.0) * 10
t = currentTime / duration
cp = [Signature._h(v, -1 if (i % 2) else 0, 1, False) for i, v in enumerate(values[7:])]
easedY = Signature.cubicBezierEased(t, cp[0], cp[1], cp[2], cp[3])
start = [float(x) for x in values[0:3]]
end = [float(x) for x in values[3:6]]
r = round(start[0] + (end[0] - start[0]) * easedY)
g = round(start[1] + (end[1] - start[1]) * easedY)
b = round(start[2] + (end[2] - start[2]) * easedY)
color = f"rgb({r}, {g}, {b})"
endAngle = Signature._h(values[6], 60, 360, True)
angle = endAngle * easedY
rad = angle * pi / 180.0
def is_effectively_zero(val: float) -> bool:
return abs(val) < 1e-7
def is_effectively_integer(val: float) -> bool:
return abs(val - round(val)) < 1e-7
cosv = cos(rad)
sinv = sin(rad)
if is_effectively_zero(cosv):
a = 0
d = 0
else:
if is_effectively_integer(cosv):
a = int(round(cosv))
d = int(round(cosv))
else:
a = f"{cosv:.6f}"
d = f"{cosv:.6f}"
if is_effectively_zero(sinv):
bval = 0
cval = 0
else:
if is_effectively_integer(sinv):
bval = int(round(sinv))
cval = int(round(-sinv))
else:
bval = f"{sinv:.7f}"
cval = f"{(-sinv):.7f}"
transform = f"matrix({a}, {bval}, {cval}, {d}, 0, 0)"
return {"color": color, "transform": transform}
@staticmethod
def xs(x_bytes: bytes, svg: str, x_values: list) -> str:
arr = list(x_bytes)
idx = arr[x_values[0]] % 16
c = ((arr[x_values[1]] % 16) * (arr[x_values[2]] % 16)) * (arr[x_values[3]] % 16)
o = Signature.xa(svg)
vals = o[idx]
k = Signature.simulateStyle(vals, c)
concat = str(k["color"]) + str(k["transform"])
matches = findall(r"[\d\.\-]+", concat)
converted = []
for m in matches:
num = float(m)
hexstr = Signature.tohex(num)
converted.append(hexstr)
joined = "".join(converted)
cleaned = joined.replace(".", "").replace("-", "")
return cleaned
@staticmethod
def generate_sign(path: str, method: str, verification: str, svg: str, x_values: list, time_n: int = None, random_float: float = None) -> str:
n = int(time() - 1682924400) if not time_n else time_n
t = pack('<I', n)
r = b64decode(verification)
o = Signature.xs(r, svg, x_values)
msg = "!".join([method, path, str(n)]) + "obfiowerehiring" + o
digest = sha256(msg.encode('utf-8')).digest()[:16]
prefix_byte = int(floor(random() if not random_float else random_float * 256))
assembled = bytes([prefix_byte]) + r + t + digest + bytes([3])
arr = bytearray(assembled)
if len(arr) > 0:
first = arr[0]
for i in range(1, len(arr)):
arr[i] = arr[i] ^ first
return b64encode(bytes(arr)).decode('ascii').replace('=', '')
def main():
sig = Signature.generate_sign('/rest/app-chat/conversations/new', 'POST', self.verification_token, self.svg_data, self.numbers)
print('Signature', sig)
pass
if __name__ == 'main':
main()
@@ -0,0 +1,8 @@
https://grok.com/ --> https://accounts.x.ai/sign-in?redirect=grok-com
--> Success --> https://grok.com/
https://grok.com/imagine
@@ -0,0 +1,36 @@
use crate::datatypes::api::baggage::Baggage;
use crate::datatypes::api::sentry_trace::SentryTrace;
use crate::datatypes::api::svg_path_data::SvgPathData;
use crate::datatypes::api::user_email::UserEmail;
use crate::datatypes::api::user_id::UserId;
use crate::datatypes::api::verification_token::VerificationToken;
use crate::datatypes::api::xsid_numbers::XsidNumbers;
#[derive(Clone)]
pub struct GrokClientSecrets {
/// Baggage is <meta> tag data from the index.html
pub baggage: Baggage,
/// Baggage is <meta> tag data from the index.html
pub sentry_trace: SentryTrace,
/// Baggage is <meta> tag data from the index.html
pub verification_token: VerificationToken,
/// The SVG path data to use (chosen via verification_token)
/// We selected one of four-ish possible SVG paths (of length >= 200) by
/// the `verification token -> animation index` algo.
pub svg_path_data: SvgPathData,
/// This doesn't come from the index.html
/// These numbers are separately loaded from the xsid javascript
pub numbers: XsidNumbers,
/// From index.html
/// Not strictly needed to sign requests, but typically needed to generate URLs.
pub user_id: UserId,
/// From index.html
/// Not needed, but returned alongside other details.
pub user_email: Option<UserEmail>,
}
@@ -0,0 +1,24 @@
#[derive(Clone)]
pub struct GrokCookies {
/// The entire cookies header (for now)
cookies: String,
}
impl GrokCookies {
pub fn new(cookies: String) -> Self {
Self { cookies }
}
pub fn as_str(&self) -> &str {
&self.cookies
}
pub fn as_bytes(&self) -> &[u8] {
self.cookies.as_bytes()
}
pub fn to_string(&self) -> String {
self.cookies.clone()
}
}

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