Compare commits

..
Author SHA1 Message Date
Brandon Thomas 0c359d55d3 prevent proration 2025-09-17 11:41:51 -04:00
Brandon Thomas 8dba5d1d46 far better management flow 2025-09-17 00:38:58 -04:00
Brandon Thomas e1d83714fc rename modules 2025-09-16 23:38:13 -04:00
Brandon Thomas d4ec659568 cleanup 2025-09-16 22:56:38 -04:00
Brandon Thomas 780ee74b95 user->customer link on checkout and portal 2025-09-16 22:30:13 -04:00
Brandon Thomas 8e7cd5573e credit pack customer reuse 2025-09-16 22:25:26 -04:00
Brandon Thomas aa0524f48c save customer <-> user 2025-09-16 22:06:11 -04:00
Brandon Thomas d6b55ae5ac new table for storing customer links 2025-09-16 20:01:42 -04:00
Brandon Thomas f9e52ecebf Customer creation on checkout 2025-09-16 13:51:53 -04:00
Brandon Thomas dd656e2b61 fix bug, comment reason 2025-09-16 13:23:52 -04:00
Brandon Thomas a7a89bbd55 on second thought, don't resurrect dead subscriptions 2025-09-16 12:45:08 -04:00
Brandon Thomas bcb74b377a pull existing inactive subscription, but no fakeyou subs 2025-09-16 12:23:00 -04:00
Brandon Thomas e0400b651e credits checkout from existing subscriber record 2025-09-16 11:53:40 -04:00
Brandon Thomas 5cb06357fa add delta to ledger entries 2025-09-16 10:19:17 -04:00
Brandon Thomas 5bdb59bcce change button labels 2025-09-16 09:30:31 -04:00
Brandon Thomas 92a0674776 end and bill dates 2025-09-16 09:11:13 -04:00
Brandon Thomas 0f5ec3c83b next payment 2025-09-16 08:46:51 -04:00
Brandon Thomas 9835a238ce fix pricing modal 2025-09-16 07:34:52 -04:00
Brandon Thomas 68e77fd7d3 update payment method 2025-09-16 07:12:27 -04:00
Brandon Thomas 01053b7ccc billing portal subscription change works now 2025-09-16 06:19:07 -04:00
Brandon Thomas dec325c190 wip 2025-09-16 06:11:16 -04:00
Brandon Thomas 6c5d956bf8 tauri handles distinct customer portal cases 2025-09-16 05:44:36 -04:00
Brandon Thomas 3eeae4651c split billing portal into three distinct endpoints 2025-09-16 05:00:29 -04:00
Brandon Thomas 191b0c1463 subscription cancel within modal 2025-09-16 04:17:12 -04:00
Brandon Thomas 0b50b2d637 wip reuse of subscription modal 2025-09-16 04:10:43 -04:00
Brandon Thomas 3a9b0c100e fix 2025-09-16 04:04:18 -04:00
Brandon Thomas b2ecd4dcd1 de-duplicate plan data 2025-09-16 04:02:07 -04:00
Brandon Thomas 62c2ab0581 refresh on subscription changed 2025-09-16 03:49:09 -04:00
Brandon Thomas f90ab5771a change plan 2025-09-16 03:10:55 -04:00
Brandon Thomas 6a0abfc177 de-dupe query 2025-09-14 22:45:30 -04:00
Brandon Thomas 681fa8e76e update event 2025-09-14 20:57:35 -04:00
Brandon Thomas 9e80e0c79d hide subscribe button in settings when paid 2025-09-14 20:50:21 -04:00
Brandon Thomas ab62bcea11 only show upgrade if there's no paid plan 2025-09-14 20:45:46 -04:00
Brandon Thomas ffad39e6b7 add subscription info 2025-09-14 20:26:22 -04:00
Brandon Thomas 9b637aa6c9 fetch current plan 2025-09-14 19:26:17 -04:00
Brandon Thomas cb41b1a0af move credits, add subscriptions 2025-09-14 18:06:49 -04:00
Brandon Thomas e680bef024 notify frontend of credits changes 2025-09-14 14:40:01 -04:00
Brandon Thomas f0f2ae00fe listen to credits change event 2025-09-13 16:27:33 -04:00
Brandon Thomas 5072e38824 test billing code in server 2025-09-12 19:22:28 -04:00
Brandon Thomas ec8623e200 remove duplicate calls 2025-09-12 18:15:26 -04:00
Brandon Thomas 15c52d4e9b fix api call 2025-09-12 18:12:32 -04:00
Brandon Thomas de58d7c033 fix api call and display credits in frontend 2025-09-12 18:09:01 -04:00
Brandon Thomas 64b91cd6bc add frontend billing package, Tauri "get credits command" 2025-09-12 17:17:43 -04:00
Brandon Thomas 5360dd0f58 get rid of login button - it gets stuck in a bad state 2025-09-12 01:09:46 -04:00
Brandon Thomas f8b53872ae clients 2025-09-12 01:00:05 -04:00
Brandon Thomas 2e024efc32 new subscriptions endpoint 2025-09-12 01:00:05 -04:00
Brandon Thomas 4bb3f6af2d wallets -> credits routes instead 2025-09-12 01:00:05 -04:00
Brandon Thomas 2de4d9d9e9 move crate, sort workspace cargo.toml 2025-09-12 01:00:05 -04:00
Brandon Thomas 80d28022c0 get wallet endpoint 2025-09-12 01:00:05 -04:00
Brandon Thomas d770d81508 wallets namespaces, shorthand for query_map 2025-09-12 01:00:05 -04:00
Brandon Thomas 1fa93b5327 more metadata 2025-09-12 01:00:05 -04:00
Brandon Thomas eebfdd3d00 subscription flow 2025-09-12 01:00:05 -04:00
Brandon Thomas 90032848ee create billing portal session 2025-09-12 01:00:05 -04:00
Brandon Thomas 07722766dd log invoice, etc. tokens 2025-09-12 01:00:04 -04:00
Brandon Thomas f272dbf401 ledger entries wip 2025-09-12 01:00:04 -04:00
Brandon Thomas 6177782946 insert entries 2025-09-12 01:00:04 -04:00
Brandon Thomas ad9bda9e22 token and entry type for ledgers 2025-09-12 01:00:04 -04:00
Brandon Thomas 41e3a384db stupid irrelevant side quest for PR - safe crockford tokens 2025-09-12 01:00:04 -04:00
Brandon Thomas 168a1ace69 change prefix for subscriptions, remove fields 2025-09-12 01:00:04 -04:00
Brandon Thomas 7e32dde5b3 update schemas 2025-09-12 01:00:04 -04:00
Brandon Thomas 9de3ccac79 fill monthly wallet 2025-09-12 01:00:04 -04:00
Brandon Thomas 3f0eccf27e subscription invoices work 2025-09-12 01:00:04 -04:00
Brandon Thomas 9bbcbca1e2 better logging 2025-09-12 01:00:04 -04:00
Brandon Thomas f5696912c2 wip 2025-09-12 01:00:04 -04:00
Brandon Thomas 68559561c1 refactor 2025-09-12 01:00:04 -04:00
Brandon Thomas 75bc2a98f8 move 2025-09-12 01:00:04 -04:00
Brandon Thomas 294ee0d006 subscription paid wip 2025-09-12 01:00:04 -04:00
Brandon Thomas b9de0bf456 billing anchor 2025-09-12 01:00:04 -04:00
Brandon Thomas 437317438e new migrations 2025-09-12 01:00:04 -04:00
Brandon Thomas 8248e53ded refactor subscriptions handlers 2025-09-12 01:00:04 -04:00
Brandon Thomas 72631c94be refactor subs creation 2025-09-12 01:00:04 -04:00
Brandon Thomas e413cc94ad fix logic bug 2025-09-12 01:00:04 -04:00
Brandon Thomas 06023ae11e refactor 2025-09-12 01:00:04 -04:00
Brandon Thomas 8712b291c2 refactor 2025-09-12 01:00:04 -04:00
Brandon Thomas 6c678ec7f3 refactor 2025-09-12 01:00:04 -04:00
Brandon Thomas cba6f25a17 refactor 2025-09-12 01:00:04 -04:00
Brandon Thomas d10cb54c62 refactor 2025-09-12 01:00:04 -04:00
Brandon Thomas b5bc532b80 refactor 2025-09-12 01:00:04 -04:00
Brandon Thomas da2e7ae4b6 big refactor of this to remove http during db transactions 2025-09-12 01:00:04 -04:00
Brandon Thomas 036dbeaeb4 wallet updates work! 2025-09-12 01:00:04 -04:00
Brandon Thomas f6ed5abdb7 wip 2025-09-12 01:00:04 -04:00
Brandon Thomas 70bed32f51 query for wallet token 2025-09-12 01:00:04 -04:00
Brandon Thomas 497dbd9b4f wip 2025-09-12 01:00:04 -04:00
Brandon Thomas 0ad8421a16 subs transactions fixed 2025-09-12 01:00:04 -04:00
Brandon Thomas aef0c2f76a compile errors 2025-09-12 01:00:03 -04:00
Brandon Thomas e5f1cd00ef transactional 2025-09-12 01:00:03 -04:00
Brandon Thomas 5f3399db62 cleanup 2025-09-12 01:00:03 -04:00
Brandon Thomas 2467d6f2c7 wip 2025-09-12 01:00:03 -04:00
Brandon Thomas e48ee589ad retry behavior 2025-09-12 01:00:03 -04:00
Brandon Thomas b3e74e25f6 close billing window when done 2025-09-12 01:00:03 -04:00
Brandon Thomas ff5ebac734 info on invoices 2025-09-12 01:00:03 -04:00
Brandon Thomas b15e50e502 lookup product info from payment intents 2025-09-12 01:00:03 -04:00
Brandon Thomas 687e33df96 database migrations 2025-09-12 01:00:03 -04:00
Brandon Thomas d08753234c notes 2025-09-12 01:00:03 -04:00
Brandon Thomas bd2e091238 note 2025-09-12 01:00:03 -04:00
Brandon Thomas 67d293804b fix credits purchase 2025-09-12 01:00:03 -04:00
Brandon Thomas 1c1a75ef6b open credits window 2025-09-12 01:00:03 -04:00
Brandon Thomas 562c9a18ef attempt to move commands into separate function failed 2025-09-12 01:00:03 -04:00
Brandon Thomas fe3388a648 rewire commands 2025-09-12 01:00:03 -04:00
Brandon Thomas c8651fa2bc rework handlers to add credits 2025-09-12 01:00:03 -04:00
Brandon Thomas 65f36704cf wip 2025-09-12 01:00:03 -04:00
Brandon Thomas 74f7422e32 notes 2025-09-12 01:00:03 -04:00
Brandon Thomas 92b3245533 fixes 2025-09-12 01:00:03 -04:00
Brandon Thomas 64fbb0b04e subscription events seem okay-ish 2025-09-12 01:00:03 -04:00
Brandon Thomas 72228345d0 wip 2025-09-12 01:00:03 -04:00
Brandon Thomas b12c81dc37 cleanup 2025-09-12 01:00:03 -04:00
Brandon Thomas 80f0235c50 wip 2025-09-12 01:00:03 -04:00
Brandon Thomas 1e4fca06ee filter existing artcraft subscriptions, allow promo codes 2025-09-12 01:00:03 -04:00
Brandon Thomas 50e228aa3f enum type 2025-09-12 01:00:03 -04:00
Brandon Thomas 8fedbfcded remove old handler 2025-09-12 01:00:03 -04:00
Brandon Thomas 6696ef3366 user lookup in component 2025-09-12 01:00:03 -04:00
Brandon Thomas 837f3ecdf3 wip 2025-09-12 01:00:03 -04:00
Brandon Thomas 2b7cc30bbe omg, this yak shave worked. 2025-09-12 01:00:02 -04:00
Brandon Thomas 35554c6dde compiles with endpoint logic 2025-09-12 01:00:02 -04:00
Brandon Thomas fe46c72d06 this builds 2025-09-12 01:00:02 -04:00
Brandon Thomas cec81bfb8e about to test new crate to isolate new version of async-stripe 2025-09-12 01:00:02 -04:00
Brandon Thomas 3c89cf595e organize products by slug + production products 2025-09-12 01:00:02 -04:00
Brandon Thomas 24b8c3e69a enums 2025-09-12 01:00:02 -04:00
Brandon Thomas 474904ddd7 open pricing window to stripe 2025-09-12 01:00:02 -04:00
Brandon Thomas 859897bedb cleanup endpoint 2025-09-12 01:00:02 -04:00
Brandon Thomas 1e65125c07 checkout session handler 2025-09-12 01:00:02 -04:00
Brandon Thomas 8ae8d77187 stripe webhook handler for artcraft 2025-09-12 01:00:02 -04:00
BFlat 0e7ebd3914 updated landing page videos (#822)
- changed videos for 3d and 2d canvas
- added a video for inpainting (copy text needs to be updated)
2025-09-11 14:51:22 +07:00
bflatastic d508bf55e2 nx workspace sync 2025-09-11 14:21:15 +07:00
Tanish Manku cac0c0b188 Auto-switch to edit mode if model supports it (#818) 2025-09-10 13:50:02 +07:00
bflatastic f837f79404 fixed title disappearing on generate 2025-09-10 13:47:34 +07:00
BFlat b632d79833 image/video promptbox generation bug fixes (#821)
- make top image prompt row always show for video
- video promptbox pops a toast when reference image is empty
- description changes for ending frame section
- hid the add image button for video generation for now since that's
almost always required
2025-09-10 13:45:00 +07:00
Tanish Mankuandbflatastic d909d55e6f Support history item deletion (#819)
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2025-09-10 13:23:05 +07:00
Tanish Manku 1847533e8a Add an image removal button in history stack 2025-09-09 12:44:17 -07:00
BFlat 95894a87b9 video page update (#817)
- video prompt box now supports image prompt (starting and ending frame)
- shared image prompts section between image / video generation prompt
boxes
- video history / pending generation like text to image
- background change

- the history stack is commented out for now because backend doesnt
support it yet.
2025-09-09 19:03:26 +07:00
Tanish Manku 3ad642f150 Make mouse dragging pixel-perfect (#816) 2025-09-04 14:38:19 -07:00
bflatastic 3fb683185a console errors and log spams fixed 2025-09-04 22:32:36 +07:00
BFlat 36780e5669 ux update inpainting history (#815)
- reverted to previous ux
- added placeholder loaders for generating images (matched by frontend
subscriber id)
- can now generate multiple images at a time
- auto scrolls to top of history stack on enqueue
2025-09-03 23:05:05 +07:00
BFlat 77614e06d9 credits UI refill (#814) 2025-09-03 21:49:36 +07:00
Brandon Thomas 4c1d1007a2 icon change, metadata change (#813) 2025-08-31 13:13:58 -04:00
bflatastic 9740121012 history items to only show after enqueue succeeds 2025-08-31 11:07:48 +07:00
BFlat 27d6794d22 credits ui - topbar and settings modal (#804)
- new progress bar and progress circle component
- clicking the credits in the top opens the settings modal on the "Plans
& Credits" tab
2025-08-31 11:06:29 +07:00
Brandon Thomas 4aa91af67e trigger payments events in tauri/artcraft from the server (#805) 2025-08-30 20:34:04 -04:00
Brandon Thomas ddc33ce600 prevent loading gigabytes of images when opening the gallery 2025-08-28 23:41:33 -04:00
BFlatandBrandon Thomas c017fbc25a new text to image prompt box image prompts (#800)
- image prompts linked to models (maximum and if it's allowed or not)
- lots of styling updates
- promptbox doesn't overflow bottom anymore (dynamic height detection)
- selected image prompts can be clicked to be previewed in a modal
<img width="938" height="472" alt="image"
src="https://github.com/user-attachments/assets/e4fc7445-0031-4adf-a6f6-dbcbe1c3ea67"
/>
- @echelon on handleEnqueue - image prompts need to be added

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2025-08-28 23:10:16 -04:00
Brandon Thomas 7557e586ba fix broken transaction 2025-08-28 22:05:45 -04:00
Brandon Thomas 2286d663ac more fal models (#802) 2025-08-28 21:18:26 -04:00
Tanish Manku 4181e24f7f Fix history stack being empty coming from gallery edit button (#799) 2025-08-28 22:59:07 +07:00
BFlat d4911f226b new top menu gifs (#801) 2025-08-28 22:58:13 +07:00
bflatastic 93db726c19 bug fix: inpainting clear button to reset generating state too so it doesn't get stuck 2025-08-28 18:29:41 +07:00
bflatastic dac64fb38c remove unused animated line css 2025-08-28 14:14:51 +07:00
bflatastic 60009f98ac generating animated canvas border, history stack sticky icon top, lock further generation (will need to persist states for page changes) 2025-08-28 14:12:24 +07:00
Tanish Manku 92b68d3f9d Make the inpainting stroke translucent blue (#797) 2025-08-27 15:54:06 -07:00
Tanish Manku c39651415d Fix lossy History Stack (#796) 2025-08-27 14:43:07 -07:00
Brandon Thomas 7dae90f1e2 start work on consolidating internal errors in http server 2025-08-27 17:41:43 -04:00
Brandon Thomas 30634e8be9 fix the model list, wrong tauri ids, new feature predicate (#798) 2025-08-27 17:17:13 -04:00
Brandon Thomas 801886d7a8 adjust enums, disable kontext on text-to-image (for now) (#795) 2025-08-27 04:11:14 -04:00
Brandon Thomas 16bb5d0771 cleanup enum 2025-08-27 03:33:07 -04:00
BFlat 9b1b7f1ad3 styling fixes for history stack (#794)
- clear all tooltip
- padding, gap, ordering fixes
2025-08-27 14:32:03 +07:00
Brandon Thomas 67b66411d7 support flux pro kontext as an "inpainting" model 2025-08-27 03:24:46 -04:00
Brandon Thomas 9fda7657d2 swap order of history stack 2025-08-27 01:49:04 -04:00
Brandon Thomas e3cb9456d0 reorder the tabs 2025-08-27 01:26:29 -04:00
Brandon Thomas 13af7b0664 fix environmental cdn issues with dev (#793) 2025-08-27 01:09:42 -04:00
Brandon Thomas 8250162d8f cleanup model metadata for prompt box (#792) 2025-08-27 00:34:08 -04:00
Brandon Thomas 4852abae72 more typesafe classes for models 2025-08-26 21:03:00 -04:00
Brandon Thomas aa03618d6c video end frame support (#791) 2025-08-26 19:13:15 -04:00
Brandon Thomas 6199f1f5a7 bypass sec-fetch-mode: no-cors for 3d (#787) 2025-08-26 00:19:10 -04:00
Brandon Thomas 27ae474797 more fal models (fal API bindings only) (#786) 2025-08-25 23:20:51 -04:00
Tanish Manku 9d3d3aa98c Merge the delete buttons to history stack (#785) 2025-08-25 16:24:29 -07:00
587 changed files with 19940 additions and 2937 deletions
Generated
+258 -3
View File
@@ -926,11 +926,155 @@ dependencies = [
"serde_qs 0.10.1",
"sha2 0.10.8",
"smart-default 0.6.0",
"smol_str",
"smol_str 0.1.24",
"thiserror 1.0.63",
"tokio",
]
[[package]]
name = "async-stripe"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecbddf002ad7a13d2041eadf1b234cb3f57653ffdd901a01bc3f1c65aa77440"
dependencies = [
"chrono",
"futures-util",
"hex",
"hmac 0.12.1",
"http-types",
"hyper 0.14.28",
"hyper-tls 0.5.0",
"serde",
"serde_json",
"serde_path_to_error",
"serde_qs 0.10.1",
"sha2 0.10.8",
"smart-default 0.6.0",
"smol_str 0.1.24",
"thiserror 1.0.63",
"tokio",
]
[[package]]
name = "async-stripe"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92140fa7152f9dcde7554cb1da37c7ae93b0c724db5214c754c55a9765afbe8"
dependencies = [
"async-stripe-client-core",
"async-stripe-shared",
"bytes 1.10.1",
"http-body-util",
"hyper 1.6.0",
"hyper-tls 0.6.0",
"hyper-util",
"miniserde",
"thiserror 2.0.12",
"tokio",
]
[[package]]
name = "async-stripe-billing"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c4cd0ac61e0dcb39da4f3c04b049c0d8a3a98868ca976e18ff56ecac1b6e08"
dependencies = [
"async-stripe-client-core",
"async-stripe-shared",
"async-stripe-types",
"miniserde",
"serde",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-checkout"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b115740c799730d4caac099d48be4908d2d99611d56a874d2d3555b5d4306a97"
dependencies = [
"async-stripe-client-core",
"async-stripe-shared",
"async-stripe-types",
"miniserde",
"serde",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-client-core"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb1f6b6693946e650a9361feb319389a1408534ec1b618cd9239f1e2e20b8668"
dependencies = [
"async-stripe-shared",
"async-stripe-types",
"bytes 1.10.1",
"futures-util",
"miniserde",
"serde",
"serde_json",
"serde_qs 0.13.0",
"thiserror 2.0.12",
"tracing",
]
[[package]]
name = "async-stripe-core"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81d0445a38170bcc3e4adf52fd80c94b70b02dc73a102afcd0bc9d6051eada18"
dependencies = [
"async-stripe-client-core",
"async-stripe-shared",
"async-stripe-types",
"miniserde",
"serde",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-shared"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b4328e5c0deeacd7cd5a95286d4f5dce045791b30be87583dd706c2342236c3"
dependencies = [
"async-stripe-types",
"miniserde",
"serde",
"serde_json",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-types"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985b4f00fd57ce9cbd015c6410d05dfbe5d2bf90513e20b417e70b6f022e3f88"
dependencies = [
"miniserde",
"serde",
"serde_json",
"smol_str 0.3.2",
]
[[package]]
name = "async-stripe-webhook"
version = "1.0.0-alpha.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f8382af92463fa8e0b86e825d7d08eecbcd8715c197f1a66173fcc18e35774"
dependencies = [
"async-stripe-shared",
"async-stripe-types",
"chrono",
"hex",
"hmac 0.12.1",
"miniserde",
"serde",
"sha2 0.10.8",
"thiserror 1.0.63",
]
[[package]]
name = "async-task"
version = "4.7.1"
@@ -1201,6 +1345,41 @@ dependencies = [
"num-traits",
]
[[package]]
name = "billing_artcraft_component"
version = "0.0.1"
dependencies = [
"actix-http",
"actix-web",
"anyhow",
"artcraft_api_defs",
"async-stripe 1.0.0-alpha.2",
"async-stripe-billing",
"async-stripe-checkout",
"async-stripe-core",
"async-stripe-shared",
"async-stripe-types",
"async-stripe-webhook",
"chrono",
"component_traits",
"enums",
"errors",
"http_server_common",
"log",
"mockall",
"mysql_queries",
"once_cell",
"reusable_types",
"serde",
"serde_derive",
"serde_json",
"sqlx",
"tokens",
"tokio",
"user_traits_component",
"workspace-build-acceleration",
]
[[package]]
name = "billing_component"
version = "0.0.1"
@@ -1208,9 +1387,11 @@ dependencies = [
"actix-http",
"actix-web",
"anyhow",
"async-stripe",
"async-stripe 0.26.0",
"async-trait",
"chrono",
"component_traits",
"enums",
"errors",
"http_server_common",
"log",
@@ -2089,6 +2270,34 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "component_traits"
version = "0.0.1"
dependencies = [
"actix-http",
"actix-web",
"anyhow",
"artcraft_api_defs",
"async-trait",
"chrono",
"enums",
"errors",
"http_server_common",
"log",
"mockall",
"mysql_queries",
"once_cell",
"reusable_types",
"serde",
"serde_derive",
"serde_json",
"sqlx",
"tokens",
"tokio",
"user_traits_component",
"workspace-build-acceleration",
]
[[package]]
name = "composite_identifiers"
version = "0.0.1"
@@ -6398,6 +6607,17 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "mini-internal"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c74ab4f1a0c0ab045260ee4727b23c00cc17e5eff5095262d08eef8c3c8d49"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "minidom"
version = "0.16.0"
@@ -6413,6 +6633,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniserde"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08ec68bf2ad170a53a6efa92c3df7187968d6e475fe7a935725868154074ca0f"
dependencies = [
"itoa 1.0.11",
"mini-internal",
"ryu",
]
[[package]]
name = "miniz_oxide"
version = "0.7.2"
@@ -9528,6 +9759,17 @@ dependencies = [
"thiserror 1.0.63",
]
[[package]]
name = "serde_qs"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6"
dependencies = [
"percent-encoding",
"serde",
"thiserror 1.0.63",
]
[[package]]
name = "serde_repr"
version = "0.1.19"
@@ -9904,6 +10146,16 @@ dependencies = [
"serde",
]
[[package]]
name = "smol_str"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
dependencies = [
"borsh",
"serde",
]
[[package]]
name = "socket2"
version = "0.5.10"
@@ -10292,9 +10544,11 @@ dependencies = [
"actix_helpers",
"anyhow",
"artcraft_api_defs",
"async-stripe",
"async-stripe 0.26.0",
"async-stripe 0.41.0",
"async-trait",
"base64 0.22.1",
"billing_artcraft_component",
"billing_component",
"bootstrap",
"bucket_paths",
@@ -10302,6 +10556,7 @@ dependencies = [
"chrono",
"cloud_storage",
"collections",
"component_traits",
"composite_identifiers",
"config",
"cookies",
+37 -32
View File
@@ -6,6 +6,7 @@ resolver = "2"
members = [
# API clients for external services
# These might be custom implementations or wrappers over other client libraries.
"crates/api_clients/artcraft_api_defs", # TODO: api_clients/{artcraft}/{api_defs,client}
"crates/api_clients/cloud_storage",
"crates/api_clients/fal_client",
"crates/api_clients/google_drive_common",
@@ -97,7 +98,6 @@ members = [
#"crates/schema/database/sqlite_queries",
# Schema (public): common strongly-typed tokens, ids, payloads, etc.
"crates/schema/public/api/artcraft_api_defs",
"crates/schema/public/composite_identifiers",
"crates/schema/public/enums",
"crates/schema/public/enums_public",
@@ -119,6 +119,8 @@ members = [
# Service Plugins: complete subsystems that can be plugged into services (users, billing, etc.)
"crates/service/plugins/billing",
"crates/service/plugins/billing_artcraft",
"crates/service/plugins/component_traits",
"crates/service/plugins/inference_workloads",
"crates/service/plugins/user_traits_component",
@@ -176,6 +178,40 @@ publish = false
#
workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# Internal libs
# Setting paths here ensures we don't need to do so in every crate's Cargo.toml
# It makes refactoring much easier.
artcraft_api_defs = { path = "crates/api_clients/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" }
component_traits = { path = "crates/service/plugins/component_traits" }
cookie_store = { path = "crates/lib/cookie_store" }
crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
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" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
jwt_light = { path = "crates/lib/jwt_light" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
midjourney_client = { path = "crates/api_clients/midjourney_client" }
mimetypes = { path = "crates/lib/files/mimetypes" }
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
storyteller_client = { path = "crates/api_clients/storyteller_client" }
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" }
web_base64 = { path = "crates/lib/data/web_base64" }
# External libs
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
@@ -219,34 +255,3 @@ thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["macros"] }
toml = "0.8.22"
url = { version = "2.5.4", features = ["serde"] }
# Internal libs
artcraft_api_defs = { path = "crates/schema/public/api/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" }
cookie_store = { path = "crates/lib/cookie_store" }
crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/schema/public/enums" }
enums_public = { path = "crates/schema/public/enums_public" }
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" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
jwt_light = { path = "crates/lib/jwt_light" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
midjourney_client = { path = "crates/api_clients/midjourney_client" }
mimetypes = { path = "crates/lib/files/mimetypes" }
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
storyteller_client = { path = "crates/api_clients/storyteller_client" }
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" }
web_base64 = { path = "crates/lib/data/web_base64" }
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_subscriptions DROP COLUMN maybe_stripe_invoice_is_paid;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_subscriptions
ADD COLUMN maybe_stripe_invoice_is_paid BOOLEAN DEFAULT NULL
AFTER maybe_stripe_subscription_status;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE wallets;
@@ -0,0 +1,66 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE wallets (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate wallets. This is effectively a wallet namespace.
-- NB: Uses the `PaymentsNamespace` enum, which is also shared with subscriptions.
wallet_namespace VARCHAR(32) NOT NULL,
-- Who owns the wallet
-- In the future, we might allow other users to use the same wallet,
-- but the wallet would still have an owner.
-- For now, we'll keep this 1:1 with users, but we may want 1:n in the future.
owner_user_token VARCHAR(32) NOT NULL,
-- The durable credits bucket.
-- This bucket is persistent and is refilled by user credit purchases.
-- Max value unsigned: 4,294,967,295 (U32)
banked_credits INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- The monthly credits bucket.
-- This bucket is ephemeral and is refilled monthly.
-- Max value unsigned: 4,294,967,295 (U32)
monthly_credits INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Whether the stripe subscription is active.
-- subscription_is_active BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the stripe subscription is paid.
-- If the subscription is not paid, we should not allow usage of monthly credits.
-- subscription_is_paid BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== VECTOR CLOCK ==========
-- Incremented with every update.
version INT NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== PAYMENTS AND SUBSCRIPTIONS TIMESTAMPS ==========
-- When the user last purchased banked (durable) credits.
-- banked_credits_last_purchased_at TIMESTAMP DEFAULT NULL,
-- When we last refilled the monthly bucket.
-- This has to do with subscription logic.
-- monthly_wallet_refilled_at TIMESTAMP DEFAULT NULL,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (wallet_namespace, owner_user_token), -- For now
INDEX idx_wallet_namespace (wallet_namespace),
INDEX idx_owner_user_token (owner_user_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE wallet_ledger_entries;
@@ -0,0 +1,52 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE wallet_ledger_entries (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- Which wallet this entry corresponds to.
wallet_token VARCHAR(32) NOT NULL,
-- The type of ledger entry this is.
entry_type VARCHAR(32) NOT NULL,
-- A record we can look up to attribute the credit or deduction against
-- This could be an internal entity token (prompt_token, job token) or an
-- external token (stripe event_id, stripe invoice.id, etc.)
-- Length is VARCHAR(255) to accommodate Stripe IDs.
maybe_entity_ref VARCHAR(255) DEFAULT NULL,
-- Signed integer that shows the delta of credits for this event.
-- This combines impacts to both banked_credits and monthly_credits.
-- This is less authoritative than the before/after fields, but is useful
credits_delta INTEGER NOT NULL DEFAULT 0,
-- Balance of banked credits before the event.
-- Max value unsigned: 4,294,967,295 (U32)
banked_credits_before INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of banked credits after the event.
banked_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of monthly credits before the event.
-- Max value unsigned: 4,294,967,295 (U32)
monthly_credits_before INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of monthly credits after the event.
monthly_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY index_wallet_token (wallet_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_subscriptions DROP COLUMN maybe_stripe_billing_cycle_anchor;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_subscriptions
ADD COLUMN maybe_stripe_billing_cycle_anchor TIMESTAMP DEFAULT NULL
AFTER maybe_stripe_invoice_is_paid;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE stripe_webhook_event_logs DROP COLUMN maybe_internal_entity_token;
@@ -0,0 +1,7 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE stripe_webhook_event_logs
ADD COLUMN maybe_internal_entity_token VARCHAR(32) DEFAULT NULL
AFTER maybe_user_token;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE user_stripe_customer_links;
@@ -0,0 +1,36 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- 1:1 links between a user and a stripe customer (per payments namespace)
-- We can hard delete records if we have to.
-- If users need a new stripe customer ID, we need to hard delete the existing record.
CREATE TABLE user_stripe_customer_links (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Composite primary key
user_token VARCHAR(32) NOT NULL,
-- Composite primary key
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate accounts.
-- NB: Uses the `PaymentsNamespace` enum, which is also shared with subscriptions.
payments_namespace VARCHAR(32) NOT NULL,
-- Unique key. (We can loosen to `stripe_customer_id, wallet_namespace` later if
-- we want to share billing.)
stripe_customer_id VARCHAR(255) NOT NULL,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (user_token, payments_namespace),
-- UNIQUE KEY (stripe_customer_id, payments_namespace)
UNIQUE KEY (stripe_customer_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,66 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE stripe_webhook_event_logs (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- ========== STRIPE DATA ==========
-- You can safely assume object IDs we generate will never exceed 255 characters,
-- but you should be able to handle IDs of up to that length. If for example you’re using MySQL,
-- you should store IDs in a VARCHAR(255) COLLATE utf8_bin column (the COLLATE configuration ensures
-- case-sensitivity in lookups).
-- https://stackoverflow.com/a/61809494
stripe_event_id VARCHAR(255) NOT NULL,
-- The type of webhook event
stripe_event_type VARCHAR(255) NOT NULL,
-- The id of the entity that was the subject of the event.
-- We don't always extract this.
maybe_stripe_event_entity_id VARCHAR(255) DEFAULT NULL,
-- The id of the entity that was the subject of the event.
-- We don't always extract this.
maybe_stripe_customer_id VARCHAR(255) DEFAULT NULL,
-- Whether this is in production or test mode in Stripe.
-- This is controlled by which API keys are used.
-- "livemode"=true in production.
stripe_is_production BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== INTERNAL SYSTEM METADATA ==========
-- Whether we handled the event with any sort of action
action_was_taken BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether we should ignore any future retries by Stripe replaying the event.
-- This helps us treat events with idempotency.
should_ignore_retry BOOLEAN NOT NULL DEFAULT FALSE,
-- Some event types may attach a user token
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- An internal optional entity token this resolves to.
-- eg. `wallets.token` or `user_subscriptions.token`.
maybe_internal_entity_token VARCHAR(32) DEFAULT NULL,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- When the event was created on Stripe's end
stripe_event_created_at TIMESTAMP NOT NULL,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (stripe_event_id),
KEY index_stripe_event_type (stripe_event_type),
KEY index_maybe_stripe_event_entity_id (maybe_stripe_event_entity_id),
KEY index_maybe_stripe_customer_id (maybe_stripe_customer_id),
KEY fk_maybe_user_token (maybe_user_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,36 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- 1:1 links between a user and a stripe customer (per payments namespace)
-- We can hard delete records if we have to.
-- If users need a new stripe customer ID, we need to hard delete the existing record.
CREATE TABLE user_stripe_customer_links (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Composite primary key
user_token VARCHAR(32) NOT NULL,
-- Composite primary key
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate accounts.
-- NB: Uses the `PaymentsNamespace` enum, which is also shared with subscriptions.
payments_namespace VARCHAR(32) NOT NULL,
-- Unique key. (We can loosen to `stripe_customer_id, wallet_namespace` later if
-- we want to share billing.)
stripe_customer_id VARCHAR(255) NOT NULL,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (user_token, payments_namespace),
-- UNIQUE KEY (stripe_customer_id, payments_namespace)
UNIQUE KEY (stripe_customer_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -53,6 +53,18 @@ CREATE TABLE user_subscriptions (
-- Subscription object status enum as string
maybe_stripe_subscription_status VARCHAR(32) DEFAULT NULL,
-- Primarily managed by the `invoice.paid` and `invoice.payment_failed` webhooks.
-- NB: Not applicable to FakeYou subscriptions!
maybe_stripe_invoice_is_paid BOOLEAN DEFAULT NULL,
-- Which day of the month, or month of the year, etc. to anchor the subscription to.
-- See docs on the subscription object.
-- Stripe:
-- "The reference point that aligns future billing cycle dates. It sets the day
-- of week for week intervals, the day of month for month and year intervals, and
-- the month of year for year intervals. The timestamp is in UTC format."
maybe_stripe_billing_cycle_anchor TIMESTAMP DEFAULT NULL,
maybe_stripe_is_production BOOLEAN DEFAULT NULL,
-- ========== VECTOR CLOCK ==========
@@ -64,6 +76,8 @@ CREATE TABLE user_subscriptions (
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- NB(bt, 2025-09-16): I don't think we write to the soft deleted_at field for user_subscriptions.
deleted_at TIMESTAMP DEFAULT NULL,
-- ========== SUBSCRIPTION TIMESTAMPS ==========
@@ -72,7 +86,10 @@ CREATE TABLE user_subscriptions (
-- This may predate the Stripe object `created` timestamp due to backdating.
subscription_start_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- We'll use this to determine if the subscription is active.
-- Our synthetic "expire" date. Moved forward for active subscriptions,
-- backdated for canceled subscriptions.
--
-- TODO: We'll (maybe?) use this to determine if the subscription is active.
--
-- Always compare against MySQL's clock rather than the app's clock so that we
-- don't get weird clock skew behaviors across multiple requests.
@@ -82,8 +99,12 @@ CREATE TABLE user_subscriptions (
subscription_expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Billing periods.
-- Maybe useful for debugging.
-- Stripe API: subscription.items[0].data.current_period_start
current_billing_period_start_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Stripe API: subscription.items[0].data.current_period_end
-- current_period_end is when the subscription renews (or cancels, if the
-- subscription is set to cancel).
current_billing_period_end_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Subscription cancellation (future and past)
@@ -0,0 +1,52 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE wallet_ledger_entries (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- Which wallet this entry corresponds to.
wallet_token VARCHAR(32) NOT NULL,
-- The type of ledger entry this is.
entry_type VARCHAR(16) NOT NULL,
-- A record we can look up to attribute the credit or deduction against
-- This could be an internal entity token (prompt_token, job token) or an
-- external token (stripe event_id, stripe invoice.id, etc.)
-- Length is VARCHAR(255) to accommodate Stripe IDs.
maybe_entity_ref VARCHAR(255) DEFAULT NULL,
-- Signed integer that shows the delta of credits for this event.
-- This combines impacts to both banked_credits and monthly_credits.
-- This is less authoritative than the before/after fields, but is useful
credits_delta INTEGER NOT NULL DEFAULT 0,
-- Balance of banked credits before the event.
-- Max value unsigned: 4,294,967,295 (U32)
banked_credits_before INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of banked credits after the event.
banked_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of monthly credits before the event.
-- Max value unsigned: 4,294,967,295 (U32)
monthly_credits_before INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Balance of monthly credits after the event.
monthly_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY index_wallet_token (wallet_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,66 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE wallets (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC)
token VARCHAR(32) NOT NULL,
-- If we host multiple sites with distinct categories, this will enable us to
-- segregate wallets. This is effectively a wallet namespace.
-- NB: Uses the `PaymentsNamespace` enum, which is also shared with subscriptions.
wallet_namespace VARCHAR(32) NOT NULL,
-- Who owns the wallet
-- In the future, we might allow other users to use the same wallet,
-- but the wallet would still have an owner.
-- For now, we'll keep this 1:1 with users, but we may want 1:n in the future.
owner_user_token VARCHAR(32) NOT NULL,
-- The durable credits bucket.
-- This bucket is persistent and is refilled by user credit purchases.
-- Max value unsigned: 4,294,967,295 (U32)
banked_credits INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- The monthly credits bucket.
-- This bucket is ephemeral and is refilled monthly.
-- Max value unsigned: 4,294,967,295 (U32)
monthly_credits INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Whether the stripe subscription is active.
-- subscription_is_active BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the stripe subscription is paid.
-- If the subscription is not paid, we should not allow usage of monthly credits.
-- subscription_is_paid BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== VECTOR CLOCK ==========
-- Incremented with every update.
version INT NOT NULL DEFAULT 0,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- ========== PAYMENTS AND SUBSCRIPTIONS TIMESTAMPS ==========
-- When the user last purchased banked (durable) credits.
-- banked_credits_last_purchased_at TIMESTAMP DEFAULT NULL,
-- When we last refilled the monthly bucket.
-- This has to do with subscription logic.
-- monthly_wallet_refilled_at TIMESTAMP DEFAULT NULL,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (wallet_namespace, owner_user_token), -- For now
INDEX idx_wallet_namespace (wallet_namespace),
INDEX idx_owner_user_token (owner_user_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,20 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GetSessionCreditsResponse {
pub success: bool,
/// Any free credits the user might have
/// We might support daily free credits.
pub free_credits: u64,
/// Any monthly refilling credits the user might have
pub monthly_credits: u64,
/// Any banked credits the user might have.
pub banked_credits: u64,
/// All the credit amounts added together.
pub sum_total_credits: u64,
}
@@ -0,0 +1 @@
pub mod get_session_credits;
@@ -0,0 +1,50 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GEMINI_25_FLASH_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/gemini_25_flash";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Gemini25FlashEditImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
// TODO: OneOf for image_urls or image_media_tokens
/// Image media tokens to include in the editing context.
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<Gemini25FlashEditImageNumImages>,
/// Quality of the image to generate. Default is High.
pub image_quality: Option<Gemini25FlashEditImageImageQuality>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Gemini25FlashEditImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Gemini25FlashEditImageImageQuality {
Auto,
Low,
Medium,
High, // Default
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Gemini25FlashEditImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +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 qwen_edit_image;
pub mod seededit_3_edit_image;
@@ -0,0 +1,51 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const QWEN_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/qwen";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct QwenEditImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// The image we're editing.
pub image_media_token: MediaFileToken,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Text prompt to generate the image from.
pub negative_prompt: Option<String>,
/// Acceleration level for image generation.
/// Options: 'none', 'regular'. Higher acceleration increases speed.
/// 'regular' balances speed and quality. Default value: "none"
pub acceleration: Option<QwenEditImageAcceleration>,
/// Number of images to generate. Default is one.
pub num_images: Option<QwenEditImageNumImages>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub enum QwenEditImageAcceleration {
None,
Regular,
High,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum QwenEditImageNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct QwenEditImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,24 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const SEEDEDIT_3_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/seededit_3";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct SeedEdit3EditImageRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// The image we're editing.
pub image_media_token: MediaFileToken,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct SeedEdit3EditImageResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -13,7 +13,11 @@ pub struct GenerateKling16ProImageToVideoRequest {
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional.
/// Image media file; the image to remove the background from.
pub end_frame_image_media_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
@@ -13,6 +13,10 @@ pub struct GenerateKling21ProImageToVideoRequest {
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional.
/// Image media file; the image to remove the background from.
pub end_frame_image_media_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
@@ -13,7 +13,11 @@ pub struct GenerateSeedance10LiteImageToVideoRequest {
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional.
/// Image media file; the image to remove the background from.
pub end_frame_image_media_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
@@ -0,0 +1,54 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_SEEDANCE_1_0_PRO_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/seedance_1.0_pro_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateSeedance10ProImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: aspect ratio of the generated video.
pub resolution: Option<GenerateSeedance10ProResolution>,
/// Optional: aspect ratio of the generated video.
pub duration: Option<GenerateSeedance10ProDuration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateSeedance10ProResolution {
FourEightyP,
SevenTwentyP,
TenEightyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateSeedance10ProDuration {
ThreeSeconds,
FourSeconds,
FiveSeconds,
SixSeconds,
SevenSeconds,
EightSeconds,
NineSeconds,
TenSeconds,
ElevenSeconds,
TwelveSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateSeedance10ProImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_VEO_3_FAST_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/veo_3_fast_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateVeo3FastImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: resolution of the generated video.
pub resolution: Option<GenerateVeo3FastResolution>,
/// Optional: aspect ratio of the generated video.
pub duration: Option<GenerateVeo3FastDuration>,
/// Optional: Generate an audio track along with the video
pub generate_audio: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3FastResolution {
SevenTwentyP, // 720p
TenEightyP, // 1080p
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3FastDuration {
EightSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateVeo3FastImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -0,0 +1,65 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const GENERATE_VEO_3_IMAGE_TO_VIDEO_URL_PATH: &str = "/v1/generate/video/veo_3_image_to_video";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateVeo3ImageToVideoRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Source of the image file to convert to video.
/// It must be an image.
pub media_file_token: Option<MediaFileToken>,
/// Optional text prompt.
pub prompt: Option<String>,
/// Optional: aspect ratio of the generated video.
pub aspect_ratio: Option<GenerateVeo3AspectRatio>,
/// Optional: resolution of the generated video.
pub resolution: Option<GenerateVeo3Resolution>,
/// Optional: aspect ratio of the generated video.
pub duration: Option<GenerateVeo3Duration>,
/// Optional: Generate an audio track along with the video
pub generate_audio: Option<bool>,
// /// Optional.
// pub enhance_prompt: Option<bool>,
// /// Optional. Whether to automatically attempt to fix prompts that fail
// /// content policy or other validation checks by rewriting them
// /// Default value: true
// pub auto_fix: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3AspectRatio {
WideSixteenNine, // 16:9
TallNineSixteen, // 9:16
Square, // 1:1
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3Resolution {
SevenTwentyP, // 720p
TenEightyP, // 1080p
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3Duration {
EightSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GenerateVeo3ImageToVideoResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -2,4 +2,7 @@ pub mod generate_kling_1_6_pro_image_to_video;
pub mod generate_kling_2_1_master_image_to_video;
pub mod generate_kling_2_1_pro_image_to_video;
pub mod generate_seedance_1_0_lite_image_to_video;
pub mod generate_veo_2_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;
@@ -0,0 +1,8 @@
pub mod common;
pub mod credits;
pub mod generate;
pub mod jobs;
pub mod media_file;
pub mod prompts;
pub mod stripe_artcraft;
pub mod subscriptions;
@@ -0,0 +1,17 @@
use enums::common::artcraft_credits_pack_slug::ArtcraftCreditsPackSlug;
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CREATE_CREDITS_PACK_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/checkout/credits_pack";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateCreditsPackCheckoutRequest {
pub credits_pack: Option<ArtcraftCreditsPackSlug>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateCreditsPackCheckoutResponse {
pub success: bool,
pub stripe_checkout_redirect_url: String,
}
@@ -0,0 +1,30 @@
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CREATE_SUBSCRIPTION_CHECKOUT_URL_PATH: &str = "/v1/stripe_artcraft/checkout/subscription";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateSubscriptionCheckoutRequest {
/// The (non-Stripe) internal identifier for the product or subscription.
/// This will be translated into a Stripe identifier.
pub plan: Option<ArtcraftSubscriptionSlug>,
pub cadence: Option<PlanBillingCadence>,
}
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
pub enum PlanBillingCadence {
#[serde(rename = "monthly")]
Monthly,
#[serde(rename = "yearly")]
Yearly,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCreateSubscriptionCheckoutResponse {
pub success: bool,
pub stripe_checkout_redirect_url: String,
}
@@ -0,0 +1,17 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CUSTOMER_PORTAL_CANCEL_PLAN_URL_PATH: &str = "/v1/stripe_artcraft/portal/cancel_plan";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalCancelPlanRequest {
// TODO: Not sure if this is needed
pub portal_config_id: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalCancelPlanResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -0,0 +1,17 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CUSTOMER_PORTAL_MANAGE_PLAN_URL_PATH: &str = "/v1/stripe_artcraft/portal/manage_plan";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalManagePlanRequest {
// TODO: Not sure if this is needed
pub portal_config_id: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalManagePlanResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -0,0 +1,37 @@
use enums::common::artcraft_subscription_slug::ArtcraftSubscriptionSlug;
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CUSTOMER_PORTAL_SWITCH_PLAN_URL_PATH: &str = "/v1/stripe_artcraft/portal/switch_plan";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalSwitchPlanRequest {
// TODO: Not sure if this is needed
pub portal_config_id: Option<String>,
/// REQUIRED
/// The plan we're switching to.
pub plan: Option<ArtcraftSubscriptionSlug>,
/// REQUIRED
/// The cadence for the plan billing.
pub cadence: Option<PlanBillingCadenceConfirmation>,
}
#[derive(Serialize, Deserialize, ToSchema, Debug, Copy, Clone)]
pub enum PlanBillingCadenceConfirmation {
#[serde(rename = "monthly")]
Monthly,
#[serde(rename = "yearly")]
Yearly,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalSwitchPlanResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -0,0 +1,17 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
pub const CUSTOMER_PORTAL_UPDATE_PAYMENT_METHOD_URL_PATH: &str = "/v1/stripe_artcraft/portal/update_payment_method";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalUpdatePaymentMethodRequest {
// TODO: Not sure if this is needed
pub portal_config_id: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct StripeArtcraftCustomerPortalUpdatePaymentMethodResponse {
pub success: bool,
pub stripe_portal_url: String,
}
@@ -0,0 +1,6 @@
pub mod create_credits_pack_checkout;
pub mod create_subscription_checkout;
pub mod customer_portal_cancel_plan;
pub mod customer_portal_manage_plan;
pub mod customer_portal_switch_plan;
pub mod customer_portal_update_payment_method;
@@ -0,0 +1,37 @@
use chrono::{DateTime, Utc};
use enums::common::payments_namespace::PaymentsNamespace;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::user_subscriptions::UserSubscriptionToken;
use utoipa::ToSchema;
// TODO: Maybe make "artcraft_subscription" or "fakeyou_subscription" with strongly typed slugs?
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GetSessionSubscriptionResponse {
pub success: bool,
/// Information about the user's subscription, if any.
pub active_subscription: Option<SubscriptionInfo>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct SubscriptionInfo {
/// Unique token for the subscription.
pub subscription_token: UserSubscriptionToken,
/// Namespace for the subscription.
pub namespace: PaymentsNamespace,
/// Local key for the subscription.
pub product_slug: String,
/// If the subscription is currently active, this is the next billing date.
pub next_bill_at: Option<DateTime<Utc>>,
/// If the subscription is set to cancel, this is the end date of the subscription.
pub subscription_end_at: Option<DateTime<Utc>>,
///// When the current billing period ends (either
///// auto-renew/rebill date or auto-cancel/cancellation date).
//pub current_billing_period_end_at: DateTime<Utc>,
}
@@ -0,0 +1 @@
pub mod get_session_subscription;
@@ -1,14 +1,23 @@
use crate::error::fal_error_plus::FalErrorPlus;
const BILLING_ERROR : &str = "{\"detail\": \"User is locked. Reason: Exhausted balance. Top up your balance at fal.ai/dashboard/billing.\"}";
/// Better classify the `fal::FalError` into a more user-friendly `FalErrorPlus`.
pub fn classify_fal_error(err: fal::FalError) -> FalErrorPlus {
// Other("{\"detail\": \"User is locked. Reason: Exhausted balance. Top up your balance at fal.ai/dashboard/billing.\"}")
match err {
fal::FalError::RequestError(_) => FalErrorPlus::FalError(err),
fal::FalError::ImageError(_) => FalErrorPlus::FalError(err),
fal::FalError::SerializeError(_) => FalErrorPlus::FalError(err),
fal::FalError::StreamError(_) => FalErrorPlus::FalError(err),
fal::FalError::Other(ref inner) => {
if inner.contains("Invalid Key Authorization header format") {
if inner == BILLING_ERROR {
FalErrorPlus::FalBillingError(inner.to_string())
} else if inner.contains("billing") {
FalErrorPlus::FalBillingError(inner.to_string())
} else if inner.contains("balance") {
FalErrorPlus::FalBillingError(inner.to_string())
} else if inner.contains("Invalid Key Authorization header format") {
FalErrorPlus::FalApiKeyError(inner.to_string())
} else if inner.contains("No user found for Key ID and Secret") {
FalErrorPlus::FalApiKeyError(inner.to_string())
@@ -9,6 +9,8 @@ pub enum FalErrorPlus {
FalError(fal::FalError),
/// The fal API key is invalid.
FalApiKeyError(String),
/// The fal account has a billing issue
FalBillingError(String),
/// Another error we didn't handle.
AnyhowError(anyhow::Error),
/// URL parse errors.
@@ -24,6 +26,7 @@ impl Display for FalErrorPlus {
match self {
Self::FalError(err) => write!(f, "FalErrorPlus::FalError: {:?}", err),
Self::FalApiKeyError(reason) => write!(f, "FalErrorPlus::FalApiKeyError: {}", reason),
Self::FalBillingError(reason) => write!(f, "FalErrorPlus::FalBillingError: {}", reason),
Self::AnyhowError(err) => write!(f, "FalErrorPlus::AnyhowError: {:?}", err),
Self::UrlParseError(err) => write!(f, "FalErrorPlus::UrlParseError: {:?}", err),
Self::UnhandledEndpoint(endpoint) => write!(f, "FalErrorPlus::UnhandledEndpoint: {:?}", endpoint),
@@ -0,0 +1,87 @@
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::gemini_25_flash_image::edit::gemini_25_flash_image_edit;
use fal::prelude::fal_ai::gemini_25_flash_image::edit::Gemini25FlashImageEditInput;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Gemini25FlashEditArgs<'a, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_urls: Vec<String>,
pub num_images: Gemini25FlashEditNumImages,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum Gemini25FlashEditNumImages {
One,
Two,
Three,
Four,
}
pub async fn enqueue_gemini_25_flash_edit_webhook<R: IntoUrl>(
args: Gemini25FlashEditArgs<'_, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = match args.num_images {
Gemini25FlashEditNumImages::One => 1,
Gemini25FlashEditNumImages::Two => 2,
Gemini25FlashEditNumImages::Three => 3,
Gemini25FlashEditNumImages::Four => 4,
};
let request = Gemini25FlashImageEditInput {
prompt: args.prompt.to_string(),
image_urls: args.image_urls,
// Constants
num_images: Some(num_images),
output_format: Some("png".to_string()),
};
let result = gemini_25_flash_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_gemini_25_flash_edit_webhook::{enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs, Gemini25FlashEditNumImages};
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 = Gemini25FlashEditArgs {
image_urls: vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
],
num_images: Gemini25FlashEditNumImages::Two,
prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly",
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_gemini_25_flash_edit_webhook(args).await?;
Ok(())
}
}
@@ -21,7 +21,7 @@ pub struct GptEditImageByokArgs<'a, V: IntoUrl> {
}
#[derive(Copy, Clone, Debug)]
pub enum GptEditImageSize{
pub enum GptEditImageSize {
Auto,
Square,
Horizontal,
@@ -92,14 +92,14 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook<V: IntoUrl>(
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::creds::open_ai_api_key::OpenAiApiKey;
use crate::requests::webhook::image::enqueue_gpt_image_1_edit_image_webhook::{enqueue_gpt_image_1_edit_image_webhook, GptEditImageByokArgs, GptEditImageNumImages, GptEditImageQuality, GptEditImageSize};
use crate::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::{enqueue_gpt_image_1_edit_image_webhook, GptEditImageByokArgs, GptEditImageNumImages, GptEditImageQuality, GptEditImageSize};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, GRASSY_HILL_TRANSPARENT_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
// XXX: Don't commit secrets!
@@ -0,0 +1,129 @@
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::qwen_image_edit::{qwen_image_edit, QwenImageEditInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct QwenImageEditArgs<'a, U: IntoUrl, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_url: U,
// Request optional
pub num_images: Option<QwenImageEditNumImages>,
pub image_size: Option<QwenImageEditSize>,
pub negative_prompt: Option<String>,
/// Acceleration level for image generation.
/// Options: 'none', 'regular'. Higher acceleration increases speed.
/// 'regular' balances speed and quality. Default value: "none"
pub acceleration: Option<String>,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
#[derive(Copy, Clone, Debug)]
pub enum QwenImageEditNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug)]
pub enum QwenImageEditSize {
Square, // 1:1
SquareHd, // 1:1
LandscapeFourByThree, // 4:3
LandscapeSixteenByNine, // 16:9
PortraitThreeByFour, // 3:4
PortraitNineBySixteen, // 9:16
//Custom { width: u32, height: u32 }, // TODO
}
pub async fn enqueue_qwen_image_edit_webhook<U: IntoUrl, R: IntoUrl>(
args: QwenImageEditArgs<'_, U, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let num_images = args.num_images
.map(|num| match num {
QwenImageEditNumImages::One => 1,
QwenImageEditNumImages::Two => 2,
QwenImageEditNumImages::Three => 3,
QwenImageEditNumImages::Four => 4,
});
let image_size = args.image_size
.map(|size| match size {
QwenImageEditSize::Square => "square",
QwenImageEditSize::SquareHd => "square_hd",
QwenImageEditSize::LandscapeFourByThree => "landscape_4_3",
QwenImageEditSize::LandscapeSixteenByNine => "landscape_16_9",
QwenImageEditSize::PortraitThreeByFour => "portrait_4_3", // NB: I think they made a mistake
QwenImageEditSize::PortraitNineBySixteen => "portrait_16_9", // NB: I think they made a mistake
})
.map(|size| size.to_string());
let request = QwenImageEditInput {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
image_size,
num_images,
// Maybe expose
output_format: Some("png".to_string()), // png or jpeg
negative_prompt: None,
seed: None,
// Constants
//sync_mode: None, // Synchronous / slow
num_inference_steps: None,
guidance_scale: None,
enable_safety_checker: None,
acceleration: None,
};
let result = qwen_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_qwen_image_edit_webhook::{enqueue_qwen_image_edit_webhook, QwenImageEditArgs, QwenImageEditNumImages};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::MOUNTAIN_TREE_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 = QwenImageEditArgs {
image_url: MOUNTAIN_TREE_IMAGE_URL,
prompt: "put christmas lights on the tree, add snow to the mountains",
num_images: Some(QwenImageEditNumImages::One),
image_size: None,
negative_prompt: None,
acceleration: None,
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_qwen_image_edit_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,67 @@
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::seededit::edit_image_v3::seededit_v3_edit;
use fal::endpoints::fal_ai::bytedance::seededit::edit_image_v3::SeedEditV3Input;
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct SeedEditV3EditArgs<'a, U: IntoUrl, R: IntoUrl> {
// Request required
pub prompt: &'a str,
pub image_url: U,
// Fulfillment
pub webhook_url: R,
pub api_key: &'a FalApiKey,
}
pub async fn enqueue_seededit_v3_edit_webhook<U: IntoUrl, R: IntoUrl>(
args: SeedEditV3EditArgs<'_, U, R>
) -> Result<WebhookResponse, FalErrorPlus> {
let request = SeedEditV3Input {
prompt: args.prompt.to_string(),
image_url: args.image_url.as_str().to_string(),
// Constants
guidance_scale: None,
enable_safety_checker: None,
};
let result = seededit_v3_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_seededit_v3_edit_webhook::{enqueue_seededit_v3_edit_webhook, SeedEditV3EditArgs};
use errors::AnyhowResult;
use std::fs::read_to_string;
use test_data::web::image_urls::MOUNTAIN_TREE_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 = SeedEditV3EditArgs {
image_url: MOUNTAIN_TREE_IMAGE_URL,
prompt: "put christmas lights on the tree, add snow to the mountains",
api_key: &api_key,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_seededit_v3_edit_webhook(args).await?;
Ok(())
}
}
@@ -1 +1,5 @@
pub mod enqueue_flux_pro_kontext_max_edit_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_qwen_image_edit_webhook;
pub mod enqueue_seededit_v3_edit_webhook;
@@ -85,7 +85,7 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_flux_1_dev() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
@@ -84,7 +84,7 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_flux_1_schnell() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
@@ -83,7 +83,7 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_flux_11_pro() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
@@ -96,7 +96,7 @@ mod tests {
#[tokio::test]
#[ignore]
async fn test_gpt_image_1_edit_image() -> AnyhowResult<()> {
async fn test() -> AnyhowResult<()> {
// XXX: Don't commit secrets!
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
@@ -3,7 +3,6 @@ pub mod enqueue_flux_1_dev_text_to_image_webhook;
pub mod enqueue_flux_1_schnell_text_to_image_webhook;
pub mod enqueue_flux_pro_11_text_to_image_webhook;
pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook;
pub mod enqueue_gpt_image_1_edit_image_webhook;
pub mod enqueue_gpt_image_1_text_to_image_webhook;
pub mod infill;
pub mod remove_background_rembg_webhook;
@@ -41,7 +41,7 @@ mod tests {
#[tokio::test]
#[ignore] // NB: Manually test, don't run in CI!
async fn test_rembg_request() -> anyhow::Result<()> {
async fn test() -> anyhow::Result<()> {
// XXX: Don't commit secrets!
let api_key = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
let api_key = FalApiKey::from_str(&api_key);
@@ -5,8 +5,9 @@ use fal::endpoints::fal_ai::kling_video::v1_6::pro::image_to_video::{image_to_vi
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling16ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub struct Kling16ProArgs<'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,
@@ -28,8 +29,8 @@ pub enum Kling16ProAspectRatio {
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling16ProArgs<'_, U, V>
pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling16ProArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling16Duration::Default => None,
@@ -45,15 +46,18 @@ pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>
let image_url = args.image_url.as_str().to_string();
let tail_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = ProImageToVideoRequest {
image_url,
tail_image_url,
prompt: args.prompt.to_string(),
aspect_ratio,
duration,
// Maybe expose these later
cfg_scale: None,
negative_prompt: None,
tail_image_url: None,
};
let result = image_to_video(request)
@@ -64,35 +68,34 @@ pub async fn enqueue_kling_16_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>
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_kling_16_image_to_video_webhook::{enqueue_kling_16_image_to_video_webhook, Kling16Args, Kling16Duration};
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 errors::AnyhowResult;
use std::fs::read_to_string;
use testing::test_file_path::test_file_path;
use test_data::web::image_urls::{JUNO_AT_LAKE_IMAGE_URL, TALL_MOCHI_WITH_GLASSES_IMAGE_URL};
#[tokio::test]
#[ignore]
async fn test_kling16_video() -> AnyhowResult<()> {
let image = test_file_path("test_data/image/juno.jpg")?;
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 = Kling16Args {
image_path: image,
prompt: "a corgi looks out over the water",
let args = Kling16ProArgs {
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,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_kling_16_image_to_video_webhook(args).await?;
let result = enqueue_kling_16_pro_image_to_video_webhook(args).await?;
Ok(())
}
}
*/
@@ -5,8 +5,9 @@ use fal::endpoints::fal_ai::kling_video::v2_1::pro::image_to_video::{image_to_vi
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Kling21ProArgs<'a, U: IntoUrl, V: IntoUrl> {
pub struct Kling21ProArgs<'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,
@@ -28,8 +29,8 @@ pub enum Kling21ProAspectRatio {
TallNineSixteen, // 9:16
}
pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Kling21ProArgs<'_, U, V>
pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, T: IntoUrl, V: IntoUrl>(
args: Kling21ProArgs<'_, U, T, V>
) -> Result<WebhookResponse, FalErrorPlus> {
let duration = match args.duration {
Kling21ProDuration::Default => None,
@@ -45,15 +46,18 @@ pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>
let image_url = args.image_url.as_str().to_string();
let tail_image_url = args.end_frame_image_url
.map(|url| url.as_str().to_string());
let request = ProImageToVideoRequest {
image_url,
prompt: args.prompt.to_string(),
tail_image_url,
aspect_ratio,
duration,
// Maybe expose these later
cfg_scale: None,
negative_prompt: None,
tail_image_url: None,
};
let result = image_to_video(request)
@@ -64,27 +68,26 @@ pub async fn enqueue_kling_21_pro_image_to_video_webhook<U: IntoUrl, V: IntoUrl>
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_kling_21_pro_image_to_video_webhook::{enqueue_kling_21_pro_image_to_video_webhook, Kling21ProArgs, Kling21ProAspectRatio, Kling21ProDuration};
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};
#[tokio::test]
#[ignore]
async fn test_kling21_pro_video() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
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 = Kling21ProArgs {
image_url: image_url,
prompt: "a shot of the mountains, the camera rotates around to show the mountain range",
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,
@@ -6,8 +6,9 @@ use fal::endpoints::fal_ai::bytedance::seedance::v1::lite::image_to_video::Image
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Seedance1LiteArgs<'a, U: IntoUrl, V: 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,
@@ -29,8 +30,8 @@ pub enum Seedance1LiteResolution {
SevenTwentyP, // 720p
}
pub async fn enqueue_seedance_1_lite_image_to_video_webhook<U: IntoUrl, V: IntoUrl>(
args: Seedance1LiteArgs<'_, U, V>
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()),
@@ -44,14 +45,20 @@ pub async fn enqueue_seedance_1_lite_image_to_video_webhook<U: IntoUrl, V: IntoU
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)
@@ -69,20 +76,20 @@ mod tests {
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_kling21_pro_video() -> AnyhowResult<()> {
let image_url = "https://cdn-2.fakeyou.com/media/3/4/h/f/s/34hfsmt8e38rvne6mwa4pwbxr6292sgy/image_34hfsmt8e38rvne6mwa4pwbxr6292sgy.png";
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: image_url,
prompt: "a shot of the mountains as the sun sets and reveals the moon and stars",
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,
@@ -0,0 +1,115 @@
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,96 @@
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::veo3::image_to_video_fast::{image_to_video_fast, ImageToVideoFastInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Veo3FastArgs<'a, U: IntoUrl, V: IntoUrl> {
pub prompt: &'a str,
pub image_url: U,
pub duration: Veo3FastDuration,
pub api_key: &'a FalApiKey,
pub resolution: Veo3FastResolution,
pub generate_audio: bool,
pub webhook_url: V,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3FastDuration {
Default,
EightSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3FastResolution {
Default,
SevenTwentyP,
TenEightyP,
}
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::EightSeconds => Some("8s".to_string()),
};
let resolution= match args.resolution {
Veo3FastResolution::Default => None,
Veo3FastResolution::SevenTwentyP => Some("720p".to_string()),
Veo3FastResolution::TenEightyP => Some("1080p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = ImageToVideoFastInput {
image_url,
prompt: args.prompt.to_string(),
resolution,
duration,
generate_audio: Some(args.generate_audio),
};
let result = image_to_video_fast(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_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;
#[tokio::test]
#[ignore]
async fn test() -> AnyhowResult<()> {
let image_url = ERNEST_GHOST_TREX_IMAGE_URL;
// 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 = Veo3FastArgs {
image_url: image_url,
prompt: "man is standing next to a ghost and t-rex, they begin to chase him as the camera pulls back to show the wider scene",
api_key: &api_key,
duration: Veo3FastDuration::EightSeconds,
generate_audio: true,
resolution: Veo3FastResolution::TenEightyP,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3_fast_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -0,0 +1,112 @@
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::veo3::image_to_video::{image_to_video, ImageToVideoInput};
use fal::webhook::WebhookResponse;
use reqwest::IntoUrl;
pub struct Veo3Args<'a, U: IntoUrl, V: IntoUrl> {
pub image_url: U,
pub prompt: &'a str,
pub duration: Veo3Duration,
pub aspect_ratio: Veo3AspectRatio,
pub resolution: Veo3Resolution,
pub generate_audio: bool,
pub api_key: &'a FalApiKey,
pub webhook_url: V,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3Duration {
Default,
EightSeconds,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3AspectRatio {
Default,
WideSixteenNine,
TallNineSixteen,
Square,
}
#[derive(Copy, Clone, Debug)]
pub enum Veo3Resolution {
Default,
SevenTwentyP,
TenEightyP,
}
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::EightSeconds => Some("8s".to_string()),
};
let aspect_ratio = match args.aspect_ratio {
Veo3AspectRatio::Default => None,
Veo3AspectRatio::WideSixteenNine => Some("16:9".to_string()),
Veo3AspectRatio::TallNineSixteen => Some("9:16".to_string()),
Veo3AspectRatio::Square => Some("1:1".to_string()),
};
let resolution= match args.resolution {
Veo3Resolution::Default => None,
Veo3Resolution::SevenTwentyP => Some("720p".to_string()),
Veo3Resolution::TenEightyP => Some("1080p".to_string()),
};
let image_url = args.image_url.as_str().to_string();
let request = ImageToVideoInput {
image_url,
prompt: args.prompt.to_string(),
aspect_ratio,
resolution,
duration,
generate_audio: Some(args.generate_audio),
};
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_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};
#[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 = Veo3Args {
image_url: TREX_SKELETON_IMAGE_URL,
prompt: "the t-rex skeleton starts walking towards the camera and roars",
api_key: &api_key,
duration: Veo3Duration::EightSeconds,
aspect_ratio: Veo3AspectRatio::WideSixteenNine,
resolution: Veo3Resolution::TenEightyP,
generate_audio: true,
webhook_url: "https://example.com/webhook",
};
let result = enqueue_veo_3_image_to_video_webhook(args).await?;
Ok(())
}
}
@@ -2,4 +2,7 @@ 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_veo_2_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;
@@ -0,0 +1,27 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_get_request::basic_json_get_request;
use artcraft_api_defs::credits::get_session_credits::GetSessionCreditsResponse;
use enums::common::payments_namespace::PaymentsNamespace;
pub async fn get_session_credits(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
payments_namespace: PaymentsNamespace
) -> Result<GetSessionCreditsResponse, StorytellerError> {
let url_path = get_url_path(payments_namespace);
Ok(basic_json_get_request(
api_host,
&url_path,
maybe_creds,
).await?)
}
fn get_url_path(payments_namespace: PaymentsNamespace) -> String {
let payments_namespace = payments_namespace.to_str();
format!("/v1/credits/namespace/{}", payments_namespace)
}
@@ -0,0 +1 @@
pub mod get_session_credits;
@@ -11,6 +11,9 @@ pub enum ApiError {
/// 401. The request was not authorized.
Unauthorized(String),
// 402. Payment required.
PaymentRequired(String),
/// 403. The request was forbidden.
Forbidden(String),
@@ -56,6 +59,7 @@ impl Display for ApiError {
match self {
// Server response code errors
ApiError::InvalidRequest(msg) => write!(f, "Invalid request: {}", msg),
ApiError::PaymentRequired(msg) => write!(f, "Payment required: {}", msg),
ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
@@ -80,6 +84,13 @@ impl Display for ApiError {
impl From<reqwest::Error> for ApiError {
fn from(error: reqwest::Error) -> Self {
if let Some(status) = error.status() {
let status = status.as_u16();
match status {
402 => return ApiError::PaymentRequired(error.to_string()),
_ => {} // NB: Fallthrough.
}
}
if error.is_timeout() {
ApiError::Timeout(error.to_string())
} else if error.is_connect() {
@@ -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::edit::gemini_25_flash_edit_image::{Gemini25FlashEditImageRequest, Gemini25FlashEditImageResponse, GEMINI_25_FLASH_EDIT_IMAGE_PATH};
pub async fn gemini_25_flash_edit_image(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: Gemini25FlashEditImageRequest,
) -> Result<Gemini25FlashEditImageResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
GEMINI_25_FLASH_EDIT_IMAGE_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,2 +1,3 @@
pub mod flux_pro_kontext_max_edit_image;
pub mod gemini_25_flash_edit_image;
pub mod gpt_image_1_edit_image;

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