Compare commits

..
Author SHA1 Message Date
Tanish Manku 4ec39de8fb docs: add PageDraw visual design & architecture review
High-end design pass over the pagedraw editor styling and architecture
(lib + webapp integration). Scores each axis against a Linear/Figma-tier
app-chrome rubric and lays out a P0-P2 roadmap: token sweep to kill
hardcoded greys and unify the accent, an owned type ramp, custom-eased
micro-motion, double-bezel panel surfaces, a concentric radius scale,
z-index hygiene, and module-size/architecture notes.
2026-06-15 17:23:43 -07:00
Tanish Manku 8f26a5182c webapp(pagedraw): open insufficient-credits modal on 402
The omni-gen enqueue (and inlined bg-removal fetch) throw on non-2xx and
the JSON body isn't surfaced, so parse the HTTP status from the error
message. On 402 Payment Required, open the shared insufficient-credits
modal instead of a raw error toast — matching the create-image and
create-video generate handlers.
2026-06-15 17:18:58 -07:00
Tanish Manku 5147752c45 webapp: add "Edit on Canvas" lightbox button -> PageDraw
Open any image output in the image editor straight from the lightbox,
matching the existing Make Video / Recreate handoffs. Mirrors the Tauri
app handleEditFromGallery flow: RESET the lib store, seed the image as
base (and into history), then route to /edit-image.

Gated to images (hidden for video/3D), so it shows on create-image,
library, and task-queue lightboxes via the shared LightboxDetails.
2026-06-15 17:09:57 -07:00
Tanish Manku 8cfd579664 webapp: add /edit-image route with REST PageDraw adapter
Plug the shared @storyteller/ui-pagedraw lib into the webapp by
implementing a REST-flavoured PageDrawAdapter, mirroring how ui-pagescene
is wired into artcraft-webapp and the artcraft-website pagedraw port:

- enqueueEditImage -> OmniGenApi.generateImage, then poll the job and feed
  results back into the lib's Zustand store (addHistoryImageBundle /
  resolvePendingGeneration), keyed on frontendSubscriberId.
- enqueueBgRemoval -> upload base64 -> media token -> POST
  /v1/generate/image/remove_background -> poll -> finishRemoveBackground.
- uploadImage -> UploadImageMedia.
- renderBaseImageSelector -> webapp upload/library/blank-canvas picker.

Inpaint is intentionally stubbed; the omni REST endpoint doesn't accept a
raw mask payload yet, so we surface a clear toast until that path exists.

Wired a /edit-image route in app.tsx and an Edit Image sidebar entry.
2026-06-15 14:32:39 -07:00
BFlatandTanish Manku 9caaad5e0d Page video editor: Mount and make public (#1611)
Co-authored-by: Tanish Manku <tanish2k09.dev@gmail.com>
2026-06-08 13:01:51 -07:00
Brandon Thomas 8253e28560 folders (#1616) 2026-06-08 12:09:02 -04:00
bflatastic 7af6e5b867 Webapp: added missing seedance variant name mappings 2026-06-08 22:19:32 +07:00
bflatastic ccdcb01035 fix(webapp): clamp duration to nearest valid value on model switch instead of resetting to min 2026-06-08 21:18:23 +07:00
BFlat e3a8c04878 Webapp: OmniGen outage handling + toast fixes (#1615)
### Outage handling
- Show an error toast when the OmniGen service is unavailable (5xx)
instead of failing silently.
- Distinct messages for the two failure points: couldn't load the model
list vs. couldn't enqueue a generation.
- Model-fetch toast fires once per failed attempt (singleton fetch +
message dedup); generation toast is gated to 5xx so content-policy/4xx
rejections still just show the failed card and 402 still opens the
credits modal.
2026-06-08 20:59:50 +07:00
BFlat 3886253ffa Webapp: prompt-box fixes, in-progress recreate, model-name consistency, sticky settings (#1614)
### Prompt box
- Fix prompt box overflowing past the viewport at some desktop widths
(Generate button hidden) - removed `w-full` so it sizes to the content
area beside the sidebar (create-image + create-video).

### Recreate action
- Add **Recreate** to in-progress jobs in the create-page gallery (grid
+ list) and the task queue - previously only on completed/failed.
- Task queue had no Recreate at all; added it to all three states
(in-progress/completed/failed) for consistency.
- Recreate on the in-progress grid card sits bottom-right inline with
the prompt; in-progress list row reveals it on hover (matching completed
rows).
- Extract shared `useRecreateFromPromptToken` hook; refactor
FailedCard/FailedRow onto it (DRY).

### Model display names
- Consolidate to a single source of truth: webapp `getModelDisplayName`
now delegates to `@storyteller/model-list` (removed the duplicate local
map). Gallery, task queue, and lightbox now agree.
- Fix missing names: `gpt_image_1` (gallery) and Midjourney in the
lightbox - added `midjourney_7` / `midjourney_7_niji` / `midjourney_8`
to the model-list name + creator-icon maps; aligned `veo_2` → "Google
Veo 2".

### Sticky prompt settings across model switches
- Prompt-box settings no longer reset when switching models - the user's
choice persists and is resolved against the new model (kept if
supported, else falls back for display/generation only, without losing
the preference).
- Covers aspect ratio, count, resolution, quality (image) and aspect
ratio, resolution, count, duration, sound (video).
- Add `lib/resolve-model-setting.ts` (`resolveModelOption` /
`resolveModelCount`); `handleModelChange` now only switches the model (+
capability-bound input mode on video).
2026-06-08 19:35:20 +07:00
bflatastic c1b257b7d5 updated download links to v0.32.0 2026-06-06 07:56:31 +07:00
bflatastic b162e2bfc0 webapp: fixes to mobile sheet component on mobile 2026-06-06 07:50:07 +07:00
BFlat 3ea33dabd5 test fix 2: mobile freezing after image modal closed on mobile (#1613) 2026-06-06 07:37:15 +07:00
BFlat aee20db947 test fix: mobile freezing after image modal closed on mobile (#1612) 2026-06-06 07:20:20 +07:00
BFlat 3847a3d39c Revert "Page video editor: Mount and make public" (#1610)
Reverts storytold/artcraft#1606
2026-06-06 06:59:01 +07:00
BFlat 5747212f37 Revert "fix(ci): use pnpm instead of npm in artcraft publish workflows" (#1609)
Reverts storytold/artcraft#1608
2026-06-06 06:58:27 +07:00
Tanish Mankuandbflatastic 04ebe2c965 fix(ci): use pnpm instead of npm in artcraft publish workflows (#1608)
The frontend migrated to pnpm workspaces in #1606, which introduced
"workspace:*" dependencies. npm cannot resolve that protocol, so the
macOS and Windows publish workflows fail at "npm ci" with
EUNSUPPORTEDPROTOCOL, breaking production release builds on main.

Add a pinned pnpm setup step and replace the npm install block with
"pnpm install --frozen-lockfile". Verified the frozen install resolves
cleanly against the committed pnpm-lock.yaml.

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-06-06 06:49:16 +07:00
Tanish Mankuandbflatastic d09d44b7e2 Page video editor: Mount and make public (#1606)
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-06-05 22:21:55 +07:00
BFlat 8e754b4b8f website: landing buttons and footer cta updates (#1607)
- Renamed the "Supercharge Credits" button to **Use on Web** (globe
icon) in the hero and bottom CTA. Routes subscribers to the webapp
homepage, everyone else to `/pricing`.
- Removed the "Create image / Create video" links from both sections.
- Split Discord + GitHub into a separate "Open source &
community-driven" CTA (subtle container) above the "Ready to craft?"
CTA.
2026-06-05 14:47:30 +07:00
BFlat cbda6b7b1f fix(webapp): prevent iOS caret loss in mobile prompt input (#1605)
MentionTextarea's DOM-sync effect rewrote innerHTML under the live caret
whenever buildHTML's identity changed, detaching the cursor on iOS
Safari mid-typing. Skip the rewrite when focused and the DOM already
matches value, and memoize colorMap in MobilePromptForm to stop the
per-render churn.
2026-06-04 13:05:30 +07:00
Brandon Thomas 186ce24390 fix routing 2026-06-03 23:59:13 -04:00
Brandon Thomas 3401e709bb rename handlers + codegen fix 2026-06-03 10:57:27 -04:00
Brandon Thomas f71d344ff3 admin email changes (#1604) 2026-06-03 10:37:28 -04:00
Brandon Thomas ca43d5cbe8 grok failure update (#1603) 2026-06-02 22:00:10 -04:00
BFlat 1d8f64dd23 webapp: midjourney icon and name mappings (#1602) 2026-06-02 22:58:33 +07:00
BFlat 2a49226479 mobile view update (#1601)
- Separate mobile generations page
- Bottom mobile navbar
2026-06-02 20:36:02 +07:00
Brandon Thomas a6f8d2a4aa midjourney images - router, web, jobs, etc. (#1600) 2026-06-02 06:49:12 -04:00
Brandon Thomas 017b923c5a midjourney client (#1599) 2026-06-02 04:02:09 -04:00
Brandon Thomas f0244c6da8 analytics queries (#1598) 2026-06-01 23:55:52 -04:00
Brandon Thomas 75788d9525 kling 1.6 pro, 2.5 turbo pro, 2.6 pro -> web omni api / artcraft_router / fal_client (#1597) 2026-06-01 19:08:25 -04:00
BFlat c5dcf0e155 app styling updates (#1596)
darker tone
2026-06-01 18:17:14 +07:00
Brandon Thomas c71af2b2b0 move deprecated modules (the bulk of them) (#1595) 2026-05-31 23:59:25 -04:00
Brandon Thomas 2f808fe61e artcraft_router cleanup from old migration (#1594) 2026-05-31 23:42:50 -04:00
Brandon Thomas 0c1bb0794a auto aspect ratio for grok 2026-05-31 16:39:46 -04:00
Brandon Thomas f551a547a0 grok max length is 4096 2026-05-31 15:05:09 -04:00
Brandon Thomas 8ac4e534e1 refactor to make intention of request prereq checks clear + reusable 2026-05-31 14:40:10 -04:00
BFlat e39fe7c106 textToVideoSupported api field (#1593) 2026-06-01 01:30:24 +07:00
Brandon Thomas e3d30a677a 400 on grok ttv (#1592) 2026-05-31 14:19:13 -04:00
Brandon Thomas e8a2cbd490 text_to_video_supported=false on new grok model (#1591) 2026-05-31 13:53:02 -04:00
Brandon Thomas 0d7168c506 grok imagine video 1.5 preview (#1590) 2026-05-31 12:21:54 -04:00
bflatastic 5d4f307320 model selector width update 2026-05-30 01:03:13 +07:00
Brandon Thomas dbd723541f add prompt token to GET /v1/media_files/list/user/{username} (#1589) 2026-05-29 13:45:17 -04:00
bflatastic c589f20110 popover updates for info tooltip 2026-05-30 00:42:55 +07:00
Brandon Thomas 59da9a2a79 descriptions got switched 2026-05-29 13:38:03 -04:00
BFlat 58d4f4ff12 webapp: extra info for models (#1588) 2026-05-30 00:30:14 +07:00
Brandon Thomas 10cb0b3030 fix build 2026-05-29 13:09:46 -04:00
Brandon Thomas 55d375895c description fields on video models 2026-05-29 13:05:55 -04:00
Brandon Thomas 5dfa059523 disable characters 2026-05-28 19:46:46 -04:00
bflatastic 9598a86dcd webapp: recreate failed jobs 2026-05-29 06:40:54 +07:00
Brandon Thomas d86da55954 update model list (#1587) 2026-05-28 19:10:36 -04:00
Brandon Thomas 6f9e240b4e ultra variant backend (#1586) 2026-05-28 19:03:12 -04:00
Brandon Thomas 491715e5a6 seedance variant - BytePlus "Ultra" (#1585) 2026-05-28 16:23:56 -04:00
Brandon Thomas 6e55dc63c0 group providers together (#1584) 2026-05-28 15:14:24 -04:00
1436 changed files with 104738 additions and 6890 deletions
@@ -0,0 +1,179 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nORDER BY f.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "owner_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 14,
"name": "is_orphaned: bool",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "05a880a2ece0236f57fe1c40100cba0c0cdcad2ebee0fecb88f9043ac7b82d8c"
}
@@ -0,0 +1,179 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "owner_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 14,
"name": "is_orphaned: bool",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "0c918aacf4ce34e1e03d2f9af49dd3138cadf59a28d0917726135ed5f34cba5e"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_email_changes\nSET\n user_token = ?,\n old_email = ?,\n new_email = ?,\n ip_address = ?,\n maybe_changed_by_user_token = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "1359fb54af12d78251fe435b063771984ee00d74a71b5d29e9df267991f38522"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET maybe_cover_image_custom_media_token = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "197e4fb3822b095cbbf8127493dad91c9254b7c242b8921a8c2df9f5a0e77424"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET has_star = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "24716cfdb77eee365bd605a40cecb02b3a27a90725dbe96e3865545d14839ab8"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_type,\n mf.media_class,\n mf.maybe_mime_type,\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n mf.maybe_duration_millis,\n mf.maybe_title\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY fmf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "membership_id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "added_to_folder_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 2,
"name": "media_file_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "media_type",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "media_class",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "maybe_mime_type",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 9,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 10,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 11,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 12,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
true,
true,
true,
true,
true,
true
]
},
"hash": "491980e0a555a900354e60525947d77548a6cb8e4305d13886c8ad7e6787476c"
}
@@ -0,0 +1,58 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_creator_user_token as `maybe_creator_user_token: UserToken`,\n media_class as `media_class: MediaFileClass`,\n media_type as `media_type: MediaFileType`,\n maybe_cover_image_media_file_token as `maybe_cover_image_media_file_token: MediaFileToken`\nFROM media_files\nWHERE token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "maybe_creator_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "media_class: MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 2,
"name": "media_type: MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "maybe_cover_image_media_file_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true
]
},
"hash": "5606a694c872d868ab3b35f4b3685f86f2b27ad16e741c8b1bf997b613f1b803"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n uec.id as `id: u64`,\n uec.old_email,\n uec.new_email,\n uec.ip_address,\n uec.created_at,\n\n u_subject.token as `user_token: tokens::tokens::users::UserToken`,\n u_subject.username as user_username,\n u_subject.display_name as user_display_name,\n u_subject.email_gravatar_hash as user_gravatar_hash,\n\n u_changer.token as `maybe_changed_by_user_token?: tokens::tokens::users::UserToken`,\n u_changer.username as `maybe_changed_by_user_username?`,\n u_changer.display_name as `maybe_changed_by_user_display_name?`,\n u_changer.email_gravatar_hash as `maybe_changed_by_user_gravatar_hash?`\n\nFROM user_email_changes AS uec\nJOIN users AS u_subject\n ON uec.user_token = u_subject.token\nLEFT JOIN users AS u_changer\n ON uec.maybe_changed_by_user_token = u_changer.token\nWHERE uec.user_token = ?\n AND uec.id < ?\nORDER BY uec.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "old_email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 2,
"name": "new_email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 4,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 5,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "user_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "user_display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 8,
"name": "user_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_changed_by_user_token?: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_changed_by_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_changed_by_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 12,
"name": "maybe_changed_by_user_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true
]
},
"hash": "5611678b6c168208ea4da9ef0e3071758c9bb8d89b54e2d3f0fc84bd0a6c915f"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET maybe_color_code = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "6a0e3f012795068a56a0dfaf567d066dc157bb06c736c1c4314b18c8e0356127"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nWITH RECURSIVE ancestor_chain (token, parent_token, depth) AS (\n SELECT token, maybe_parent_folder_token, 0\n FROM folders\n WHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\n UNION ALL\n SELECT f.token, f.maybe_parent_folder_token, ac.depth + 1\n FROM folders f\n INNER JOIN ancestor_chain ac\n ON f.token = ac.parent_token\n WHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND ac.depth < ?\n)\nSELECT token as `token!: FolderToken`\nFROM ancestor_chain\nWHERE depth > 0\nORDER BY depth ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token!: FolderToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
true
]
},
"hash": "780891e8294b78cc0e5a05a8b5a8949a763bcb8c5d6526a621234020b154bdc9"
}
@@ -0,0 +1,168 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at\nFROM folders f\nWHERE f.maybe_parent_folder_token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\nORDER BY f.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "owner_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "a13680fb2b54809b6d9d9356f8eec87cd0f1f37405b84bbf734ed06cc244efb1"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO folders\nSET\n token = ?,\n name = ?,\n owner_user_token = ?,\n maybe_parent_folder_token = ?,\n maybe_color_code = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "c099b274026619760f0982f0e5082da1e8de16c26122f2750667b9cdc2c4abf3"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n uec.id as `id: u64`,\n uec.old_email,\n uec.new_email,\n uec.ip_address,\n uec.created_at,\n\n u_subject.token as `user_token: tokens::tokens::users::UserToken`,\n u_subject.username as user_username,\n u_subject.display_name as user_display_name,\n u_subject.email_gravatar_hash as user_gravatar_hash,\n\n u_changer.token as `maybe_changed_by_user_token?: tokens::tokens::users::UserToken`,\n u_changer.username as `maybe_changed_by_user_username?`,\n u_changer.display_name as `maybe_changed_by_user_display_name?`,\n u_changer.email_gravatar_hash as `maybe_changed_by_user_gravatar_hash?`\n\nFROM user_email_changes AS uec\nJOIN users AS u_subject\n ON uec.user_token = u_subject.token\nLEFT JOIN users AS u_changer\n ON uec.maybe_changed_by_user_token = u_changer.token\nWHERE uec.user_token = ?\nORDER BY uec.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "old_email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 2,
"name": "new_email",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 4,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 5,
"name": "user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "user_username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 7,
"name": "user_display_name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 8,
"name": "user_gravatar_hash",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_changed_by_user_token?: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_changed_by_user_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 11,
"name": "maybe_changed_by_user_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 12,
"name": "maybe_changed_by_user_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true
]
},
"hash": "c17e1d6b567dd378bb4cb0478fb9558e907a0f36498bd049c7c526e496e89259"
}
@@ -0,0 +1,179 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at,\n (\n f.maybe_parent_folder_token IS NOT NULL\n AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)\n ) as `is_orphaned: bool`\nFROM folders f\nLEFT JOIN folders p\n ON p.token = f.maybe_parent_folder_token\nWHERE f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND f.id < ?\nORDER BY f.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "owner_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 14,
"name": "is_orphaned: bool",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "d6f0c65abce2da20fe86805ff053b8c2569bf0095f6245a5529f0c71854a604b"
}
@@ -0,0 +1,168 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n f.id as `id: u64`,\n f.token as `token: FolderToken`,\n f.name,\n f.owner_user_token as `owner_user_token: UserToken`,\n f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,\n f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,\n f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,\n f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,\n f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,\n f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,\n f.maybe_color_code,\n f.has_star as `has_star: bool`,\n f.created_at,\n f.updated_at\nFROM folders f\nWHERE f.maybe_parent_folder_token = ?\n AND f.owner_user_token = ?\n AND f.maybe_deleted_at IS NULL\n AND f.id < ?\nORDER BY f.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "owner_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "maybe_parent_folder_token: FolderToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "maybe_last_media_file_token_1: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_last_media_file_token_2: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_last_media_file_token_3: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_last_media_file_token_4: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_cover_image_custom_media_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 10,
"name": "maybe_color_code",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "has_star: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 13,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "d85c1a844f14b9416c6260d7a906fec709b50eb4583b98dffb52ebbe6abcb7d5"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET name = ?\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "e32e195ef41bc833e5b7fc53cfde8669f88aeb66d279e5213d047f4860ec86ab"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n fmf.id as `membership_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_type,\n mf.media_class,\n mf.maybe_mime_type,\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n mf.maybe_duration_millis,\n mf.maybe_title\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND fmf.id < ?\nORDER BY fmf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "membership_id: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNIQUE_KEY | UNSIGNED | AUTO_INCREMENT",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 1,
"name": "added_to_folder_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 2,
"name": "media_file_token: MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "media_type",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 4,
"name": "media_class",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "maybe_mime_type",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 9,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 10,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
}
},
{
"ordinal": 11,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 12,
"name": "maybe_title",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
false,
true,
false,
true,
true,
true,
true,
true,
true
]
},
"hash": "ea5f5e5b3caf990e9c25a3e8fe2e9625c12d6a36dd5dd91fc4615a45acdd33b8"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE folders\nSET maybe_deleted_at = NOW()\nWHERE token = ?\n AND owner_user_token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "f9e3ff8ca682948076b799d6b2d934be4a87a4e04665d6f12cc32095494129ad"
}
Generated
+1
View File
@@ -809,6 +809,7 @@ dependencies = [
"seedance2pro_client",
"serde",
"serde_derive",
"serde_json",
"strum",
"test_data",
"tokens",
@@ -0,0 +1,23 @@
-- Daily ArtCraft generations broken down by model_type since January 1st, 2026.
-- One row per (day, model_type) pair.
SELECT
DATE(created_at) AS day,
maybe_model_type AS model_type,
COUNT(*) AS generations
FROM generic_inference_jobs
WHERE created_at >= '2026-01-01'
AND maybe_model_type IS NOT NULL
AND job_type NOT IN (
'gpt_sovits',
'tacotron2',
'rvc_v2',
'f5_tts',
'so_vits_svc',
'styletts2',
'face_fusion',
'seed_vc',
'comfy_ui'
)
GROUP BY day, model_type
ORDER BY day DESC, generations DESC;
@@ -0,0 +1,53 @@
select
u.created_at,
CASE
WHEN u.maybe_landing_url LIKE '%?r=%' THEN 'user_referral'
WHEN u.maybe_landing_url LIKE '%gad_source%' THEN 'google_ads'
WHEN u.maybe_landing_url LIKE '%utm_source=fakeyou%' THEN 'fakeyou'
WHEN u.maybe_landing_url LIKE '%utm_source=storyteller%' THEN 'storyteller'
WHEN u.maybe_landing_url LIKE '%utm_source=artcraftai%' THEN 'artcraft_dot_ai_domain'
WHEN u.maybe_referral_url LIKE '%instagram%' THEN 'instagram'
WHEN u.maybe_referral_url LIKE '%reddit%' THEN 'reddit'
WHEN u.maybe_referral_url LIKE '%google%' THEN 'google'
WHEN u.maybe_referral_url LIKE '%brave%' THEN 'brave'
WHEN u.maybe_referral_url LIKE '%duckduckgo%' THEN 'duckduckgo'
WHEN u.maybe_referral_url LIKE '%yahoo.com%' THEN 'yahoo'
WHEN u.maybe_referral_url LIKE '%yandex%' THEN 'yandex'
WHEN u.maybe_referral_url LIKE '%bing.com%' THEN 'bing'
WHEN u.maybe_referral_url LIKE '%github.com%' THEN 'github'
WHEN u.maybe_referral_url LIKE '%chatgpt%' THEN 'chatgpt'
WHEN u.maybe_referral_url LIKE '%https://t.co%' THEN 'twitter'
WHEN u.maybe_referral_url LIKE '%x.com%' THEN 'twitter'
WHEN u.maybe_referral_url LIKE '%youtube%' THEN 'youtube'
WHEN u.maybe_referral_url LIKE '%fakeyou.com%' THEN 'fakeyou'
WHEN u.maybe_referral_url LIKE '%ycombinator%' THEN 'hackernews'
WHEN u.maybe_referral_url LIKE '%seedanceprice%' THEN 'seedanceprice_reddit'
WHEN u.maybe_landing_url LIKE '%utm_source=ig%' THEN 'instagram'
WHEN u.maybe_landing_url LIKE '%utm_source=chatgpt%' THEN 'chatgpt'
WHEN u.maybe_landing_url LIKE '%/media/%' THEN 'reddit'
ELSE 'unknown'
END AS user_source,
u.maybe_referral_url,
u.maybe_landing_url,
u.username,
us.maybe_stripe_customer_id as subscription_customer,
l.stripe_customer_id as credits_customer,
u.email_address,
CASE
WHEN u.maybe_source LIKE '%artcraft_get_web%' THEN 'website'
WHEN u.maybe_source LIKE '%artcraft%' THEN 'app_or_website'
WHEN u.maybe_source LIKE '%artcraft_get_s%' THEN 'website_stripe_checkout'
ELSE u.maybe_source
END AS maybe_source,
u.maybe_signup_method,
u.created_at,
u.updated_at
from users u
left outer join user_subscriptions us
on us.user_token = u.token
and us.subscription_namespace = 'artcraft'
left outer join user_stripe_customer_links l
on l.user_token = u.token
and l.payments_namespace = 'artcraft'
where (us.maybe_stripe_customer_id IS NOT NULL OR l.stripe_customer_id IS NOT NULL)
order by u.created_at desc;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_email_changes;
@@ -0,0 +1,35 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE user_email_changes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The user whose email was changed.
user_token VARCHAR(32) NOT NULL,
-- The email address before the change.
old_email VARCHAR(255) NOT NULL,
-- The email address after the change.
new_email VARCHAR(255) NOT NULL,
-- The IP address the change was made from.
-- Wide enough for IPv4/6.
ip_address VARCHAR(40) NOT NULL,
-- The user that performed the change, if known. NULL when the change was
-- performed by the user themselves, by an automated process, or in any
-- other context where there is no acting user.
maybe_changed_by_user_token VARCHAR(32) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
KEY index_user_token (user_token),
KEY index_old_email (old_email),
KEY index_new_email (new_email),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS folders;
@@ -0,0 +1,56 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE folders (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC, used by the API).
token VARCHAR(32) NOT NULL,
-- User-supplied folder name.
name VARCHAR(255) NOT NULL,
-- The user that owns this folder.
owner_user_token VARCHAR(32) NOT NULL,
-- Parent folder for nesting, NULL for top-level folders.
maybe_parent_folder_token VARCHAR(32) DEFAULT NULL,
-- The last four media files added to the folder, in order. These are
-- denormalized for cheap thumbnail-grid rendering on folder cards
-- without a separate fan-out query.
maybe_last_media_file_token_1 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_2 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_3 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_4 VARCHAR(32) DEFAULT NULL,
-- Optional user-chosen cover image (a media file). Overrides the
-- auto-grid thumbnail derived from the last-N media files above.
maybe_cover_image_custom_media_token VARCHAR(32) DEFAULT NULL,
-- Optional user-chosen color tag. Wide enough for "#RRGGBBAA" plus a
-- short named-color fallback.
maybe_color_code VARCHAR(16) DEFAULT NULL,
-- Whether the user has starred / favorited this folder.
has_star BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Soft-delete timestamp. NULL means the folder is live.
maybe_deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY fk_owner_user_token (owner_user_token),
KEY fk_maybe_parent_folder_token (maybe_parent_folder_token),
KEY index_folders_created_at (created_at),
KEY index_folders_updated_at (updated_at),
KEY index_maybe_deleted_at (maybe_deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS folder_media_files;
@@ -0,0 +1,27 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE folder_media_files (
-- Auto-increment for posterity / row-creation ordering. Not the primary
-- key — the composite (folder_token, media_file_token) is. AUTO_INCREMENT
-- requires its own key, hence the UNIQUE KEY below.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The folder this membership belongs to.
folder_token VARCHAR(32) NOT NULL,
-- The media file in the folder.
media_file_token VARCHAR(32) NOT NULL,
-- When the media file was added to the folder. Rows are hard-deleted
-- when a media file is removed, so there is no `maybe_deleted_at`.
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (folder_token, media_file_token),
UNIQUE KEY (id),
KEY index_folder_token (folder_token),
KEY index_media_file_token (media_file_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,32 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- N:M mapping between `folders` and `media_files`. Rows are hard-deleted
-- when a media file is removed from a folder — no soft-delete column.
CREATE TABLE folder_media_files (
-- Auto-increment for posterity / row-creation ordering. Not the primary
-- key — the composite (folder_token, media_file_token) is. AUTO_INCREMENT
-- requires its own key, hence the UNIQUE KEY below.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The folder this membership belongs to.
folder_token VARCHAR(32) NOT NULL,
-- The media file in the folder.
media_file_token VARCHAR(32) NOT NULL,
-- When the media file was added to the folder. Rows are hard-deleted
-- when a media file is removed, so there is no `maybe_deleted_at`.
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (folder_token, media_file_token),
UNIQUE KEY (id),
KEY index_folder_token (folder_token),
KEY index_media_file_token (media_file_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,59 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE folders (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (PUBLIC, used by the API).
token VARCHAR(32) NOT NULL,
-- User-supplied folder name.
name VARCHAR(255) NOT NULL,
-- The user that owns this folder.
owner_user_token VARCHAR(32) NOT NULL,
-- Parent folder for nesting, NULL for top-level folders.
maybe_parent_folder_token VARCHAR(32) DEFAULT NULL,
-- The last four media files added to the folder, in order. These are
-- denormalized for cheap thumbnail-grid rendering on folder cards
-- without a separate fan-out query.
maybe_last_media_file_token_1 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_2 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_3 VARCHAR(32) DEFAULT NULL,
maybe_last_media_file_token_4 VARCHAR(32) DEFAULT NULL,
-- Optional user-chosen cover image (a media file). Overrides the
-- auto-grid thumbnail derived from the last-N media files above.
maybe_cover_image_custom_media_token VARCHAR(32) DEFAULT NULL,
-- Optional user-chosen color tag. Wide enough for "#RRGGBBAA" plus a
-- short named-color fallback.
maybe_color_code VARCHAR(16) DEFAULT NULL,
-- Whether the user has starred / favorited this folder.
has_star BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Soft-delete timestamp. NULL means the folder is live.
maybe_deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY fk_owner_user_token (owner_user_token),
KEY fk_maybe_parent_folder_token (maybe_parent_folder_token),
KEY index_folders_created_at (created_at),
KEY index_folders_updated_at (updated_at),
KEY index_maybe_deleted_at (maybe_deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,38 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE user_email_changes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The user whose email was changed.
user_token VARCHAR(32) NOT NULL,
-- The email address before the change.
old_email VARCHAR(255) NOT NULL,
-- The email address after the change.
new_email VARCHAR(255) NOT NULL,
-- The IP address the change was made from.
-- Wide enough for IPv4/6.
ip_address VARCHAR(40) NOT NULL,
-- The user that performed the change, if known. NULL when the change was
-- performed by the user themselves, by an automated process, or in any
-- other context where there is no acting user.
maybe_changed_by_user_token VARCHAR(32) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
KEY index_user_token (user_token),
KEY index_old_email (old_email),
KEY index_new_email (new_email),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+203
View File
@@ -0,0 +1,203 @@
# PageDraw — Visual Design & Architecture Review
**Surface:** `@storyteller/ui-pagedraw` image editor + its `artcraft-webapp` integration (`/edit-image`).
**Lens:** Awwwards / Linear-tier "premium app chrome" rubric, adapted for an in-product creative tool.
**Date:** 2026-06-15 · **Branch:** `worktree-pagedraw-webapp`
---
## 0. How to read this
PageDraw is a **professional canvas tool**, not a marketing page. So the elite-design rubric is
applied in its *app-chrome* register — the north star is **Figma / Linear / Photoshop-web**, not an
Awwwards landing page. That means:
- **Applies:** token discipline, concentric radii, hairline + inset-highlight surfaces ("double-bezel"),
island/pill controls, custom-eased micro-motion, z-index hygiene, premium type ramp.
- **Does _not_ apply:** `py-40` macro-whitespace, hero typography, editorial-split layouts, scroll-reveal
entrances. A tool surface is dense and persistent by design; forcing landing-page whitespace would hurt it.
Citations are `file:line` against the current tree.
---
## 1. Verdict
| Axis | Score | One-liner |
|---|---|---|
| Color system | ⚠️ 5/10 | Good token foundation, but hardcoded greys (`#1b1b1b`, `#303030`, `#404040`) leak through a *themeable lib*. |
| Typography | ❌ 3/10 | No type ramp, no font-family ownership; rides whatever the host sets. Banned-by-rubric default stack risk. |
| Radii | ⚠️ 4/10 | Five radius scales in play (`sm`→`2xl`→`full`) with no concentric nesting. |
| Surfaces (border/shadow) | ⚠️ 5/10 | Flat `shadow-lg` everywhere; no inset highlights; no double-bezel. Reads "functional", not "machined". |
| Motion | ❌ 3/10 | Only `transition-colors`/`ease-in-out`, durations 75–200ms; **zero custom easing**, no spring, no entrance choreography. |
| Layout / z-index | ⚠️ 6/10 | Clean fixed-panel composition, but a `z-[9999999]` escape hatch and a 6-tier ad-hoc z-stack. |
| Icons | ✅ 7/10 | FontAwesome Pro (solid + regular) — acceptable, but solid weight is heavier than the rubric's "ultra-light line" ideal. |
| Architecture | ⚠️ 5/10 | Clean adapter boundary; but `PaintSurface.tsx` (2005 LOC), `SceneState.ts` (1539), `PageDraw.tsx` (1292) are oversized, and the lib hardcodes host-theme colors. |
**Bottom line:** the editor is *structurally competent and functional* but visually reads as
"capable internal tool," not "$150k product surface." The single highest-leverage fix is a **shared
design-token layer** (color + radius + motion + type), because today the lib both (a) reaches for
host tokens (`bg-ui-panel`, `text-base-fg`) *and* (b) hardcodes its own greys — so it can never be
fully reskinned by the webapp vs. the desktop app.
---
## 2. What's already good (keep)
- **Token-aware base.** Most surfaces use semantic tokens (`bg-ui-panel`, `bg-ui-controls`,
`text-base-fg`, `border-ui-panel-border`) — the right instinct (SideToolbar, PromptEditor, HistoryStack).
- **Clean platform boundary.** The lib has zero REST/Tauri imports; everything I/O flows through
`PageDrawAdapter` (`adapter.ts`). The webapp adapter (`web-adapter.tsx`) and desktop adapter stay
swappable. This is genuinely good architecture and should be protected.
- **Performance-minded canvas.** Composite pre-baking on idle (`PageDraw.tsx` ~661–710) and worker
encoding (`generatePipeline.worker.ts`) keep the generate click off the main thread. Don't regress this.
- **CORS already handled** for browser canvas export (`crossOrigin="anonymous"` in `Node.ts:134`,
`SceneState.ts:1473`, `imageHelpers.ts:16`) — the web port works because of this.
---
## 3. Findings & recommendations
### 3.1 Color — stop hardcoding greys in a themeable lib ⚠️ P0
**Current:** semantic tokens *and* raw hex coexist:
- `#1b1b1b` pegboard (`pagedraw.css:6`), `#1A1A1A`/`#333333` context menu (`ContextMenu.tsx:44`),
`#303030`/`#404040` dividers + popovers (`SideToolbar.tsx:381,550`), `bg-zinc-700/600` swatch
(`SideToolbar.tsx:173`), plus ad-hoc `bg-blue-600/500` accents (`PageDraw.tsx:157`, base selector).
- Accent color is inconsistent: the lib's primary action uses raw **blue-600**, while the webapp's
design system uses `--primary` / `brand-primary`. So the editor's blue doesn't match the product.
**Why it fails the bar:** a premium component library exposes *one* surface ramp and *one* accent, both
driven by tokens, so it reskins per host. Hardcoded greys mean the desktop and web themes can drift, and
the raw `blue-600` clashes with `brand-primary` used elsewhere in the webapp.
**Do:**
- Replace every raw grey with the existing ramp: `#1b1b1b`→`bg-ui-background`, `#1A1A1A`→`bg-ui-panel`,
`#303030`/`#404040`→`border-ui-panel-border` / `bg-ui-controls`. For the pegboard, add a token
`--st-canvas` so hosts can theme the canvas backdrop.
- Replace `blue-600/500` accents with `bg-primary` / `text-primary` (the token the rest of the webapp uses),
so the Generate/Edit-3D buttons match `brand-primary`.
- Move the react-colorful overrides off pure `rgb(255,255,255)` borders (`pagedraw.css:15,19`) onto
`white/15` hairlines for consistency with the rest of the chrome.
### 3.2 Typography — own a type ramp ❌ P0
**Current:** no `font-family` anywhere in the lib (grep: 0 hits); only `text-xs/sm/base/lg/xl` +
`font-medium/semibold/bold`. The editor inherits whatever the host's body font is.
**Why it fails the bar:** the rubric explicitly bans the default web stack (Inter/Roboto/Arial/Helvetica).
Inheriting an unspecified stack means the tool can render in a banned font with no recourse, and there's
no deliberate hierarchy (numerics in sliders, labels, modal titles all share one undifferentiated ramp).
**Do:**
- Adopt the product's display/grotesk token (e.g. a `font-sans` mapped to Geist / Plus Jakarta Sans at
the app level) and reference it in the lib's root container, not per-component.
- Define a small semantic ramp: `text-[11px] tracking-[0.16em] uppercase` for toolbar/section labels,
`text-sm` for controls, `text-base font-semibold` for modal titles. Apply `tabular-nums` to slider
read-outs (`SliderWithIndicator.tsx`) and dimension fields (`BlankCanvasModal`) so numbers don't jitter.
### 3.3 Radii — collapse to a concentric scale ⚠️ P1
**Current:** `rounded-sm / md / lg / xl / 2xl / full` all appear, with no parent→child relationship
(e.g. `rounded-2xl` card at `PageDraw.tsx:1123`, `rounded-xl` panels in SideToolbar, `rounded-lg`
buttons in InpaintToolBar).
**Do:** pick a 3-step system — **`rounded-2xl` shells, `rounded-xl` inner cards, `rounded-lg` controls,
`rounded-full` pills** — and apply concentrically (the "double-bezel" curve math: inner radius =
outer − padding). Drop `rounded-sm`/`rounded-md` from the editor entirely.
### 3.4 Surfaces — add the "machined" double-bezel ⚠️ P1
**Current:** panels are a single `bg-ui-panel` + `border` + `shadow-lg`. No nested enclosure, no inset
top-highlight. Reads flat.
**Do (for the four floating panels — SideToolbar, HistoryStack, PromptEditor, InpaintToolBar):**
- Outer shell: `bg-ui-panel/80 ring-1 ring-white/10 backdrop-blur-xl p-1.5 rounded-2xl`.
- Inner core: own bg + `shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] rounded-xl`.
- This is the single change that most moves the surface from "tool" to "premium hardware." The panels are
already `fixed` (`PageDraw.tsx:1144,1175,1195`), so `backdrop-blur` here is GPU-safe per the perf rules.
### 3.5 Motion — replace linear/ease with spring-eased micro-motion ❌ P0 (cheap, high impact)
**Current:** `transition-colors`/`transition-all` with `ease-in-out` and `duration-75/150/200`
(`SideToolbar.tsx:381,550`, `SliderWithIndicator.tsx:65`, `ContextMenu.tsx:53`). No custom easing,
no entrance/exit on popovers, color pickers, or the context menu (they pop instantly).
**Why it fails the bar:** instant state changes and `ease-in-out` are explicitly banned. A pro tool still
deserves *interpolated* motion — it's what separates Figma from a jQuery plugin.
**Do:**
- Standardize on one curve: `transition-[transform,opacity,background-color] duration-300
ease-[cubic-bezier(0.32,0.72,0,1)]`.
- Tool buttons: `active:scale-[0.97]` press physics; selected tool gets a spring-eased indicator slide
rather than a hard background swap.
- Popovers / color picker / context menu: animate in with `opacity-0 translate-y-1 scale-[0.98]` →
resolved, ~180ms. Transform+opacity only (perf rule).
- History tiles: stagger their `opacity` reveal when a generation lands (they already use
`transition-opacity`, just add per-index delay).
### 3.6 Layout & z-index — tame the stack ⚠️ P1
**Current:** clean fixed-panel composition, but z-index is ad-hoc across 6 tiers and includes a
`z-[9999999]` on the context menu (`ContextMenu.tsx:44`) and a one-off `z-[60]` (`PageDraw.tsx:237`).
**Do:** define a named scale in one place — `canvas:0, chrome:10, floating-controls:20, overlay-button:40,
menu:50` — and replace the magic numbers. The portal menu only needs to clear the chrome, not `9999999`.
### 3.7 Responsive — declare intent ⚠️ P2
**Current:** essentially desktop-only; fixed sidebars assume a wide viewport; only LightboxDetails has
`sm:` variants. The webapp wrapper scopes the lib correctly (`pagedraw.tsx` `translateZ(0)` containing
block) but there's no mobile story.
**Do:** mirror the Edit-3D page's explicit **mobile gate** (`pagescene.tsx` `MobileGate`) on `/edit-image`
rather than letting fixed toolbars overlap on touch. A precision canvas tool is legitimately desktop-first;
just say so deliberately instead of degrading silently.
---
## 4. Architecture notes
- **Oversized modules.** `PaintSurface.tsx` (2005 LOC), `SceneState.ts` (1539), `PageDraw.tsx` (1292).
These are review/maintenance hotspots. Suggest extracting from `SceneState.ts`: history/undo slice,
base-image-load slice, and generation/pending slice into composable Zustand slices. `PageDraw.tsx`'s
generate-flow (cases 1–3, ~780–868) could move to a `useGenerate()` hook.
- **Theme ownership.** Per §3.1, the lib should *only* consume tokens. Today it mixes tokens + hex, which
defeats the adapter pattern's promise (one component, many themed hosts).
- **Adapter surface is healthy.** `PageDrawAdapter` is small and intentional; the web vs. Tauri split is
clean. The webapp's `enqueueInpaint` stub and 402→credits-modal handling are explicit and documented.
- **Icon weight.** FontAwesome **solid** is the heaviest option; if a line/light set is available
(Phosphor/Remix or FA light), the toolbar would read more refined. Low priority.
---
## 5. Prioritized roadmap
| P | Item | Effort | Payoff |
|---|---|---|---|
| **P0** | Token sweep: kill hardcoded greys; unify accent on `--primary` (§3.1) | M | Highest — enables true theming + product-consistent accent |
| **P0** | Own a font-family + semantic type ramp at the lib root (§3.2) | S | High — removes banned-font risk, adds hierarchy |
| **P0** | One custom easing curve + press/popover micro-motion (§3.5) | S | High — cheapest "feels expensive" win |
| **P1** | Double-bezel + inset highlights on the 4 floating panels (§3.4) | M | High — "machined" surface upgrade |
| **P1** | Concentric 3-step radius system (§3.3) | S | Medium — visual coherence |
| **P1** | Named z-index scale; remove `z-[9999999]` (§3.6) | S | Medium — maintainability |
| **P2** | Explicit mobile gate on `/edit-image` (§3.7) | S | Medium — correctness on touch |
| **P2** | Split `SceneState.ts` / `PaintSurface.tsx` into slices (§4) | L | Medium — maintainability |
---
## 6. Appendix — proposed token additions
```css
/* canvas + chrome surface ramp (lib should consume only these) */
--st-canvas: <theme>; /* replaces #1b1b1b pegboard */
--st-accent: var(--primary); /* replaces blue-600/500 */
--st-hairline: rgb(255 255 255 / 0.10);
--st-inset-hi: inset 0 1px 0 rgb(255 255 255 / 0.08);
/* motion */
--st-ease: cubic-bezier(0.32, 0.72, 0, 1);
--st-dur-fast: 180ms;
--st-dur: 300ms;
/* radius scale (concentric) */
--st-r-shell: 1.25rem; /* rounded-2xl */
--st-r-card: 0.875rem; /* rounded-xl */
--st-r-control: 0.5rem; /* rounded-lg */
```
> Scope note: every recommendation here is **styling/architecture only** — none change the generate,
> polling, upload, bg-removal, or adapter contracts validated during the functional debug pass.
@@ -0,0 +1,54 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
/// Canonical wire shape for a folder. Used by single-folder GETs, create
/// responses, list rows, and subfolder list rows.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct FolderInfo {
pub token: FolderToken,
pub name: String,
pub owner_user_token: UserToken,
pub maybe_parent_folder_token: Option<FolderToken>,
pub maybe_last_media_file_token_1: Option<MediaFileToken>,
pub maybe_last_media_file_token_2: Option<MediaFileToken>,
pub maybe_last_media_file_token_3: Option<MediaFileToken>,
pub maybe_last_media_file_token_4: Option<MediaFileToken>,
pub maybe_cover_image_custom_media_token: Option<MediaFileToken>,
pub maybe_color_code: Option<String>,
pub has_star: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
/// True when `maybe_parent_folder_token` is set but the referenced
/// parent row is missing or soft-deleted.
pub is_orphaned: bool,
}
/// Canonical wire shape for a media file inside a folder. Lean enough for
/// thumbnail rendering; callers that need richer fields should hit the
/// media-file batch-get endpoint.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct FolderMediaFileInfo {
pub media_file_token: MediaFileToken,
pub added_to_folder_at: DateTime<Utc>,
pub media_type: String,
pub media_class: String,
pub maybe_mime_type: Option<String>,
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_frame_width: Option<i32>,
pub maybe_frame_height: Option<i32>,
pub maybe_duration_millis: Option<i32>,
pub maybe_title: Option<String>,
}
@@ -0,0 +1,116 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::folders::common::FolderInfo;
// ── POST /v1/folders/create ──
#[derive(Deserialize, ToSchema)]
pub struct CreateFolderRequest {
pub name: String,
pub maybe_parent_folder_token: Option<FolderToken>,
pub maybe_color_code: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct CreateFolderSuccessResponse {
pub success: bool,
pub folder: FolderInfo,
}
// ── GET /v1/folders/list_all ──
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListFoldersQueryParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Serialize, ToSchema)]
pub struct ListFoldersSuccessResponse {
pub success: bool,
pub folders: Vec<FolderInfo>,
pub maybe_cursor: Option<String>,
}
// ── GET /v1/folders/folder/{folder_token} ──
#[derive(Deserialize, ToSchema)]
pub struct FolderPathInfo {
pub folder_token: FolderToken,
}
#[derive(Serialize, ToSchema)]
pub struct GetFolderSuccessResponse {
pub success: bool,
pub folder: FolderInfo,
}
// ── PUT /v1/folders/folder/{folder_token}/rename ──
#[derive(Deserialize, ToSchema)]
pub struct RenameFolderRequest {
pub new_name: String,
}
#[derive(Serialize, ToSchema)]
pub struct RenameFolderSuccessResponse {
pub success: bool,
}
// ── PUT /v1/folders/folder/{folder_token}/star ──
#[derive(Deserialize, ToSchema)]
pub struct SetFolderStarRequest {
pub has_star: bool,
}
#[derive(Serialize, ToSchema)]
pub struct SetFolderStarSuccessResponse {
pub success: bool,
}
// ── PUT /v1/folders/folder/{folder_token}/color_code ──
#[derive(Deserialize, ToSchema)]
pub struct SetFolderColorCodeRequest {
/// `None` clears the color tag.
pub maybe_color_code: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct SetFolderColorCodeSuccessResponse {
pub success: bool,
}
// ── PUT /v1/folders/folder/{folder_token}/cover_image ──
#[derive(Deserialize, ToSchema)]
pub struct SetFolderCoverImageRequest {
/// `None` clears the cover image. If set, the server resolves it to a
/// usable cover-image media file token: image/video media files are
/// used directly; other types fall back to the file's own
/// `maybe_cover_image_media_file_token` (and fail if neither path
/// applies).
pub maybe_media_file_token: Option<MediaFileToken>,
}
#[derive(Serialize, ToSchema)]
pub struct SetFolderCoverImageSuccessResponse {
pub success: bool,
/// The media file token that ended up being stored as the cover image
/// (may differ from the requested token if the server resolved through
/// the file's own cover image). `None` when the cover was cleared.
pub maybe_resolved_cover_media_file_token: Option<MediaFileToken>,
}
// ── DELETE /v1/folders/folder/{folder_token} ──
#[derive(Serialize, ToSchema)]
pub struct DeleteFolderSuccessResponse {
pub success: bool,
}
@@ -0,0 +1,59 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::folders::FolderToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::folders::common::FolderMediaFileInfo;
// ── GET /v1/folders/media_files/{folder_token} ──
#[derive(Deserialize, ToSchema)]
pub struct FolderMediaFilesPathInfo {
pub folder_token: FolderToken,
}
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListFolderMediaFilesQueryParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Serialize, ToSchema)]
pub struct ListFolderMediaFilesSuccessResponse {
pub success: bool,
pub media_files: Vec<FolderMediaFileInfo>,
pub maybe_cursor: Option<String>,
}
// ── PUT /v1/folders/media_files/{folder_token}/bulk_add ──
#[derive(Deserialize, ToSchema)]
pub struct BulkAddFolderMediaFilesRequest {
/// Media files to add to the folder. Tokens that don't exist or are
/// soft-deleted are silently skipped. Already-added rows are no-ops
/// (idempotent via INSERT IGNORE on the composite primary key).
pub media_file_tokens: Vec<MediaFileToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkAddFolderMediaFilesSuccessResponse {
pub success: bool,
/// Tokens that pre-existed (existed + not deleted). Already-added rows
/// are included since the operation is idempotent.
pub accepted_media_file_tokens: Vec<MediaFileToken>,
}
// ── PUT /v1/folders/media_files/{folder_token}/bulk_remove ──
#[derive(Deserialize, ToSchema)]
pub struct BulkRemoveFolderMediaFilesRequest {
pub media_file_tokens: Vec<MediaFileToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkRemoveFolderMediaFilesSuccessResponse {
pub success: bool,
/// How many membership rows were actually hard-deleted.
pub removed_count: u64,
}
@@ -0,0 +1,4 @@
pub mod common;
pub mod folder;
pub mod media_files;
pub mod subfolder;
@@ -0,0 +1,61 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use tokens::tokens::folders::FolderToken;
use crate::folders::common::FolderInfo;
// ── GET /v1/folders/subfolders/{folder_token} ──
#[derive(Deserialize, ToSchema)]
pub struct SubfolderPathInfo {
pub folder_token: FolderToken,
}
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListSubfoldersQueryParams {
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Serialize, ToSchema)]
pub struct ListSubfoldersSuccessResponse {
pub success: bool,
pub subfolders: Vec<FolderInfo>,
pub maybe_cursor: Option<String>,
}
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_add ──
#[derive(Deserialize, ToSchema)]
pub struct BulkAddSubfoldersRequest {
/// The folders to reparent under the URL `folder_token`. Tokens that
/// don't exist, aren't owned by the caller, are soft-deleted, or are
/// the parent itself are silently skipped (idempotent).
pub subfolder_tokens: Vec<FolderToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkAddSubfoldersSuccessResponse {
pub success: bool,
/// Tokens that were actually accepted (existed + owned + not deleted +
/// not the parent itself). Already-parented tokens are included here
/// since the operation is idempotent.
pub accepted_subfolder_tokens: Vec<FolderToken>,
}
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_remove ──
#[derive(Deserialize, ToSchema)]
pub struct BulkRemoveSubfoldersRequest {
/// Folders to unparent. Only rows currently parented to the URL
/// `folder_token` are affected; others are silently skipped.
pub subfolder_tokens: Vec<FolderToken>,
}
#[derive(Serialize, ToSchema)]
pub struct BulkRemoveSubfoldersSuccessResponse {
pub success: bool,
/// How many rows were actually unparented.
pub removed_count: u64,
}
@@ -2,6 +2,7 @@ pub mod analytics;
pub mod characters;
pub mod common;
pub mod credits;
pub mod folders;
pub mod generate;
pub mod jobs;
pub mod media_file;
@@ -71,7 +71,19 @@ pub struct OmniGenVideoModelDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub full_name: Option<String>,
/// Additional details about the model.
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_info: Option<String>,
/// Additional details about the model. (Brief; only a few words.)
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_info_short: Option<String>,
/// Whether a pure text-to-video modality is supported
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_video_supported: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_prompt_supported: Option<bool>,
@@ -183,6 +195,9 @@ impl Default for OmniGenVideoModelDetails {
model: DEFAULT_VIDEO_MODEL,
model_creator: None,
full_name: None,
extra_info: None,
extra_info_short: None,
text_to_video_supported: None,
text_prompt_supported: None,
text_prompt_max_length: None,
negative_text_prompt_supported: None,
@@ -40,4 +40,5 @@ uuid.workspace = true
[dev-dependencies]
test_data.workspace = true
strum.workspace = true
serde_json.workspace = true
@@ -2,7 +2,7 @@ use serde_derive::{Deserialize, Serialize};
/// Common image models supported by the router.
/// Not all models are available through all providers.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RouterImageModel {
#[serde(rename = "flux_1_dev")]
@@ -23,6 +23,12 @@ pub enum RouterImageModel {
GrokImagineImage,
#[serde(rename = "grok_imagine_image_q")]
GrokImagineImageQuality,
#[serde(rename = "midjourney_7")]
Midjourney7,
#[serde(rename = "midjourney_7_niji")]
Midjourney7Niji,
#[serde(rename = "midjourney_8")]
Midjourney8,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_2")]
@@ -40,3 +46,31 @@ pub enum RouterImageModel {
#[serde(rename = "flux_2_lora_angles")]
Flux2LoraAngles,
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_round_trip(model: RouterImageModel, expected_wire: &str) {
let json = serde_json::to_string(&model).unwrap();
let unquoted = json.trim_matches('"');
assert_eq!(unquoted, expected_wire, "serialize {:?}", model);
let back: RouterImageModel = serde_json::from_str(&json).unwrap();
assert_eq!(back, model, "deserialize round-trip for {:?}", model);
}
#[test]
fn midjourney_7_serializes_to_midjourney_7() {
assert_round_trip(RouterImageModel::Midjourney7, "midjourney_7");
}
#[test]
fn midjourney_7_niji_serializes_to_midjourney_7_niji() {
assert_round_trip(RouterImageModel::Midjourney7Niji, "midjourney_7_niji");
}
#[test]
fn midjourney_8_serializes_to_midjourney_8() {
assert_round_trip(RouterImageModel::Midjourney8, "midjourney_8");
}
}
@@ -11,6 +11,9 @@ pub enum RouterVideoModel {
#[serde(rename = "grok_imagine_video")]
GrokImagineVideo,
#[serde(rename = "grok_imagine_video_1p5")]
GrokImagineVideo1p5,
#[serde(rename = "kling_1p6_pro")]
Kling16Pro,
@@ -56,6 +59,12 @@ pub enum RouterVideoModel {
#[serde(rename = "seedance_2p0_u_fast")]
Seedance2p0UltraFast,
#[serde(rename = "seedance_2p0_bpu")]
Seedance2p0BytePlusUltra,
#[serde(rename = "seedance_2p0_bpu_fast")]
Seedance2p0BytePlusUltraFast,
#[serde(rename = "happy_horse_1p0")]
HappyHorse1p0,
@@ -6,7 +6,7 @@ use fal_client::creds::fal_api_key::FalApiKey;
/// - `None` → endpoints will use the queue/polling flow (where available).
///
/// Some endpoints are webhook-only (their fal_client wrapper lives under the
/// `requests::webhook::*` namespace and has no `api::` queue variant). Those
/// `requests_old::webhook::*` namespace and has no `api::` queue variant). Those
/// will return `ClientError::WebhookUrlRequired` when constructed with `None`.
pub struct RouterFalClient {
pub(crate) api_key: FalApiKey,
@@ -7,37 +7,43 @@ use crate::api::router_provider::RouterProvider;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::build::build_artcraft_flux_1_dev;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::build::build_artcraft_flux_1_schnell;
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::build::build_artcraft_flux_2_lora_angles;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::build::build_artcraft_flux_pro_1p1;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::build::build_artcraft_flux_pro_1p1_ultra;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::build::build_artcraft_gpt_image_1;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::build::build_artcraft_gpt_image_1p5;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::build::build_artcraft_gpt_image_2;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::build::build_artcraft_nano_banana;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_2::build::build_artcraft_nano_banana_2;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_pro::build::build_artcraft_nano_banana_pro;
use crate::generate::generate_image_v2::providers::artcraft::qwen_edit_2511_angles::build::build_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image_v2::providers::artcraft::seedream_4::build::build_artcraft_seedream_4;
use crate::generate::generate_image_v2::providers::artcraft::seedream_4p5::build::build_artcraft_seedream_4p5;
use crate::generate::generate_image_v2::providers::artcraft::seedream_5_lite::build::build_artcraft_seedream_5_lite;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::build::build_fal_flux_1_dev;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::build::build_fal_flux_1_schnell;
use crate::generate::generate_image_v2::providers::fal::flux_2_lora_angles::build::build_fal_flux_2_lora_angles;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::build::build_fal_flux_pro_1p1;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::build::build_fal_flux_pro_1p1_ultra;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::build::build_fal_gpt_image_1;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::build::build_fal_gpt_image_1p5;
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::build::build_fal_gpt_image_2;
use crate::generate::generate_image_v2::providers::fal::nano_banana::build::build_fal_nano_banana;
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::build::build_fal_nano_banana_2;
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::build::build_fal_nano_banana_pro;
use crate::generate::generate_image_v2::providers::fal::qwen_edit_2511_angles::build::build_fal_qwen_edit_2511_angles;
use crate::generate::generate_image_v2::providers::fal::seedream_4::build::build_fal_seedream_4;
use crate::generate::generate_image_v2::providers::fal::seedream_4p5::build::build_fal_seedream_4p5;
use crate::generate::generate_image_v2::providers::fal::seedream_5_lite::build::build_fal_seedream_5_lite;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::providers::artcraft::flux_1_dev::build::build_artcraft_flux_1_dev;
use crate::generate::generate_image::providers::artcraft::flux_1_schnell::build::build_artcraft_flux_1_schnell;
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::build::build_artcraft_flux_2_lora_angles;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1::build::build_artcraft_flux_pro_1p1;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::build::build_artcraft_flux_pro_1p1_ultra;
use crate::generate::generate_image::providers::artcraft::gpt_image_1::build::build_artcraft_gpt_image_1;
use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::build::build_artcraft_gpt_image_1p5;
use crate::generate::generate_image::providers::artcraft::gpt_image_2::build::build_artcraft_gpt_image_2;
use crate::generate::generate_image::providers::artcraft::midjourney_7::build::build_artcraft_midjourney_7;
use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::build::build_artcraft_midjourney_7_niji;
use crate::generate::generate_image::providers::artcraft::midjourney_8::build::build_artcraft_midjourney_8;
use crate::generate::generate_image::providers::artcraft::nano_banana::build::build_artcraft_nano_banana;
use crate::generate::generate_image::providers::artcraft::nano_banana_2::build::build_artcraft_nano_banana_2;
use crate::generate::generate_image::providers::artcraft::nano_banana_pro::build::build_artcraft_nano_banana_pro;
use crate::generate::generate_image::providers::artcraft::qwen_edit_2511_angles::build::build_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image::providers::artcraft::seedream_4::build::build_artcraft_seedream_4;
use crate::generate::generate_image::providers::artcraft::seedream_4p5::build::build_artcraft_seedream_4p5;
use crate::generate::generate_image::providers::artcraft::seedream_5_lite::build::build_artcraft_seedream_5_lite;
use crate::generate::generate_image::providers::fal::flux_1_dev::build::build_fal_flux_1_dev;
use crate::generate::generate_image::providers::fal::flux_1_schnell::build::build_fal_flux_1_schnell;
use crate::generate::generate_image::providers::fal::flux_2_lora_angles::build::build_fal_flux_2_lora_angles;
use crate::generate::generate_image::providers::fal::flux_pro_1p1::build::build_fal_flux_pro_1p1;
use crate::generate::generate_image::providers::fal::flux_pro_1p1_ultra::build::build_fal_flux_pro_1p1_ultra;
use crate::generate::generate_image::providers::fal::gpt_image_1::build::build_fal_gpt_image_1;
use crate::generate::generate_image::providers::fal::gpt_image_1p5::build::build_fal_gpt_image_1p5;
use crate::generate::generate_image::providers::fal::gpt_image_2::build::build_fal_gpt_image_2;
use crate::generate::generate_image::providers::fal::nano_banana::build::build_fal_nano_banana;
use crate::generate::generate_image::providers::fal::nano_banana_2::build::build_fal_nano_banana_2;
use crate::generate::generate_image::providers::fal::nano_banana_pro::build::build_fal_nano_banana_pro;
use crate::generate::generate_image::providers::fal::qwen_edit_2511_angles::build::build_fal_qwen_edit_2511_angles;
use crate::generate::generate_image::providers::fal::seedream_4::build::build_fal_seedream_4;
use crate::generate::generate_image::providers::fal::seedream_4p5::build::build_fal_seedream_4p5;
use crate::generate::generate_image::providers::fal::seedream_5_lite::build::build_fal_seedream_5_lite;
use crate::generate::generate_image::providers::kinovi::midjourney_7::build::build_kinovi_midjourney_7;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::build::build_kinovi_midjourney_7_niji;
use crate::generate::generate_image::providers::kinovi::midjourney_8::build::build_kinovi_midjourney_8;
#[derive(Clone, Debug)]
pub struct GenerateImageRequestBuilder {
@@ -115,6 +121,9 @@ impl GenerateImageRequestBuilder {
(RouterProvider::Artcraft, RouterImageModel::Seedream5Lite) => build_artcraft_seedream_5_lite(self),
(RouterProvider::Artcraft, RouterImageModel::QwenEdit2511Angles) => build_artcraft_qwen_edit_2511_angles(self),
(RouterProvider::Artcraft, RouterImageModel::Flux2LoraAngles) => build_artcraft_flux_2_lora_angles(self),
(RouterProvider::Artcraft, RouterImageModel::Midjourney7) => build_artcraft_midjourney_7(self),
(RouterProvider::Artcraft, RouterImageModel::Midjourney7Niji) => build_artcraft_midjourney_7_niji(self),
(RouterProvider::Artcraft, RouterImageModel::Midjourney8) => build_artcraft_midjourney_8(self),
(RouterProvider::Fal, RouterImageModel::Flux1Dev) => build_fal_flux_1_dev(self),
(RouterProvider::Fal, RouterImageModel::Flux1Schnell) => build_fal_flux_1_schnell(self),
@@ -132,6 +141,11 @@ impl GenerateImageRequestBuilder {
(RouterProvider::Fal, RouterImageModel::QwenEdit2511Angles) => build_fal_qwen_edit_2511_angles(self),
(RouterProvider::Fal, RouterImageModel::Flux2LoraAngles) => build_fal_flux_2_lora_angles(self),
// Kinovi / Seedance2Pro (Midjourney)
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7) => build_kinovi_midjourney_7(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7Niji) => build_kinovi_midjourney_7_niji(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney8) => build_kinovi_midjourney_8(self),
_ => self.unsupported_provider_and_model(),
}
}
@@ -25,10 +25,22 @@ pub struct FalImageResponsePayload {
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
}
/// Response from the Seedance2Pro/Kinovi provider (used for Midjourney
/// image generation). Mirrors `Seedance2proVideoResponsePayload` on the
/// video side.
#[derive(Clone, Debug)]
pub struct Seedance2proImageResponsePayload {
pub order_id: String,
pub task_id: String,
pub maybe_order_ids: Option<Vec<String>>,
pub maybe_task_ids: Option<Vec<String>>,
}
#[derive(Clone, Debug)]
pub enum GenerateImageResponse {
Artcraft(ArtcraftImageResponsePayload),
Fal(FalImageResponsePayload),
Seedance2Pro(Seedance2proImageResponsePayload),
}
impl GenerateImageResponse {
@@ -45,4 +57,11 @@ impl GenerateImageResponse {
_ => None,
}
}
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proImageResponsePayload> {
match self {
Self::Seedance2Pro(p) => Some(p.clone()),
_ => None,
}
}
}
@@ -0,0 +1,60 @@
use crate::api::router_provider::RouterProvider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::kinovi::midjourney_7::cost::KinoviMidjourney7CostState;
use crate::generate::generate_image::providers::kinovi::midjourney_7::draft::KinoviMidjourney7DraftState;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::cost::KinoviMidjourney7NijiCostState;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::draft::KinoviMidjourney7NijiDraftState;
use crate::generate::generate_image::providers::kinovi::midjourney_8::cost::KinoviMidjourney8CostState;
use crate::generate::generate_image::providers::kinovi::midjourney_8::draft::KinoviMidjourney8DraftState;
/// Wrapper for all image generation draft requests.
///
/// Drafts hold validated/planned parameters but may still need media
/// resolution (e.g. uploading local files to a provider) before they can
/// be sent. Today only Kinovi/Midjourney needs this — when the caller
/// supplies reference images, we must upload them to the Seedance2Pro
/// CDN before sending the request.
#[derive(Clone, Debug)]
pub enum ImageGenerationDraftRequest {
KinoviMidjourney7(KinoviMidjourney7DraftState),
KinoviMidjourney7Niji(KinoviMidjourney7NijiDraftState),
KinoviMidjourney8(KinoviMidjourney8DraftState),
}
impl ImageGenerationDraftRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
}
}
pub fn estimate_cost(&self) -> Result<ImageGenerationCostEstimate, ArtcraftRouterError> {
match self {
Self::KinoviMidjourney7(draft) => Ok(KinoviMidjourney7CostState::from_draft(draft).estimate_cost()),
Self::KinoviMidjourney7Niji(draft) => Ok(KinoviMidjourney7NijiCostState::from_draft(draft).estimate_cost()),
Self::KinoviMidjourney8(draft) => Ok(KinoviMidjourney8CostState::from_draft(draft).estimate_cost()),
}
}
pub async fn finalize(self, draft_context: ImageGenerationDraftContext<'_>) -> Result<ImageGenerationRequest, ArtcraftRouterError> {
match self {
Self::KinoviMidjourney7(mut draft) => {
let result = draft.to_request(&draft_context).await?;
Ok(ImageGenerationRequest::KinoviMidjourney7(result))
}
Self::KinoviMidjourney7Niji(mut draft) => {
let result = draft.to_request(&draft_context).await?;
Ok(ImageGenerationRequest::KinoviMidjourney7Niji(result))
}
Self::KinoviMidjourney8(mut draft) => {
let result = draft.to_request(&draft_context).await?;
Ok(ImageGenerationRequest::KinoviMidjourney8(result))
}
}
}
}
@@ -0,0 +1,46 @@
use std::collections::HashMap;
use std::fmt::Debug;
use tokens::tokens::media_files::MediaFileToken;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
/// Context passed when finalizing an image draft into a concrete request.
///
/// Drafts only exist for providers that need to upload assets before
/// sending the request. Today that's the Seedance2Pro/Kinovi pipeline
/// (used for Midjourney image generation with reference images); other
/// providers — Artcraft, Fal — accept image URLs directly and skip the
/// draft phase entirely.
#[derive(Clone, Default)]
pub struct ImageGenerationDraftContext<'a> {
/// Optional: the router client, needed for providers that require
/// authenticated calls (e.g. Seedance2Pro/Kinovi uploads).
pub client: Option<&'a RouterClient>,
/// Optional: a map of Media File Tokens to their ArtCraft URLs.
/// Required only when the draft has unresolved `MediaFileToken` image
/// inputs that must be downloaded from ArtCraft and re-uploaded to the
/// target provider's CDN.
pub media_file_to_artcraft_url_map: Option<&'a HashMap<MediaFileToken, String>>,
}
impl<'a> ImageGenerationDraftContext<'a> {
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)
}
}
impl Debug for ImageGenerationDraftContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageGenerationDraftContext")
.field("client", &self.client.is_some())
.field("media_file_to_artcraft_url_map", &self.media_file_to_artcraft_url_map.map(|m| m.len()))
.finish()
}
}
@@ -1,8 +1,8 @@
use crate::api::router_provider::RouterProvider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
#[derive(Debug, Clone)]
pub enum ImageGenerationDraftOrRequest {
@@ -3,66 +3,78 @@ use crate::client::router_client::RouterClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::cost::ArtcraftFlux1DevCostState;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::cost::ArtcraftFlux1SchnellCostState;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::cost::ArtcraftFlux2LoraAnglesCostState;
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::cost::ArtcraftFluxPro1p1CostState;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::cost::ArtcraftFluxPro1p1UltraCostState;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::cost::ArtcraftGptImage1CostState;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::cost::ArtcraftGptImage1p5CostState;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::cost::ArtcraftGptImage2CostState;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::cost::ArtcraftNanoBananaCostState;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_2::cost::ArtcraftNanoBanana2CostState;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_2::request::ArtcraftNanoBanana2RequestState;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_pro::cost::ArtcraftNanoBananaProCostState;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_pro::request::ArtcraftNanoBananaProRequestState;
use crate::generate::generate_image_v2::providers::artcraft::qwen_edit_2511_angles::cost::ArtcraftQwenEdit2511AnglesCostState;
use crate::generate::generate_image_v2::providers::artcraft::qwen_edit_2511_angles::request::ArtcraftQwenEdit2511AnglesRequestState;
use crate::generate::generate_image_v2::providers::artcraft::seedream_4::cost::ArtcraftSeedream4CostState;
use crate::generate::generate_image_v2::providers::artcraft::seedream_4::request::ArtcraftSeedream4RequestState;
use crate::generate::generate_image_v2::providers::artcraft::seedream_4p5::cost::ArtcraftSeedream4p5CostState;
use crate::generate::generate_image_v2::providers::artcraft::seedream_4p5::request::ArtcraftSeedream4p5RequestState;
use crate::generate::generate_image_v2::providers::artcraft::seedream_5_lite::cost::ArtcraftSeedream5LiteCostState;
use crate::generate::generate_image_v2::providers::artcraft::seedream_5_lite::request::ArtcraftSeedream5LiteRequestState;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::cost::FalFlux1DevCostState;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::cost::FalFlux1SchnellCostState;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
use crate::generate::generate_image_v2::providers::fal::flux_2_lora_angles::cost::FalFlux2LoraAnglesCostState;
use crate::generate::generate_image_v2::providers::fal::flux_2_lora_angles::request::FalFlux2LoraAnglesRequestState;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::cost::FalFluxPro1p1CostState;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::cost::FalFluxPro1p1UltraCostState;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::cost::FalGptImage1CostState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::cost::FalGptImage1p5CostState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::cost::FalGptImage2CostState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
use crate::generate::generate_image_v2::providers::fal::nano_banana::cost::FalNanoBananaCostState;
use crate::generate::generate_image_v2::providers::fal::nano_banana::request::FalNanoBananaRequestState;
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::cost::FalNanoBanana2CostState;
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::cost::FalNanoBananaProCostState;
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::request::FalNanoBananaProRequestState;
use crate::generate::generate_image_v2::providers::fal::qwen_edit_2511_angles::cost::FalQwenEdit2511AnglesCostState;
use crate::generate::generate_image_v2::providers::fal::qwen_edit_2511_angles::request::FalQwenEdit2511AnglesRequestState;
use crate::generate::generate_image_v2::providers::fal::seedream_4::cost::FalSeedream4CostState;
use crate::generate::generate_image_v2::providers::fal::seedream_4::request::FalSeedream4RequestState;
use crate::generate::generate_image_v2::providers::fal::seedream_4p5::cost::FalSeedream4p5CostState;
use crate::generate::generate_image_v2::providers::fal::seedream_4p5::request::FalSeedream4p5RequestState;
use crate::generate::generate_image_v2::providers::fal::seedream_5_lite::cost::FalSeedream5LiteCostState;
use crate::generate::generate_image_v2::providers::fal::seedream_5_lite::request::FalSeedream5LiteRequestState;
use crate::generate::generate_image::providers::artcraft::flux_1_dev::cost::ArtcraftFlux1DevCostState;
use crate::generate::generate_image::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
use crate::generate::generate_image::providers::artcraft::flux_1_schnell::cost::ArtcraftFlux1SchnellCostState;
use crate::generate::generate_image::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::cost::ArtcraftFlux2LoraAnglesCostState;
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1::cost::ArtcraftFluxPro1p1CostState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::cost::ArtcraftFluxPro1p1UltraCostState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1::cost::ArtcraftGptImage1CostState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::cost::ArtcraftGptImage1p5CostState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_2::cost::ArtcraftGptImage2CostState;
use crate::generate::generate_image::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::ArtcraftMidjourney7CostState;
use crate::generate::generate_image::providers::artcraft::midjourney_7::request::ArtcraftMidjourney7RequestState;
use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::cost::ArtcraftMidjourney7NijiCostState;
use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::request::ArtcraftMidjourney7NijiRequestState;
use crate::generate::generate_image::providers::artcraft::midjourney_8::cost::ArtcraftMidjourney8CostState;
use crate::generate::generate_image::providers::artcraft::midjourney_8::request::ArtcraftMidjourney8RequestState;
use crate::generate::generate_image::providers::artcraft::nano_banana::cost::ArtcraftNanoBananaCostState;
use crate::generate::generate_image::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
use crate::generate::generate_image::providers::artcraft::nano_banana_2::cost::ArtcraftNanoBanana2CostState;
use crate::generate::generate_image::providers::artcraft::nano_banana_2::request::ArtcraftNanoBanana2RequestState;
use crate::generate::generate_image::providers::artcraft::nano_banana_pro::cost::ArtcraftNanoBananaProCostState;
use crate::generate::generate_image::providers::artcraft::nano_banana_pro::request::ArtcraftNanoBananaProRequestState;
use crate::generate::generate_image::providers::artcraft::qwen_edit_2511_angles::cost::ArtcraftQwenEdit2511AnglesCostState;
use crate::generate::generate_image::providers::artcraft::qwen_edit_2511_angles::request::ArtcraftQwenEdit2511AnglesRequestState;
use crate::generate::generate_image::providers::artcraft::seedream_4::cost::ArtcraftSeedream4CostState;
use crate::generate::generate_image::providers::artcraft::seedream_4::request::ArtcraftSeedream4RequestState;
use crate::generate::generate_image::providers::artcraft::seedream_4p5::cost::ArtcraftSeedream4p5CostState;
use crate::generate::generate_image::providers::artcraft::seedream_4p5::request::ArtcraftSeedream4p5RequestState;
use crate::generate::generate_image::providers::artcraft::seedream_5_lite::cost::ArtcraftSeedream5LiteCostState;
use crate::generate::generate_image::providers::artcraft::seedream_5_lite::request::ArtcraftSeedream5LiteRequestState;
use crate::generate::generate_image::providers::fal::flux_1_dev::cost::FalFlux1DevCostState;
use crate::generate::generate_image::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
use crate::generate::generate_image::providers::fal::flux_1_schnell::cost::FalFlux1SchnellCostState;
use crate::generate::generate_image::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
use crate::generate::generate_image::providers::fal::flux_2_lora_angles::cost::FalFlux2LoraAnglesCostState;
use crate::generate::generate_image::providers::fal::flux_2_lora_angles::request::FalFlux2LoraAnglesRequestState;
use crate::generate::generate_image::providers::fal::flux_pro_1p1::cost::FalFluxPro1p1CostState;
use crate::generate::generate_image::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
use crate::generate::generate_image::providers::fal::flux_pro_1p1_ultra::cost::FalFluxPro1p1UltraCostState;
use crate::generate::generate_image::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
use crate::generate::generate_image::providers::fal::gpt_image_1::cost::FalGptImage1CostState;
use crate::generate::generate_image::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
use crate::generate::generate_image::providers::fal::gpt_image_1p5::cost::FalGptImage1p5CostState;
use crate::generate::generate_image::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
use crate::generate::generate_image::providers::fal::gpt_image_2::cost::FalGptImage2CostState;
use crate::generate::generate_image::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
use crate::generate::generate_image::providers::fal::nano_banana::cost::FalNanoBananaCostState;
use crate::generate::generate_image::providers::fal::nano_banana::request::FalNanoBananaRequestState;
use crate::generate::generate_image::providers::fal::nano_banana_2::cost::FalNanoBanana2CostState;
use crate::generate::generate_image::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
use crate::generate::generate_image::providers::fal::nano_banana_pro::cost::FalNanoBananaProCostState;
use crate::generate::generate_image::providers::fal::nano_banana_pro::request::FalNanoBananaProRequestState;
use crate::generate::generate_image::providers::fal::qwen_edit_2511_angles::cost::FalQwenEdit2511AnglesCostState;
use crate::generate::generate_image::providers::fal::qwen_edit_2511_angles::request::FalQwenEdit2511AnglesRequestState;
use crate::generate::generate_image::providers::fal::seedream_4::cost::FalSeedream4CostState;
use crate::generate::generate_image::providers::fal::seedream_4::request::FalSeedream4RequestState;
use crate::generate::generate_image::providers::fal::seedream_4p5::cost::FalSeedream4p5CostState;
use crate::generate::generate_image::providers::fal::seedream_4p5::request::FalSeedream4p5RequestState;
use crate::generate::generate_image::providers::fal::seedream_5_lite::cost::FalSeedream5LiteCostState;
use crate::generate::generate_image::providers::fal::seedream_5_lite::request::FalSeedream5LiteRequestState;
use crate::generate::generate_image::providers::kinovi::midjourney_7::cost::KinoviMidjourney7CostState;
use crate::generate::generate_image::providers::kinovi::midjourney_7::request::KinoviMidjourney7RequestState;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::cost::KinoviMidjourney7NijiCostState;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::request::KinoviMidjourney7NijiRequestState;
use crate::generate::generate_image::providers::kinovi::midjourney_8::cost::KinoviMidjourney8CostState;
use crate::generate::generate_image::providers::kinovi::midjourney_8::request::KinoviMidjourney8RequestState;
#[derive(Clone, Debug)]
pub enum ImageGenerationRequest {
@@ -82,6 +94,9 @@ pub enum ImageGenerationRequest {
ArtcraftSeedream5Lite(ArtcraftSeedream5LiteRequestState),
ArtcraftQwenEdit2511Angles(ArtcraftQwenEdit2511AnglesRequestState),
ArtcraftFlux2LoraAngles(ArtcraftFlux2LoraAnglesRequestState),
ArtcraftMidjourney7(ArtcraftMidjourney7RequestState),
ArtcraftMidjourney7Niji(ArtcraftMidjourney7NijiRequestState),
ArtcraftMidjourney8(ArtcraftMidjourney8RequestState),
// ── Fal provider ──
FalFlux1Dev(FalFlux1DevRequestState),
@@ -99,6 +114,11 @@ pub enum ImageGenerationRequest {
FalSeedream5Lite(FalSeedream5LiteRequestState),
FalQwenEdit2511Angles(FalQwenEdit2511AnglesRequestState),
FalFlux2LoraAngles(FalFlux2LoraAnglesRequestState),
// ── Kinovi / Seedance2Pro provider (Midjourney image generation) ──
KinoviMidjourney7(KinoviMidjourney7RequestState),
KinoviMidjourney7Niji(KinoviMidjourney7NijiRequestState),
KinoviMidjourney8(KinoviMidjourney8RequestState),
}
impl ImageGenerationRequest {
@@ -119,6 +139,9 @@ impl ImageGenerationRequest {
Self::ArtcraftSeedream5Lite(_) => RouterProvider::Artcraft,
Self::ArtcraftQwenEdit2511Angles(_) => RouterProvider::Artcraft,
Self::ArtcraftFlux2LoraAngles(_) => RouterProvider::Artcraft,
Self::ArtcraftMidjourney7(_) => RouterProvider::Artcraft,
Self::ArtcraftMidjourney7Niji(_) => RouterProvider::Artcraft,
Self::ArtcraftMidjourney8(_) => RouterProvider::Artcraft,
Self::FalFlux1Dev(_) => RouterProvider::Fal,
Self::FalFlux1Schnell(_) => RouterProvider::Fal,
@@ -135,6 +158,10 @@ impl ImageGenerationRequest {
Self::FalSeedream5Lite(_) => RouterProvider::Fal,
Self::FalQwenEdit2511Angles(_) => RouterProvider::Fal,
Self::FalFlux2LoraAngles(_) => RouterProvider::Fal,
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
}
}
@@ -186,6 +213,15 @@ impl ImageGenerationRequest {
Self::ArtcraftFlux2LoraAngles(request) => {
Ok(ArtcraftFlux2LoraAnglesCostState::from_request(request).estimate_cost())
}
Self::ArtcraftMidjourney7(request) => {
Ok(ArtcraftMidjourney7CostState::from_request(request).estimate_cost())
}
Self::ArtcraftMidjourney7Niji(request) => {
Ok(ArtcraftMidjourney7NijiCostState::from_request(request).estimate_cost())
}
Self::ArtcraftMidjourney8(request) => {
Ok(ArtcraftMidjourney8CostState::from_request(request).estimate_cost())
}
// ── Fal ──
Self::FalFlux1Dev(request) => Ok(FalFlux1DevCostState::from_request(request).estimate_cost()),
@@ -203,6 +239,10 @@ impl ImageGenerationRequest {
Self::FalSeedream5Lite(request) => Ok(FalSeedream5LiteCostState::from_request(request).estimate_cost()),
Self::FalQwenEdit2511Angles(request) => Ok(FalQwenEdit2511AnglesCostState::from_request(request).estimate_cost()),
Self::FalFlux2LoraAngles(request) => Ok(FalFlux2LoraAnglesCostState::from_request(request).estimate_cost()),
Self::KinoviMidjourney7(request) => Ok(KinoviMidjourney7CostState::from_request(request).estimate_cost()),
Self::KinoviMidjourney7Niji(request) => Ok(KinoviMidjourney7NijiCostState::from_request(request).estimate_cost()),
Self::KinoviMidjourney8(request) => Ok(KinoviMidjourney8CostState::from_request(request).estimate_cost()),
}
}
@@ -269,6 +309,18 @@ impl ImageGenerationRequest {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftMidjourney7(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftMidjourney7Niji(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftMidjourney8(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
// ── Fal ──
Self::FalFlux1Dev(request) => {
@@ -336,6 +388,23 @@ impl ImageGenerationRequest {
let fal_client = client.get_fal_client_ref()?;
request.send(fal_client).await
}
// ── Kinovi / Seedance2Pro (Midjourney) ──
Self::KinoviMidjourney7(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviMidjourney7Niji(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviMidjourney8(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
}
}
}
@@ -1,3 +1,8 @@
pub mod generate_image_request_builder;
pub mod generate_image_response;
pub mod image_generation_cost_estimate;
pub mod image_generation_draft;
pub mod image_generation_draft_context;
pub mod image_generation_draft_or_request;
pub mod image_generation_request;
pub mod providers;
@@ -2,10 +2,10 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
pub fn build_artcraft_flux_1_dev(
builder: GenerateImageRequestBuilder,
@@ -1,5 +1,5 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
use crate::generate::generate_image::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
/// Cost state for Artcraft Flux 1 Dev. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_flux_1_dev`): flat 2¢ per output image.
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftFlux1DevRequestState {
@@ -2,10 +2,10 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
pub fn build_artcraft_flux_1_schnell(
builder: GenerateImageRequestBuilder,
@@ -1,5 +1,5 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
use crate::generate::generate_image::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
/// Cost state for Artcraft Flux 1 Schnell. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_flux_1_schnell`): free, no per-image cost,
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftFlux1SchnellRequestState {
@@ -2,11 +2,11 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::angle_validation::require_at_least_one_image_input;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::angle_validation::require_at_least_one_image_input;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
pub fn build_artcraft_flux_2_lora_angles(
builder: GenerateImageRequestBuilder,
@@ -1,5 +1,5 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
/// Cost state for Artcraft Flux 2 LoRA Angles. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_flux_2_lora_angles`): 4¢ per output image
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftFlux2LoraAnglesRequestState {
@@ -2,10 +2,10 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
pub fn build_artcraft_flux_pro_1p1(
builder: GenerateImageRequestBuilder,
@@ -1,5 +1,5 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
/// Cost state for Artcraft Flux Pro 1.1. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_flux_pro_1p1`): 4¢ per output image (Fal's
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftFluxPro1p1RequestState {
@@ -2,10 +2,10 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
pub fn build_artcraft_flux_pro_1p1_ultra(
builder: GenerateImageRequestBuilder,
@@ -1,5 +1,5 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
/// Cost state for Artcraft Flux Pro 1.1 Ultra. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_flux_pro_1p1_ultra`): 6¢ per output image.
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftFluxPro1p1UltraRequestState {
@@ -2,10 +2,10 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
pub fn build_artcraft_gpt_image_1(
builder: GenerateImageRequestBuilder,
@@ -2,7 +2,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonA
use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
/// Cost state for Artcraft GPT Image 1. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_gpt_image_1`):
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftGptImage1RequestState {
@@ -2,10 +2,10 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
pub fn build_artcraft_gpt_image_1p5(
builder: GenerateImageRequestBuilder,
@@ -2,7 +2,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonA
use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
/// Cost state for Artcraft GPT Image 1.5. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_gpt_image_1p5`):
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftGptImage1p5RequestState {
@@ -2,10 +2,10 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
pub fn build_artcraft_gpt_image_2(
builder: GenerateImageRequestBuilder,
@@ -2,7 +2,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonA
use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
/// Cost state for Artcraft GPT Image 2. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_gpt_image_2`):
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftGptImage2RequestState {
@@ -0,0 +1,17 @@
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::midjourney_7::request::ArtcraftMidjourney7RequestState;
pub fn build_artcraft_midjourney_7(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney7)?;
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::ArtcraftMidjourney7(ArtcraftMidjourney7RequestState { request }),
))
}
@@ -0,0 +1,186 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::providers::artcraft::midjourney_7::request::ArtcraftMidjourney7RequestState;
/// Cost state for Artcraft Midjourney 7. Pricing mirrors the Kinovi
/// midjourney_7 provider 1:1 in USD cents — ArtCraft credits map cent-for-cent.
///
/// | batch | credits | USD cents |
/// |-------|---------|-----------|
/// | 1 | 6 | 6¢ |
/// | 2 | 12 | 12¢ |
/// | 3 | 19 | 19¢ | (interpolated; Kinovi doesn't expose 3)
/// | 4 | 25 | 25¢ |
///
/// Kinovi-side math: 12 Kinovi credits per Midjourney task; 193 Kinovi
/// credits per USD. Kinovi rounds to nearest cent, so batches 1/2/4 land
/// on 6/12/25. Batch 3 is `ceil((12 × 3 / 193) × 100) = 19`.
#[derive(Clone, Debug)]
pub struct ArtcraftMidjourney7CostState {
pub num_images: u16,
}
impl ArtcraftMidjourney7CostState {
pub fn from_request(request: &ArtcraftMidjourney7RequestState) -> Self {
Self {
num_images: request.request.image_batch_count.unwrap_or(1),
}
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
let cost_in_usd_cents = midjourney_cost_for_batch(self.num_images);
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
/// Returns the customer-facing midjourney cost in USD cents for a given
/// batch count. Shared between v7, v7-niji, and v8 (all three Midjourney
/// models have identical pricing on Kinovi).
pub(crate) fn midjourney_cost_for_batch(batch_count: u16) -> u64 {
match batch_count {
0 => 0, // Defensive; the builder rejects zero before reaching cost.
1 => 6,
2 => 12,
3 => 19,
4 => 25,
// Should never happen — `plan_batch_count` clamps to 1..=4.
_ => 25,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::router_image_model::RouterImageModel;
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn cost_cents(image_batch_count: u16) -> u64 {
let builder = GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7,
provider: RouterProvider::Artcraft,
prompt: Some("a corgi astronaut".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
}
fn cost_credits(image_batch_count: u16) -> u64 {
let builder = GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7,
provider: RouterProvider::Artcraft,
prompt: Some("a corgi astronaut".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
builder.build2().unwrap().estimate_cost().unwrap().cost_in_credits.unwrap()
}
// ── Spot checks: each supported batch size ──
#[test]
fn one_image_is_six_cents() { assert_eq!(cost_cents(1), 6); }
#[test]
fn two_images_is_twelve_cents() { assert_eq!(cost_cents(2), 12); }
#[test]
fn three_images_is_nineteen_cents() { assert_eq!(cost_cents(3), 19); }
#[test]
fn four_images_is_twentyfive_cents() { assert_eq!(cost_cents(4), 25); }
// ── Credits track USD cents exactly (1 credit = 1 cent) ──
#[test]
fn credits_equal_usd_cents() {
for batch in 1..=4u16 {
assert_eq!(cost_credits(batch), cost_cents(batch),
"credits should equal usd cents at batch {}", batch);
}
}
// ── 1:1 cost parity with Kinovi for batches 1/2/4 ──
//
// Kinovi midjourney_7 returns 6/12/25¢ for batches 1/2/4. The artcraft
// provider must match those values byte-for-byte so users pay the same.
mod kinovi_parity {
use super::*;
use crate::generate::generate_image::providers::kinovi::midjourney_7::cost::KinoviMidjourney7CostState;
use seedance2pro_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount;
#[test]
fn batch_1_matches_kinovi() {
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::One }
.estimate_cost();
assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap());
}
#[test]
fn batch_2_matches_kinovi() {
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Two }
.estimate_cost();
assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap());
}
#[test]
fn batch_4_matches_kinovi() {
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Four }
.estimate_cost();
assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap());
}
}
// ── Monotonicity ──
#[test]
fn cost_is_monotonically_increasing() {
let c1 = cost_cents(1);
let c2 = cost_cents(2);
let c3 = cost_cents(3);
let c4 = cost_cents(4);
assert!(c1 < c2, "{} < {}", c1, c2);
assert!(c2 < c3, "{} < {}", c2, c3);
assert!(c3 < c4, "{} < {}", c3, c4);
}
// ── Flags ──
#[test]
fn cost_flags_are_correct() {
let cost = ArtcraftMidjourney7CostState { num_images: 1 }.estimate_cost();
assert!(!cost.is_free);
assert!(!cost.is_unlimited);
assert!(!cost.is_rate_limited);
assert!(!cost.has_watermark);
assert_eq!(cost.failures_are_refunded, None);
}
}
@@ -0,0 +1,17 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftMidjourney7RequestState {
pub request: OmniGenImageCostAndGenerateRequest,
}
impl ArtcraftMidjourney7RequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
send_artcraft_omni_image_request(&self.request, client).await
}
}
@@ -0,0 +1,17 @@
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::request::ArtcraftMidjourney7NijiRequestState;
pub fn build_artcraft_midjourney_7_niji(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney7Niji)?;
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::ArtcraftMidjourney7Niji(ArtcraftMidjourney7NijiRequestState { request }),
))
}
@@ -0,0 +1,113 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::midjourney_cost_for_batch;
use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::request::ArtcraftMidjourney7NijiRequestState;
/// Cost state for Artcraft Midjourney 7 Niji. Pricing is identical to v7
/// and v8 on Kinovi, so we share the per-batch table via
/// `midjourney_cost_for_batch`.
#[derive(Clone, Debug)]
pub struct ArtcraftMidjourney7NijiCostState {
pub num_images: u16,
}
impl ArtcraftMidjourney7NijiCostState {
pub fn from_request(request: &ArtcraftMidjourney7NijiRequestState) -> Self {
Self {
num_images: request.request.image_batch_count.unwrap_or(1),
}
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
let cost_in_usd_cents = midjourney_cost_for_batch(self.num_images);
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::router_image_model::RouterImageModel;
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn cost_cents(image_batch_count: u16) -> u64 {
let builder = GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7Niji,
provider: RouterProvider::Artcraft,
prompt: Some("anime forest".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
}
#[test]
fn batch_1_is_6() { assert_eq!(cost_cents(1), 6); }
#[test]
fn batch_2_is_12() { assert_eq!(cost_cents(2), 12); }
#[test]
fn batch_3_is_19() { assert_eq!(cost_cents(3), 19); }
#[test]
fn batch_4_is_25() { assert_eq!(cost_cents(4), 25); }
#[test]
fn niji_pricing_matches_v7() {
use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::ArtcraftMidjourney7CostState;
for batch in 1..=4u16 {
let v7 = ArtcraftMidjourney7CostState { num_images: batch }.estimate_cost();
let niji = ArtcraftMidjourney7NijiCostState { num_images: batch }.estimate_cost();
assert_eq!(v7.cost_in_usd_cents, niji.cost_in_usd_cents, "batch {}", batch);
assert_eq!(v7.cost_in_credits, niji.cost_in_credits, "batch {}", batch);
}
}
// ── 1:1 cost parity with Kinovi 7-niji ──
mod kinovi_parity {
use super::*;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::cost::KinoviMidjourney7NijiCostState;
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount;
#[test]
fn batch_1_matches_kinovi() {
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::One }
.estimate_cost();
assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap());
}
#[test]
fn batch_2_matches_kinovi() {
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Two }
.estimate_cost();
assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap());
}
#[test]
fn batch_4_matches_kinovi() {
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Four }
.estimate_cost();
assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap());
}
}
}
@@ -0,0 +1,17 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftMidjourney7NijiRequestState {
pub request: OmniGenImageCostAndGenerateRequest,
}
impl ArtcraftMidjourney7NijiRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
send_artcraft_omni_image_request(&self.request, client).await
}
}
@@ -0,0 +1,17 @@
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::midjourney_8::request::ArtcraftMidjourney8RequestState;
pub fn build_artcraft_midjourney_8(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney8)?;
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::ArtcraftMidjourney8(ArtcraftMidjourney8RequestState { request }),
))
}
@@ -0,0 +1,113 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::midjourney_cost_for_batch;
use crate::generate::generate_image::providers::artcraft::midjourney_8::request::ArtcraftMidjourney8RequestState;
/// Cost state for Artcraft Midjourney 8. Pricing is identical to v7 and
/// v7-niji on Kinovi; shares the per-batch table via
/// `midjourney_cost_for_batch`.
#[derive(Clone, Debug)]
pub struct ArtcraftMidjourney8CostState {
pub num_images: u16,
}
impl ArtcraftMidjourney8CostState {
pub fn from_request(request: &ArtcraftMidjourney8RequestState) -> Self {
Self {
num_images: request.request.image_batch_count.unwrap_or(1),
}
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
let cost_in_usd_cents = midjourney_cost_for_batch(self.num_images);
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::router_image_model::RouterImageModel;
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn cost_cents(image_batch_count: u16) -> u64 {
let builder = GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney8,
provider: RouterProvider::Artcraft,
prompt: Some("a haunted mansion".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
}
#[test]
fn batch_1_is_6() { assert_eq!(cost_cents(1), 6); }
#[test]
fn batch_2_is_12() { assert_eq!(cost_cents(2), 12); }
#[test]
fn batch_3_is_19() { assert_eq!(cost_cents(3), 19); }
#[test]
fn batch_4_is_25() { assert_eq!(cost_cents(4), 25); }
#[test]
fn v8_pricing_matches_v7() {
use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::ArtcraftMidjourney7CostState;
for batch in 1..=4u16 {
let v7 = ArtcraftMidjourney7CostState { num_images: batch }.estimate_cost();
let v8 = ArtcraftMidjourney8CostState { num_images: batch }.estimate_cost();
assert_eq!(v7.cost_in_usd_cents, v8.cost_in_usd_cents, "batch {}", batch);
assert_eq!(v7.cost_in_credits, v8.cost_in_credits, "batch {}", batch);
}
}
// ── 1:1 cost parity with Kinovi v8 ──
mod kinovi_parity {
use super::*;
use crate::generate::generate_image::providers::kinovi::midjourney_8::cost::KinoviMidjourney8CostState;
use seedance2pro_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount;
#[test]
fn batch_1_matches_kinovi() {
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::One }
.estimate_cost();
assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap());
}
#[test]
fn batch_2_matches_kinovi() {
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Two }
.estimate_cost();
assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap());
}
#[test]
fn batch_4_matches_kinovi() {
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Four }
.estimate_cost();
assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap());
}
}
}
@@ -0,0 +1,17 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftMidjourney8RequestState {
pub request: OmniGenImageCostAndGenerateRequest,
}
impl ArtcraftMidjourney8RequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
send_artcraft_omni_image_request(&self.request, client).await
}
}
@@ -8,6 +8,9 @@ pub mod flux_pro_1p1_ultra;
pub mod gpt_image_1;
pub mod gpt_image_1p5;
pub mod gpt_image_2;
pub mod midjourney_7;
pub mod midjourney_7_niji;
pub mod midjourney_8;
pub mod nano_banana;
pub mod nano_banana_2;
pub mod nano_banana_pro;
@@ -2,10 +2,10 @@ use enums::common::generation::common_image_model::CommonImageModel as CommonIma
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
pub fn build_artcraft_nano_banana(
builder: GenerateImageRequestBuilder,
@@ -1,5 +1,5 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
use crate::generate::generate_image::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
/// Cost state for Artcraft Nano Banana (Gemini 2.5 Flash). Pricing mirrors v1
/// (`estimate_image_cost_artcraft_nano_banana`): 4¢ per output image.
@@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request;
#[derive(Clone, Debug)]
pub struct ArtcraftNanoBananaRequestState {

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