Compare commits

...
Author SHA1 Message Date
Brandon Thomas 7c02fff23e development ports 2026-06-23 09:22:01 -04:00
bflatastic 2f9ffd4575 webapp: hide edit image for now 2026-06-23 18:03:48 +07:00
Brandon Thomas e8b19148fa seedance2pro better polling (#1678) 2026-06-22 21:01:08 -04:00
BFlat 31e912c4be Webapp: omni gen added bitrate field (#1677)
Add bitrate selector to video create page

- Add bitrate / bitrate_options / bitrate_default to OmniGenApi TS types
- Add bitrate to video UI store (sticky + persisted)
- Send bitrate in generate and cost-estimate requests
- Render Bitrate popover (desktop) and drawer section (mobile), shown
  only when the model returns bitrate_options
2026-06-22 21:56:34 +07:00
Brandon Thomas ceb9d402cf alter prompts add bitrate (#1676) 2026-06-22 10:18:43 -04:00
Brandon Thomas ac2e728000 seedance bitrate (#1675) 2026-06-22 08:24:29 -04:00
BFlat ef255f4ee6 Website: seedance 2.5 page (#1674) 2026-06-22 13:09:09 +07:00
Brandon Thomas 0c11a95669 resolution measure (#1673) 2026-06-20 10:13:30 -04:00
Brandon Thomas 9191fe337a handler filename (#1672) 2026-06-20 09:09:49 -04:00
Brandon Thomas eb3b733852 seedance cors (#1671) 2026-06-20 00:00:18 -04:00
Brandon Thomas 7d8d9462d1 rename legacy bucket client (#1670) 2026-06-19 23:52:30 -04:00
Brandon Thomas 660cf9f0ac bucket client (#1669) 2026-06-19 23:35:22 -04:00
Brandon Thomas bf373b1d5c netlify configs 2026-06-19 21:27:36 -04:00
Brandon Thomas 914c2532ec uploaded videos - tables, endpoints, frontend (#1668) 2026-06-19 21:17:45 -04:00
Brandon Thomas c7341d8f63 video info website (#1667) 2026-06-19 13:42:34 -04:00
Brandon Thomas 92e99cfcb6 video info handler (#1666) 2026-06-19 10:44:22 -04:00
Brandon Thomas 754cdb8a6c parse video info (#1665) 2026-06-19 10:24:01 -04:00
Brandon Thomas 7b9db101fe fix float/int credits parsing (#1664) 2026-06-18 19:37:13 -04:00
BFlat e94987e5a4 Restyle manifesto "control" emphasis as a tilted marker highlight
Swap the underline for a shared MarkerHighlight SVG stroke behind the word. Desktop reveal wipes left-to-right via clip-path instead of scaleX so the stroke is uncovered, not stretched.
2026-06-18 23:05:09 +07:00
BFlat 47d83be3fb Landing page polish: manifesto, feature deck, model marquee, communit… (#1663)
…y CTA

- Manifesto: drop "3D" and animate a blue underline on "control"
- Feature cards: scroll-driven stacking deck (scale back + fade once
covered)
- Use Every Model: fill chips from canonical model list with correct
brand icons
- Use Every Model: alternating-direction marquees, highlight on hover
- Stop renting: italicize "renting"; make the good/bad comparison read
clearly
- Community CTA: "Join our community" banner restyled to fit site + not
steal focus from final CTA
2026-06-18 11:25:24 +07:00
bflatastic fa4b57980c Webapp: rounded corner ui updates for library page 2026-06-17 22:15:57 +07:00
Brandon Thomas 87d060e68c fixes for frontend build (#1660) 2026-06-16 03:03:38 -04:00
Brandon Thomas fc7651071c more frontend build fixes (#1659) 2026-06-16 02:53:06 -04:00
Brandon Thomas 7a0bfa35e6 fixes for frontend build failures (#1658) 2026-06-16 02:41:36 -04:00
Brandon Thomas 0b4e288a3e refactor another module location 2026-06-16 01:53:13 -04:00
Brandon Thomas a0b6047a78 refactor module location 2026-06-16 01:42:01 -04:00
Brandon Thomas ebe1287182 misc cleanup, update claude docs (#1657) 2026-06-16 01:34:50 -04:00
Brandon Thomas de538ba94c package import 2026-06-16 00:37:23 -04:00
Brandon Thomas 29a127aac0 jsonify actix handlers (#1656) 2026-06-16 00:30:06 -04:00
Brandon Thomas 126f874c90 api key feature flag (#1655) 2026-06-15 23:25:58 -04:00
Brandon Thomas 65e72f8c24 omni api fixes (#1654) 2026-06-15 23:06:05 -04:00
Brandon Thomas fafb485a60 omni upload APIs (#1653) 2026-06-15 22:37:11 -04:00
Brandon Thomas 1e20f2bfff omni api image and video (#1652) 2026-06-15 22:08:31 -04:00
BFlat e30b4366ce Update SettingsModal.tsx to flip api feature flag 2026-06-16 09:06:38 +07:00
Tanish Manku d27d5cec75 PageDraw: Port and mount to web app (#1650) 2026-06-15 17:53:15 -07:00
Brandon Thomas 749124f834 api groundwork (#1648) 2026-06-15 20:11:36 -04:00
bflatastic 65b18b9b51 Webapp: API section styling updates 2026-06-16 00:10:04 +07:00
BFlat 6eb85010d6 Webapp: API keys section in settings modal (#1647)
- create, update, delete api keys
- feature flagged (api_key)
2026-06-15 22:38:21 +07:00
Brandon Thomas 855e169b44 codegen database 2026-06-15 11:10:11 -04:00
Brandon Thomas ea2f5106df api keys (#1646) 2026-06-15 11:09:31 -04:00
Brandon Thomas 0de867ad13 upgrade tauri 2.8 -> 2.11 (#1645) 2026-06-14 22:20:29 -04:00
Brandon Thomas a1cb1b2629 enhance database connection pooling (#1644) 2026-06-14 18:09:42 -04:00
BFlat 8da68b009c Desktop create pages redesign: shared generation feed, embedded model selector, gallery folders sidebar (#1635)
## Summary

Reworks the desktop (Tauri) app's Create Image / Create Video pages to
match the webapp,
extracts the webapp's generation gallery into a shared library used by
both apps, embeds the
model selector into every prompt box, adds a folders sidebar + marquee
selection to the gallery
modal, restyles the login/pricing/credits modals to the webapp look, and
turns the Apps page
into the default landing tab.

### Shared generation-list lib

- Extracted the webapp's generation feed into
`@storyteller/ui-generation-list`: list + masonry
grid views, pending/failed/completed rows and cards, thumbnail handling,
gallery/prompt data
hooks, and the shared grid/list view store + toggle. Host-specific
actions (recreate,
make-video, share, download, toasts) inject via render props — webapp
markup unchanged, its
  old files are now thin wrappers.

### Desktop create pages

- Prompt box fixed at the bottom, `max-w-5xl` like the webapp; pages
renamed to
  "Create Image" / "Create Video".
- Generation feed: in-progress/failed from the Tauri task queue, history
from the library API,
rendered as grid or list (toggle in the TopBar next to credits). Typed
Tauri completion events
  push finished media in instantly; the poll reconciles and dedupes.
- Row/card quick actions (recreate, make video, copy link, download)
backed by a shared
  `desktopMediaActions` module extracted from the TopBar gallery wiring.
- Hero + background act as a splash that fades when the feed loads;
bottom gradient behind the
prompt box. Batch generations show "Generating N images" on the single
pending chip.
- Video prompt box now expands vertically as far as the image prompt box
(viewport-relative).

### Embedded model selector

- `ClassyModelSelector` gained `variant="embedded"`: a compact pill that
opens the webapp-style
rich-list popover while keeping the desktop's provider selection.
Embedded on all five
prompt-input pages; floating selectors removed; legacy "10 sec." badges
dropped.

### Gallery modal

- Left sidebar lists the user's folders (drag-and-drop, right-click
management, new-folder
button) and shows on the Folders tab too, highlighting the active
folder's root branch.
- Marquee (drag-rectangle) selection in view mode, with edge
auto-scroll; starts only on blank
  background so it doesn't interfere with drag-to-folder/canvas.

### Restyled modals (desktop)

- Login/signup: webapp 2-column layout (form left, Vimeo showcase
right), no Google button
(Google auth isn't wired for Tauri), `artcraft-icon` logo, eye toggles.
The dismiss button is
removed so login is required; the post-auth Discord + Success onboarding
steps are kept, as is
  the existing Tauri auth flow.
- Pricing + credits: match the webapp visually (unified monochrome plan
cards with
Most Popular / Best Value highlight frames; `#161618` credit-pack cards
with Popular badge).
All billing stays on the existing Tauri `invoke` calls — no billing
logic changed.

### Apps page / navigation

- The Apps page is now the first tab and the default landing tab, with a
Home icon. Restyled to
the webapp home: "What will you craft today?" hero, Create / Edit
sections, gradient-hover
cards sized to their content, full-width scrollbar. The TopBar
quick-menu header reads
  "Create" instead of "Generate".

### Fixes

- Video Editor no longer crashes on the desktop app (`Tooltip must be
used within
TooltipProvider`): the editor shell now mounts its own Radix
`TooltipProvider`. The webapp got
one app-wide via `SidebarProvider`; the Tauri app had none, so panel
tooltips threw.

### Misc

- `getCreatorIconPathForModelId()` added to `@storyteller/model-list`
(model id → creator icon
  path, desktop/webapp aware).
2026-06-13 17:25:40 -04:00
Brandon Thomas e4bc031fbb modernize require_user_session_extended() (#1643) 2026-06-13 17:03:21 -04:00
Brandon Thomas d937a7f78f cleanup require_user_session (#1642) 2026-06-13 05:54:01 -04:00
Brandon Thomas fef96c3835 cleanup usage of require_moderator further (#1641) 2026-06-13 05:26:36 -04:00
Brandon Thomas b9e793098c cleanup session and require_moderator (#1640) 2026-06-13 04:52:35 -04:00
782 changed files with 26696 additions and 6111 deletions
+12 -3
View File
@@ -19,9 +19,18 @@ Handlers in `storyteller_web` follow this structure:
### Session/Auth
- `require_user_session_using_connection()` reuses an open DB connection (preferred)
- `require_user_session()` acquires its own connection (deprecated)
- `require_moderator()` for admin-only endpoints
Helpers live in `storyteller_web` under `http_server/web_utils/user_session/`. Each takes a
generic `mysql_executor: E` (any `sqlx::Executor`), so whether it reuses a connection or acquires
its own depends on the **argument you pass**:
- `require_user_session(http_request, session_checker, mysql_executor)` — require a logged-in user.
- `require_user_session_extended(...)` — same, with extra session detail.
- `require_moderator(http_request, session_checker, mysql_executor)` — admin-only endpoints.
PREFER passing an already-open connection (`&mut *conn`) so the helper reuses it. Passing
`&server_state.mysql_pool` makes it acquire a fresh connection — avoid that when the handler
already holds one (the double-acquire adds pool pressure; this crate has had pool-timeout
incidents). There is no `*_using_connection` variant — the reuse/acquire choice is the argument.
### API Type Definitions
+5 -4
View File
@@ -7,10 +7,11 @@
- `#[macro_use] extern crate serde_derive;` is used in binary crates; library crates use `use serde_derive::{Serialize, Deserialize};`
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros (`info!`, `warn!`, `error!`, `debug!`, `trace!`) instead
- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code; new database queries should use `Result<T, sqlx::Error>` instead
- SQLx queries: prefer `sqlx::query!` and `sqlx::query_as!` compile-time checked macros over
runtime `sqlx::query()` / `sqlx::query_as()`. The macros catch SQL typos, column mismatches,
and type errors at build time. Use runtime queries only when assembling complicated
dynamic/branching queries that cannot be expressed as a compile-time macro.
- SQLx: MySQL queries belong in the `mysql_queries` crate. Do NOT write `sqlx::query!` /
`sqlx::query()` in application/service crates (e.g. `storyteller-web`, which has zero direct
SQLx queries) — add or reuse a function in `mysql_queries` and call it. The compile-time
`query!` / `query_as!` macro conventions for that crate are documented in
`crates/schema/database/mysql_queries/CLAUDE.md`.
- Enum Display/Debug: use `impl_enum_display_and_debug_using_to_str!` macro, not `derive_more::Display` (CLion doesn't understand it)
- Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`, `maybe_prompt_token`
- When implementing `fmt::Display` for error types, use the pattern: `write!(f, "{:?}", self)`
@@ -120,7 +120,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 3
}
},
{
@@ -60,7 +60,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -70,7 +70,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: ApiKeyToken`,\n LEFT(api_key, 20) as `api_key_prefix!`,\n name,\n maybe_description,\n owner_user_token as `owner_user_token: UserToken`,\n ip_address_creation,\n ip_address_update,\n created_at,\n updated_at,\n maybe_deleted_at\nFROM api_keys\nWHERE owner_user_token = ?\nORDER BY id DESC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "api_key_prefix!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "maybe_description",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 4,
"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": 5,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "ip_address_update",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_deleted_at",
"type_info": {
"type": "Timestamp",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
true,
false,
true,
false,
false,
false,
false,
false,
true
]
},
"hash": "097b8a8d02734c8c8e31dbb8a3bf4ace970693a2a0df2f05a31caf86b4d3d0ab"
}
@@ -210,7 +210,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -220,7 +220,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO uploaded_videos\nSET\n token = ?,\n sha1_checksum = ?,\n filesize_bytes = ?,\n maybe_filename = ?,\n maybe_width = ?,\n maybe_height = ?,\n maybe_resolution = ?,\n maybe_detected_model_family = ?,\n maybe_detected_model_type = ?,\n maybe_report = ?,\n upload_ip_address = ?,\n created_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
},
"hash": "14e63e59f7d550181126151029bcd17d08278196ba0ee4d32450c2567b1da26e"
}
@@ -30,7 +30,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 3
}
},
{
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::generation::common_model_type::CommonModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n\n p.maybe_generation_mode as `maybe_generation_mode: enums::common::generation::common_generation_mode::CommonGenerationMode`,\n p.maybe_aspect_ratio as `maybe_aspect_ratio: enums::common::generation::common_aspect_ratio::CommonAspectRatio`,\n p.maybe_resolution as `maybe_resolution: enums::common::generation::common_resolution::CommonResolution`,\n p.maybe_batch_count as `maybe_batch_count: u8`,\n p.maybe_generate_audio as `maybe_generate_audio: bool`,\n p.maybe_duration_seconds as `maybe_duration_seconds: u32`,\n\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
"query": "\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::generation::common_model_type::CommonModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n\n p.maybe_generation_mode as `maybe_generation_mode: enums::common::generation::common_generation_mode::CommonGenerationMode`,\n p.maybe_aspect_ratio as `maybe_aspect_ratio: enums::common::generation::common_aspect_ratio::CommonAspectRatio`,\n p.maybe_resolution as `maybe_resolution: enums::common::generation::common_resolution::CommonResolution`,\n p.maybe_bitrate as `maybe_bitrate: enums::common::generation::common_bitrate::CommonBitrate`,\n p.maybe_batch_count as `maybe_batch_count: u8`,\n p.maybe_generate_audio as `maybe_generate_audio: bool`,\n p.maybe_duration_seconds as `maybe_duration_seconds: u32`,\n\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
"describe": {
"columns": [
{
@@ -105,6 +105,16 @@
},
{
"ordinal": 10,
"name": "maybe_bitrate: enums::common::generation::common_bitrate::CommonBitrate",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "maybe_batch_count: u8",
"type_info": {
"type": "Tiny",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_generate_audio: bool",
"type_info": {
"type": "Tiny",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_duration_seconds: u32",
"type_info": {
"type": "Long",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_other_args",
"type_info": {
"type": "Blob",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "creator_ip_address",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -182,9 +192,10 @@
true,
true,
true,
true,
false,
false
]
},
"hash": "4a50b7bf06061f05e769a41e547cb927bd2b3a6f67c8ceb37883b744237ba34c"
"hash": "1b69f192c3bb51bd249f97d37f209ae271d498bb46c978351b2c1d8759338bab"
}
@@ -180,7 +180,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
"max_size": 10
}
},
{
@@ -40,7 +40,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -50,7 +50,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -50,7 +50,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -60,7 +60,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -210,7 +210,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -220,7 +220,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE uploaded_video_notes\nSET\n maybe_filename = ?,\n maybe_reported_model_type = ?,\n maybe_reported_model_name = ?,\n maybe_website = ?,\n maybe_other_website = ?,\n maybe_comments = ?,\n maybe_email_address = ?,\n can_share_report = ?,\n was_scammed = ?,\n maybe_comment_update_ip_address = ?,\n version = version + 1\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
},
"hash": "4036dfd9d4e9e77e557776468c14b75f49170de1e55a1b9b9e83446a0c356e74"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: UploadedVideoToken`,\n sha1_checksum,\n filesize_bytes as `filesize_bytes: u32`,\n maybe_width as `maybe_width: u32`,\n maybe_height as `maybe_height: u32`,\n maybe_resolution,\n maybe_detected_model_family as `maybe_detected_model_family: UploadedVideoDetectedModelFamily`,\n maybe_detected_model_type as `maybe_detected_model_type: UploadedVideoDetectedModelType`,\n maybe_report,\n upload_ip_address,\n maybe_updated_ip_address,\n created_at,\n updated_at\nFROM uploaded_videos\nWHERE sha1_checksum = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: UploadedVideoToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "sha1_checksum",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 2,
"name": "filesize_bytes: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 3,
"name": "maybe_width: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 4,
"name": "maybe_height: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "maybe_resolution",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 48
}
},
{
"ordinal": 6,
"name": "maybe_detected_model_family: UploadedVideoDetectedModelFamily",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_detected_model_type: UploadedVideoDetectedModelType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_report",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 9,
"name": "upload_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 10,
"name": "maybe_updated_ip_address",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 12,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "408c893a0ced805de4c3c6cfdf0c6a4bcc93dd1eaf98382522eae46e72eb3aa5"
}
@@ -40,7 +40,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -50,7 +50,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT status as `status: enums::common::job_status_plus::JobStatusPlus`\nFROM generic_inference_jobs\nWHERE token = ?\nFOR UPDATE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "status: enums::common::job_status_plus::JobStatusPlus",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 76
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "489c6ef00507330fedbaf2b6e024ecf409a920efd85f75456829a8ed412d9bc3"
}
@@ -180,7 +180,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
"max_size": 10
}
},
{
@@ -30,7 +30,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 3
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -30,7 +30,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 3
}
}
],
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
"max_size": 10
}
},
{
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: UploadedVideoToken`,\n sha1_checksum,\n filesize_bytes as `filesize_bytes: u32`,\n maybe_width as `maybe_width: u32`,\n maybe_height as `maybe_height: u32`,\n maybe_resolution,\n maybe_detected_model_family as `maybe_detected_model_family: UploadedVideoDetectedModelFamily`,\n maybe_detected_model_type as `maybe_detected_model_type: UploadedVideoDetectedModelType`,\n maybe_report,\n upload_ip_address,\n maybe_updated_ip_address,\n created_at,\n updated_at\nFROM uploaded_videos\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: UploadedVideoToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "sha1_checksum",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 2,
"name": "filesize_bytes: u32",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 3,
"name": "maybe_width: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 4,
"name": "maybe_height: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "maybe_resolution",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 48
}
},
{
"ordinal": 6,
"name": "maybe_detected_model_family: UploadedVideoDetectedModelFamily",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_detected_model_type: UploadedVideoDetectedModelType",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 8,
"name": "maybe_report",
"type_info": {
"type": "Blob",
"flags": "BLOB | BINARY",
"char_set": 224,
"max_size": 67108860
}
},
{
"ordinal": 9,
"name": "upload_ip_address",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 10,
"name": "maybe_updated_ip_address",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 11,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 12,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "73d70c5ee77f5059801d324610a80b3f52da3ddec2e89f63e8f41fc68ff0aaec"
}
@@ -60,7 +60,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -70,7 +70,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n \n maybe_model_type = ?,\n maybe_generation_provider = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n\n maybe_generation_mode = ?,\n maybe_aspect_ratio = ?,\n maybe_resolution = ?,\n maybe_batch_count = ?,\n maybe_generate_audio = ?,\n maybe_duration_seconds = ?,\n\n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
"query": "\nINSERT INTO prompts\nSET\n token = ?,\n prompt_type = ?,\n\n maybe_creator_user_token = ?,\n \n maybe_model_type = ?,\n maybe_generation_provider = ?,\n\n maybe_positive_prompt = ?,\n maybe_negative_prompt = ?,\n\n maybe_generation_mode = ?,\n maybe_aspect_ratio = ?,\n maybe_resolution = ?,\n maybe_bitrate = ?,\n maybe_batch_count = ?,\n maybe_generate_audio = ?,\n maybe_duration_seconds = ?,\n\n maybe_other_args = ?,\n\n creator_ip_address = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 15
"Right": 16
},
"nullable": []
},
"hash": "ccf92ebe0328d3019ac6b01be4b442df706a183362fa7e5c331f5a4bf1a74a00"
"hash": "7519c1dcc2946f4c31a8aad995bbb66a5519f6ee571797a6c0f597c9e57986a7"
}
@@ -150,7 +150,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 3
}
},
{
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
"max_size": 10
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -200,7 +200,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 5
}
},
{
@@ -210,7 +210,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 5
}
},
{
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
"max_size": 10
}
},
{
@@ -50,7 +50,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -60,7 +60,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT status as `status: enums::common::job_status_plus::JobStatusPlus`\nFROM generic_inference_jobs\nWHERE token = ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "status: enums::common::job_status_plus::JobStatusPlus",
"type_info": {
"type": "String",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | ENUM",
"char_set": 224,
"max_size": 76
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "9312694d1651235b06d1744d904b59f64d66449b8927c1606bb08bd5fdc15c7e"
}
@@ -140,7 +140,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: ApiKeyToken`,\n LEFT(api_key, 20) as `api_key_prefix!`,\n name,\n maybe_description,\n owner_user_token as `owner_user_token: UserToken`,\n ip_address_creation,\n ip_address_update,\n created_at,\n updated_at,\n maybe_deleted_at\nFROM api_keys\nWHERE api_key = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "api_key_prefix!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "maybe_description",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 4,
"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": 5,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "ip_address_update",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_deleted_at",
"type_info": {
"type": "Timestamp",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
true,
false,
false,
false,
false,
false,
true
]
},
"hash": "9b30c67842e4c408c9002fc116dc86705484268efcadb3100ef13244bcd8f922"
}
@@ -150,7 +150,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 3
}
},
{
@@ -30,7 +30,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 3
}
},
{
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
"max_size": 10
}
},
{
@@ -270,7 +270,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO uploaded_video_notes\nSET\n token = ?,\n uploaded_video_token = ?,\n maybe_filename = ?,\n maybe_reported_model_type = ?,\n maybe_reported_model_name = ?,\n maybe_website = ?,\n maybe_other_website = ?,\n maybe_comments = ?,\n maybe_email_address = ?,\n can_share_report = ?,\n was_scammed = ?,\n comment_create_ip_address = ?,\n created_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "b1f30a79864351a28a58cae43ee38ff12a8d64d9393b8f27590fe03b0ae3ab05"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE api_keys\nSET\n maybe_description = ?,\n ip_address_update = ?\nWHERE token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "b72d951fc8b60e3622bb981dd5cd66ce86c6c2d788c5606d2df80a3d07e588fe"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO api_keys\nSET\n token = ?,\n api_key = ?,\n name = ?,\n maybe_description = ?,\n owner_user_token = ?,\n ip_address_creation = ?,\n ip_address_update = ?,\n created_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "bb39045ffcc9d3a9e150b32e7ff884dccbae60101deeaaad23ccd430b25bb5a4"
}
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n token as `token: ApiKeyToken`,\n LEFT(api_key, 20) as `api_key_prefix!`,\n name,\n maybe_description,\n owner_user_token as `owner_user_token: UserToken`,\n ip_address_creation,\n ip_address_update,\n created_at,\n updated_at,\n maybe_deleted_at\nFROM api_keys\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "api_key_prefix!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 3,
"name": "maybe_description",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 2048
}
},
{
"ordinal": 4,
"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": 5,
"name": "ip_address_creation",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 6,
"name": "ip_address_update",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 160
}
},
{
"ordinal": 7,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 9,
"name": "maybe_deleted_at",
"type_info": {
"type": "Timestamp",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
true,
false,
false,
false,
false,
false,
true
]
},
"hash": "bb542b8a86cf2bf770daf29e09a92be981b307810241dd3cc1a7225a83f6824c"
}
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
"max_size": 10
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -200,7 +200,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 5
}
},
{
@@ -210,7 +210,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 5
}
},
{
@@ -150,7 +150,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 3
}
},
{
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE api_keys\nSET maybe_deleted_at = NOW()\nWHERE token = ?\n AND maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "c28c716aa08601b208c727102680bfa91025f0e3958390ed9e27e521d38b5805"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE uploaded_videos\nSET\n maybe_filename = ?,\n maybe_width = ?,\n maybe_height = ?,\n maybe_resolution = ?,\n maybe_detected_model_family = ?,\n maybe_detected_model_type = ?,\n maybe_report = ?,\n maybe_updated_ip_address = ?,\n version = version + 1\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "c6bbdc963e0d49ac55973a728d04b371967c20bfb9dd4bd75e4693dc7a114c99"
}
@@ -180,7 +180,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -140,7 +140,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -180,7 +180,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -0,0 +1,47 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n api_keys.token as `api_key_token: ApiKeyToken`,\n users.token as `user_token: UserToken`,\n users.is_banned\nFROM api_keys\nINNER JOIN users\n ON users.token = api_keys.owner_user_token\nWHERE api_keys.api_key = ?\n AND api_keys.maybe_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "api_key_token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "is_banned",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "f52c495a7ff5cafb357f79c4cdb503bbbef045fd193a5a37a66180fcac77f363"
}
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
@@ -140,7 +140,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
"max_size": 10
}
},
{
+1 -1
View File
@@ -58,7 +58,7 @@ artcraft/
- Rust with no minimum supported version
- Actix-web for HTTP services
- SQLx for MySQL and SQLite; prefer `sqlx::query!` / `sqlx::query_as!` compile-time checked macros over runtime `sqlx::query()` whenever possible
- SQLx for MySQL and SQLite. MySQL queries belong in the `mysql_queries` crate, not in application/service crates like `storyteller-web` — call those query functions instead of embedding `sqlx::query!` / `sqlx::query()` in handlers. (Query-writing conventions live in `crates/schema/database/mysql_queries/CLAUDE.md`.)
- A mix of wreq and reqwest for Rust HTTP clients
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead
- When two crates export the same type name, alias imports with a suffix: `use foo::Bar as BarFoo;`
Generated
+531 -564
View File
File diff suppressed because it is too large Load Diff
+13 -5
View File
@@ -7,8 +7,10 @@ members = [
# API clients for external services
# These might be custom implementations or wrappers over other client libraries.
"crates/api_clients/artcraft/artcraft_api_defs",
"crates/api_clients/artcraft/artcraft_api_keys",
"crates/api_clients/artcraft/artcraft_client",
"crates/api_clients/artcraft/artcraft_router",
"crates/api_clients/bucket_client",
"crates/api_clients/cloud_storage",
"crates/api_clients/comet_api_client",
"crates/api_clients/datadog_client",
@@ -86,6 +88,7 @@ members = [
"crates/lib/pager",
"crates/lib/password",
"crates/lib/server_environment",
"crates/lib/sha1_hash",
"crates/lib/sqlx_mysql_helpers",
"crates/lib/storyteller_root",
"crates/lib/url_config",
@@ -93,6 +96,7 @@ members = [
"crates/lib/user_input_common",
"crates/lib/users",
"crates/lib/uuid_utils",
"crates/lib/video_info",
#"crates/lib/api_clients/fakeyou_client",
#"crates/lib/deprecated/machine_learning/ml_models",
@@ -232,9 +236,11 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
actix_artcraft = { path = "crates/lib/actix_artcraft" }
actix_helpers = { path = "crates/lib/actix_helpers" }
artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
artcraft_api_keys = { path = "crates/api_clients/artcraft/artcraft_api_keys" }
artcraft_client = { path = "crates/api_clients/artcraft/artcraft_client" }
artcraft_router = { path = "crates/api_clients/artcraft/artcraft_router" }
bootstrap = { path = "crates/lib/bootstrap" }
bucket_client = { path = "crates/api_clients/bucket_client" }
bucket_paths = { path = "crates/schema/buckets/bucket_paths" }
build_metadata = { path = "crates/lib/build_metadata" }
cloud_storage = { path = "crates/api_clients/cloud_storage" }
@@ -279,6 +285,7 @@ openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
seedance2pro_client = { path = "crates/api_clients/seedance2pro_client" }
server_environment = { path = "crates/lib/server_environment" }
sha1_hash = { path = "crates/lib/sha1_hash" }
shared_service_components = { path = "crates/lib/shared_service_components" }
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
subprocess_common = { path = "crates/lib/deprecated/subprocess_common" }
@@ -288,6 +295,7 @@ tokens = { path = "crates/schema/public/tokens" }
url_utils = { path = "crates/lib/url_utils" }
users = { path = "crates/lib/users" }
uuid_utils = { path = "crates/lib/uuid_utils" }
video_info = { path = "crates/lib/video_info" }
web_base64 = { path = "crates/lib/data/web_base64" }
worldlabs_api_client = { path = "crates/api_clients/worldlabs_api_client" }
worldlabs_consumer_client = { path = "crates/api_clients/worldlabs_consumer_client" }
@@ -342,11 +350,11 @@ sha2 = "0.11.0-rc.5"
sha3 = "0.10.8"
speculoos = "0.11.0"
strum = { version = "0.28.0", features = ["derive"] }
tauri = { version = "2.8.3", features = ["unstable", "devtools", "protocol-asset"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
tauri-plugin-dialog = "2.3.3"
tauri-plugin-fs = "2.4.2"
tauri-plugin-log = "2.6.0"
tauri-plugin-upload = "2.3.1"
tauri = { version = "2.11.2", features = ["unstable", "devtools", "protocol-asset"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
tauri-plugin-dialog = "2.7.1"
tauri-plugin-fs = "2.5.1"
tauri-plugin-log = "2.8.0"
tauri-plugin-upload = "2.4.0"
tempdir = "0.3.7"
tempfile = "3.27.0"
thiserror = "2.0.18"
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS api_keys;
@@ -0,0 +1,50 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE api_keys (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- The actual API key value presented by clients to authenticate.
api_key VARCHAR(255) NOT NULL,
-- ========== KEY INFO ==========
-- User-supplied display name for the key.
name VARCHAR(255) NOT NULL,
-- Optional user-supplied description.
maybe_description VARCHAR(512) DEFAULT NULL,
-- ========== OWNER ==========
-- The user that owns this API key.
owner_user_token VARCHAR(32) NOT NULL,
-- ========== IP ADDRESSES ==========
-- IP address that created the key. Wide enough for IPv4/IPv6.
ip_address_creation VARCHAR(40) NOT NULL,
-- IP address that last updated the key. Wide enough for IPv4/IPv6.
ip_address_update VARCHAR(40) NOT NULL,
-- ========== 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 key is live.
maybe_deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (api_key),
KEY fk_owner_user_token (owner_user_token),
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 uploaded_videos;
@@ -0,0 +1,62 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE uploaded_videos (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- ========== FILE ==========
-- SHA-1 checksum of the uploaded bytes (40 hex chars). De-dupes uploads.
sha1_checksum VARCHAR(40) NOT NULL,
-- Size of the uploaded file in bytes.
filesize_bytes INT(10) UNSIGNED NOT NULL,
-- Pixel dimensions, when they could be determined.
maybe_width INT(10) UNSIGNED DEFAULT NULL,
maybe_height INT(10) UNSIGNED DEFAULT NULL,
-- Resolution label (e.g. "1080p", "1920x1080"), when known.
maybe_resolution VARCHAR(12) DEFAULT NULL,
-- ========== DETECTION ==========
-- Detected provenance family (e.g. "seedance", "veo", "sora", "dreamina", "kling").
maybe_detected_model_family VARCHAR(32) DEFAULT NULL,
-- Detected model type / variant (e.g. "full", "fast", "mini").
maybe_detected_model_type VARCHAR(32) DEFAULT NULL,
-- Free-form report / analysis blob for this upload.
maybe_report MEDIUMTEXT DEFAULT NULL,
-- ========== IP ADDRESSES ==========
-- IP address that uploaded the video. Wide enough for IPv4/IPv6.
upload_ip_address VARCHAR(40) NOT NULL,
-- IP address that last updated the row. Wide enough for IPv4/IPv6.
maybe_updated_ip_address VARCHAR(40) DEFAULT NULL,
-- ========== VERSION ==========
-- Optimistic-concurrency "vector clock". Bump by 1 on every update — set in
-- the UPDATE statement (MySQL can't auto-increment a plain column via schema).
version INT(8) UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (sha1_checksum),
KEY index_maybe_detected_model_family (maybe_detected_model_family)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS uploaded_video_notes;
@@ -0,0 +1,69 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE uploaded_video_notes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- The uploaded video this note is about (FK to uploaded_videos.token).
uploaded_video_token VARCHAR(32) NOT NULL,
-- ========== SUBMISSION ==========
-- Original filename of the submitted clip, as provided by the user.
maybe_filename VARCHAR(512) DEFAULT NULL,
-- The model type the submitter claims this video is.
maybe_reported_model_type VARCHAR(32) DEFAULT NULL,
-- Free-form custom model name when the submitter's model isn't a known type.
maybe_reported_model_name VARCHAR(64) DEFAULT NULL,
-- The website / platform the video was obtained from.
maybe_website VARCHAR(32) DEFAULT NULL,
-- Free-form website when it isn't a known option.
maybe_other_website VARCHAR(128) DEFAULT NULL,
-- Free-form comments from the submitter.
maybe_comments VARCHAR(1024) DEFAULT NULL,
-- ========== CONTACT & FLAGS ==========
-- Optional email address for follow-up.
maybe_email_address VARCHAR(255) DEFAULT NULL,
-- Whether the submitter consents to us sharing the report.
can_share_report BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the submitter believes they were scammed.
was_scammed BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== IP ADDRESSES ==========
-- IP address that submitted the note. Wide enough for IPv4/IPv6.
comment_create_ip_address VARCHAR(40) NOT NULL,
-- IP address that last updated the note. Wide enough for IPv4/IPv6.
maybe_comment_update_ip_address VARCHAR(40) DEFAULT NULL,
-- ========== VERSION ==========
-- Optimistic-concurrency "vector clock". Bump by 1 on every update — set in
-- the UPDATE statement (MySQL can't auto-increment a plain column via schema).
version INT(8) UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY fk_uploaded_video_token (uploaded_video_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE uploaded_videos
DROP COLUMN maybe_filename;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE uploaded_videos
ADD COLUMN maybe_filename VARCHAR(512) DEFAULT NULL AFTER filesize_bytes;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE prompts
DROP COLUMN maybe_bitrate;
@@ -0,0 +1,8 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Add bitrate parameter to prompts table.
-- Represented by the enum `CommonBitrate` (normal, high).
ALTER TABLE prompts
ADD COLUMN maybe_bitrate VARCHAR(16) DEFAULT NULL;
@@ -0,0 +1,50 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE api_keys (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- The actual API key value presented by clients to authenticate.
api_key VARCHAR(255) NOT NULL,
-- ========== KEY INFO ==========
-- User-supplied display name for the key.
name VARCHAR(255) NOT NULL,
-- Optional user-supplied description.
maybe_description VARCHAR(512) DEFAULT NULL,
-- ========== OWNER ==========
-- The user that owns this API key.
owner_user_token VARCHAR(32) NOT NULL,
-- ========== IP ADDRESSES ==========
-- IP address that created the key. Wide enough for IPv4/IPv6.
ip_address_creation VARCHAR(40) NOT NULL,
-- IP address that last updated the key. Wide enough for IPv4/IPv6.
ip_address_update VARCHAR(40) NOT NULL,
-- ========== 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 key is live.
maybe_deleted_at TIMESTAMP NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (api_key),
KEY fk_owner_user_token (owner_user_token),
KEY index_maybe_deleted_at (maybe_deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -72,6 +72,10 @@ CREATE TABLE prompts (
-- The quality setting (eg. high, medium, low)
maybe_quality VARCHAR(16) DEFAULT NULL,
-- Represented by the enum `CommonBitrate`.
-- The output bitrate setting (eg. normal, high)
maybe_bitrate VARCHAR(16) DEFAULT NULL,
-- The duration in seconds
maybe_duration_seconds INT UNSIGNED DEFAULT NULL,
@@ -0,0 +1,69 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE uploaded_video_notes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- The uploaded video this note is about (FK to uploaded_videos.token).
uploaded_video_token VARCHAR(32) NOT NULL,
-- ========== SUBMISSION ==========
-- Original filename of the submitted clip, as provided by the user.
maybe_filename VARCHAR(512) DEFAULT NULL,
-- The model type the submitter claims this video is.
maybe_reported_model_type VARCHAR(32) DEFAULT NULL,
-- Free-form custom model name when the submitter's model isn't a known type.
maybe_reported_model_name VARCHAR(64) DEFAULT NULL,
-- The website / platform the video was obtained from.
maybe_website VARCHAR(32) DEFAULT NULL,
-- Free-form website when it isn't a known option.
maybe_other_website VARCHAR(128) DEFAULT NULL,
-- Free-form comments from the submitter.
maybe_comments VARCHAR(1024) DEFAULT NULL,
-- ========== CONTACT & FLAGS ==========
-- Optional email address for follow-up.
maybe_email_address VARCHAR(255) DEFAULT NULL,
-- Whether the submitter consents to us sharing the report.
can_share_report BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the submitter believes they were scammed.
was_scammed BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== IP ADDRESSES ==========
-- IP address that submitted the note. Wide enough for IPv4/IPv6.
comment_create_ip_address VARCHAR(40) NOT NULL,
-- IP address that last updated the note. Wide enough for IPv4/IPv6.
maybe_comment_update_ip_address VARCHAR(40) DEFAULT NULL,
-- ========== VERSION ==========
-- Optimistic-concurrency "vector clock". Bump by 1 on every update — set in
-- the UPDATE statement (MySQL can't auto-increment a plain column via schema).
version INT(8) UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
KEY fk_uploaded_video_token (uploaded_video_token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,65 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE uploaded_videos (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective "primary key" (used internally for lookups and as a foreign key).
token VARCHAR(32) NOT NULL,
-- ========== FILE ==========
-- SHA-1 checksum of the uploaded bytes (40 hex chars). De-dupes uploads.
sha1_checksum VARCHAR(40) NOT NULL,
-- Size of the uploaded file in bytes.
filesize_bytes INT(10) UNSIGNED NOT NULL,
-- Original filename of the uploaded clip, as provided by the user.
maybe_filename VARCHAR(512) DEFAULT NULL,
-- Pixel dimensions, when they could be determined.
maybe_width INT(10) UNSIGNED DEFAULT NULL,
maybe_height INT(10) UNSIGNED DEFAULT NULL,
-- Resolution label (e.g. "1080p", "1920x1080"), when known.
maybe_resolution VARCHAR(12) DEFAULT NULL,
-- ========== DETECTION ==========
-- Detected provenance family (e.g. "seedance", "veo", "sora", "dreamina", "kling").
maybe_detected_model_family VARCHAR(32) DEFAULT NULL,
-- Detected model type / variant (e.g. "full", "fast", "mini").
maybe_detected_model_type VARCHAR(32) DEFAULT NULL,
-- Free-form report / analysis blob for this upload.
maybe_report MEDIUMTEXT DEFAULT NULL,
-- ========== IP ADDRESSES ==========
-- IP address that uploaded the video. Wide enough for IPv4/IPv6.
upload_ip_address VARCHAR(40) NOT NULL,
-- IP address that last updated the row. Wide enough for IPv4/IPv6.
maybe_updated_ip_address VARCHAR(40) DEFAULT NULL,
-- ========== VERSION ==========
-- Optimistic-concurrency "vector clock". Bump by 1 on every update — set in
-- the UPDATE statement (MySQL can't auto-increment a plain column via schema).
version INT(8) UNSIGNED NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (sha1_checksum),
KEY index_maybe_detected_model_family (maybe_detected_model_family)
) 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,42 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::api_keys::ApiKeyToken;
use tokens::tokens::users::UserToken;
/// Canonical wire shape for an API key, used by the list rows and the
/// single-key GET.
///
/// NB: the full secret `api_key` is NEVER included here — only
/// `truncated_api_key` (its first 20 characters), enough to disambiguate keys
/// in a UI. The full value is returned exactly once, at creation time.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct ApiKeyInfo {
pub token: ApiKeyToken,
/// First 20 characters of the API key. The full secret is never returned
/// after creation.
pub truncated_api_key: String,
pub name: String,
pub maybe_description: Option<String>,
pub owner_user_token: UserToken,
pub ip_address_creation: String,
pub ip_address_update: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
/// Soft-delete timestamp. `None` for live keys.
pub maybe_deleted_at: Option<DateTime<Utc>>,
}
/// Path info for endpoints addressed by the API key's `token` (NOT the secret
/// `api_key` value): `GET` / `DELETE` / `PUT` `/v1/api_keys/{api_key_token}`.
#[derive(Deserialize, ToSchema)]
pub struct ApiKeyPathInfo {
pub api_key_token: ApiKeyToken,
}
@@ -0,0 +1,28 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::api_keys::ApiKeyToken;
// ── POST /v1/api_keys/create ──
#[derive(Deserialize, ToSchema)]
pub struct CreateApiKeyRequest {
/// Display name (title) for the key.
pub name: String,
/// Optional description.
pub maybe_description: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct CreateApiKeySuccessResponse {
pub success: bool,
/// The key's `token` — used for all subsequent management
/// (get / update / delete). Safe to store and display.
pub api_key_token: ApiKeyToken,
/// The full secret API key value. This is the ONLY time it is ever
/// returned — the caller must store it now, as it cannot be retrieved again.
pub api_key: String,
}
@@ -0,0 +1,9 @@
use serde_derive::Serialize;
use utoipa::ToSchema;
// ── DELETE /v1/api_keys/{api_key_token} ──
#[derive(Serialize, ToSchema)]
pub struct DeleteApiKeySuccessResponse {
pub success: bool,
}
@@ -0,0 +1,12 @@
use serde_derive::Serialize;
use utoipa::ToSchema;
use crate::api_keys::common::ApiKeyInfo;
// ── GET /v1/api_keys/{api_key_token} ──
#[derive(Serialize, ToSchema)]
pub struct GetApiKeySuccessResponse {
pub success: bool,
pub api_key: ApiKeyInfo,
}
@@ -0,0 +1,24 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use crate::api_keys::common::ApiKeyInfo;
// ── GET /v1/api_keys/list ──
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct ListApiKeysQueryParams {
/// Page size. Defaults server-side if omitted.
pub limit: Option<u32>,
/// Number of rows to skip. Defaults to 0.
pub offset: Option<u32>,
}
#[derive(Serialize, ToSchema)]
pub struct ListApiKeysSuccessResponse {
pub success: bool,
/// The user's API keys (including soft-deleted ones), newest first. Each
/// entry's key value is truncated — the full secret is never listed.
pub api_keys: Vec<ApiKeyInfo>,
}
@@ -0,0 +1,6 @@
pub mod common;
pub mod create_api_key;
pub mod delete_api_key;
pub mod get_api_key;
pub mod list_api_keys;
pub mod update_api_key;
@@ -0,0 +1,15 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
// ── PUT /v1/api_keys/{api_key_token} ──
#[derive(Deserialize, ToSchema)]
pub struct UpdateApiKeyRequest {
/// New description for the key. `None` clears it.
pub maybe_description: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct UpdateApiKeySuccessResponse {
pub success: bool,
}
@@ -1,4 +1,5 @@
pub mod analytics;
pub mod api_keys;
pub mod characters;
pub mod common;
pub mod credits;
@@ -7,6 +8,7 @@ pub mod generate;
pub mod jobs;
pub mod media_file;
pub mod moderation;
pub mod omni_api;
pub mod omni_gen;
pub mod prompts;
pub mod stripe_artcraft;
@@ -14,4 +16,5 @@ pub mod subscriptions;
pub mod user_referral_codes;
pub mod users;
pub mod utils;
pub mod video_info;
pub mod web_referrals;
@@ -0,0 +1,3 @@
pub mod omni_upload_audio;
pub mod omni_upload_image;
pub mod omni_upload_video;
@@ -0,0 +1,15 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
/// Success response for the API-key authenticated audio upload endpoint
/// (`POST /v1/omni_api/upload/audio`).
///
/// NB: The request is a `multipart/form-data` body. Because the multipart form type is bound to the
/// web framework, it is declared alongside the handler in `storyteller_web` rather than here.
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
pub struct OmniUploadAudioMediaFileSuccessResponse {
pub success: bool,
pub media_file_token: MediaFileToken,
}
@@ -0,0 +1,15 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
/// Success response for the API-key authenticated image upload endpoint
/// (`POST /v1/omni_api/upload/image`).
///
/// NB: The request is a `multipart/form-data` body. Because the multipart form type is bound to the
/// web framework, it is declared alongside the handler in `storyteller_web` rather than here.
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
pub struct OmniUploadImageMediaFileSuccessResponse {
pub success: bool,
pub media_file_token: MediaFileToken,
}
@@ -0,0 +1,15 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::media_files::MediaFileToken;
/// Success response for the API-key authenticated video upload endpoint
/// (`POST /v1/omni_api/upload/video`).
///
/// NB: The request is a `multipart/form-data` body. Because the multipart form type is bound to the
/// web framework, it is declared alongside the handler in `storyteller_web` rather than here.
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
pub struct OmniUploadVideoMediaFileSuccessResponse {
pub success: bool,
pub media_file_token: MediaFileToken,
}
@@ -2,6 +2,7 @@ use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_bitrate::CommonBitrate;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_video_model::CommonVideoModel;
@@ -50,6 +51,10 @@ pub struct OmniGenVideoCostAndGenerateRequest {
/// The aspect ratio to use.
pub aspect_ratio: Option<CommonAspectRatio>,
/// The output bitrate to use.
/// Not all models support this; models that don't simply ignore it.
pub bitrate: Option<CommonBitrate>,
/// The quality to use.
pub quality: Option<CommonQuality>,
@@ -6,6 +6,7 @@ use enums::common::generation::model_creator::ModelCreator;
use enums::common::generation_provider::GenerationProvider;
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use enums::common::generation::common_bitrate::CommonBitrate;
/// Video model to default to if none is specified
const DEFAULT_VIDEO_MODEL : CommonVideoModel = CommonVideoModel::Seedance2p0;
@@ -150,6 +151,12 @@ pub struct OmniGenVideoModelDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution_default: Option<CommonResolution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bitrate_options: Option<Vec<CommonBitrate>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bitrate_default: Option<CommonBitrate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality_options: Option<Vec<CommonQuality>>,
@@ -220,6 +227,8 @@ impl Default for OmniGenVideoModelDetails {
aspect_ratio_default: None,
resolution_options: None,
resolution_default: None,
bitrate_options: None,
bitrate_default: None,
quality_options: None,
default_quality: None,
duration_seconds_min: None,
@@ -2,6 +2,7 @@ use crate::prompts::get_prompt::GetPromptImageContextItem;
use chrono::{DateTime, Utc};
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_bitrate::CommonBitrate;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_class::CommonModelClass;
use enums::common::generation::common_model_type::CommonModelType;
@@ -55,6 +56,9 @@ pub struct BatchPromptInfo {
/// OPTIONAL. The resolution (eg. one_k, two_k, four_k, etc.)
pub maybe_resolution: Option<CommonResolution>,
/// OPTIONAL. The output bitrate (eg. normal, high).
pub maybe_bitrate: Option<CommonBitrate>,
/// OPTIONAL. The number of outputs to generate (0-255).
pub maybe_batch_count: Option<u8>,
@@ -3,6 +3,7 @@ use chrono::{DateTime, Utc};
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_bitrate::CommonBitrate;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation::common_resolution::CommonResolution;
@@ -59,6 +60,9 @@ pub struct PromptInfo {
/// OPTIONAL. The resolution (eg. one_k, two_k, four_k, etc.)
pub maybe_resolution: Option<CommonResolution>,
/// OPTIONAL. The output bitrate (eg. normal, high).
pub maybe_bitrate: Option<CommonBitrate>,
/// OPTIONAL. The number of outputs to generate (0-255).
pub maybe_batch_count: Option<u8>,
@@ -0,0 +1,3 @@
pub mod notes;
pub mod read_only;
pub mod upload;
@@ -0,0 +1,60 @@
//! Request/response types for `POST /v1/video_info/notes`.
//!
//! A note is user-submitted context about an uploaded video (what model they
//! believe it is, where it came from, free-form comments). Always tied to an
//! [`UploadedVideoToken`]; pass a [`UploadVideoNoteToken`] to update an existing
//! note instead of creating a new one.
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::by_table::uploaded_video_notes::uploaded_video_note_reported_model_type::UploadedVideoNoteReportedModelType;
use enums::by_table::uploaded_video_notes::uploaded_video_note_reported_website::UploadedVideoNoteReportedWebsite;
use tokens::tokens::uploaded_video_notes::UploadVideoNoteToken;
use tokens::tokens::uploaded_videos::UploadedVideoToken;
/// Request body for `POST /v1/video_info/notes`.
#[derive(Deserialize, ToSchema, Debug, Clone)]
pub struct VideoInfoNoteRequest {
/// The uploaded video this note is about. Required.
pub uploaded_video_token: UploadedVideoToken,
/// If present, update this existing note instead of creating a new one.
pub maybe_uploaded_video_note_token: Option<UploadVideoNoteToken>,
/// Original filename of the submitted clip.
pub maybe_filename: Option<String>,
/// The model type the submitter claims this video is (dropdown / enum).
pub maybe_reported_model_type: Option<UploadedVideoNoteReportedModelType>,
/// Free-form custom model name (trimmed; empty becomes absent).
pub maybe_reported_model_name: Option<String>,
/// The website / platform the video came from (dropdown / enum).
pub maybe_website: Option<UploadedVideoNoteReportedWebsite>,
/// Free-form website (trimmed; empty becomes absent).
pub maybe_other_website: Option<String>,
/// Free-form comments (trimmed; empty becomes absent).
pub maybe_comments: Option<String>,
/// Optional contact email (trimmed; empty becomes absent).
pub maybe_email_address: Option<String>,
/// Whether the submitter consents to us sharing the report. Defaults to false.
pub can_share_report: Option<bool>,
/// Whether the submitter believes they were scammed. Defaults to false.
pub was_scammed: Option<bool>,
}
/// Response body for `POST /v1/video_info/notes`.
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct VideoInfoNoteResponse {
pub success: bool,
/// The created or updated note's token (e.g. `uvnote_…`).
pub uploaded_video_note_token: UploadVideoNoteToken,
}
@@ -0,0 +1,167 @@
//! Response types for `POST /v1/video_info/read_only`.
//!
//! The endpoint accepts a `multipart/form-data` video upload, inspects its
//! embedded provenance (C2PA manifests, AIGC labels, in-stream watermarks), and
//! returns what it found. These are the public, wire-stable API types — they
//! deliberately mirror, but do not re-export, the internal `video_info` crate
//! types so the parsing library can evolve independently of the HTTP contract.
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
/// Response for `POST /v1/video_info/read_only`.
///
/// [`kind`](Self::kind) names the detected provenance family; the matching
/// `maybe_*` field carries its details (all others are `null`). `unrecognized`
/// means no supported provenance was found — [`maybe_encoder`](Self::maybe_encoder)
/// still reports the container encoder tag when present (an `Lavf…` value means
/// the file was re-encoded through ffmpeg, which strips provenance).
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct VideoInfoReadOnlyResponse {
pub success: bool,
/// Which provenance family was detected (if any).
pub kind: VideoProvenanceKind,
/// The container `©too` encoder tag, e.g. `"Lavf60.16.100"` or `"Google"`.
pub maybe_encoder: Option<String>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Seedance`].
pub maybe_seedance: Option<SeedanceVideoInfo>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Veo`].
pub maybe_veo: Option<VeoVideoInfo>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Sora`].
pub maybe_sora: Option<SoraVideoInfo>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Dreamina`].
pub maybe_dreamina: Option<DreaminaVideoInfo>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Kling`].
pub maybe_kling: Option<KlingVideoInfo>,
}
/// The recognized AI-video provenance family.
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VideoProvenanceKind {
/// ByteDance Seedance API (Volcengine / BytePlus C2PA).
Seedance,
/// Google Veo (Google Generative AI video C2PA).
Veo,
/// OpenAI Sora (OpenAI Generative AI video C2PA).
Sora,
/// Dreamina (ByteDance/CapCut consumer app `ilst` metadata).
Dreamina,
/// Kling (Kuaishou AIGC-label / in-stream watermark).
Kling,
/// No supported provenance was found (often a re-encoded / stripped file).
Unrecognized,
}
/// Seedance (ByteDance) provenance from an embedded C2PA manifest.
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct SeedanceVideoInfo {
/// `"volcengine"` or `"byteplus"`.
pub platform: String,
/// C2PA `softwareAgent`, e.g. `"Volcengine_Ark_CN"`.
pub software_agent: String,
pub maybe_software_agent_version: Option<String>,
/// Full model identifier, e.g. `"doubao-seedance-2-0-fast"`.
pub model_name: String,
pub maybe_model_brand: Option<String>,
/// Parsed version, e.g. `"2.0"` / `"1.0"`.
pub maybe_model_version: Option<String>,
pub is_fast: bool,
pub is_lite: bool,
/// Raw generation timestamp string (RFC 3339).
pub generated_at: String,
/// Normalized RFC 3339 timestamp, when parseable.
pub maybe_generated_at_utc: Option<String>,
pub maybe_log_id: Option<String>,
pub maybe_log_id_decoded_hex: Option<String>,
pub maybe_digital_source_type: Option<String>,
pub maybe_claim_generator: Option<String>,
pub maybe_claim_generator_version: Option<String>,
pub maybe_manifest_id: Option<String>,
pub maybe_instance_id: Option<String>,
pub maybe_signer_email: Option<String>,
pub maybe_signer_org_id: Option<String>,
pub maybe_signer_country: Option<String>,
pub maybe_cert_serial: Option<String>,
}
/// Google Veo provenance.
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct VeoVideoInfo {
/// Always `"Google"`.
pub producer: String,
/// Whether a full Google C2PA manifest is present (`false` = recognized only
/// by the `encoder=Google` tag, C2PA likely stripped by a re-encode).
pub has_c2pa_manifest: bool,
pub maybe_encoder: Option<String>,
pub maybe_created_description: Option<String>,
pub has_synthid_watermark: bool,
pub maybe_synthid_description: Option<String>,
pub maybe_digital_source_type: Option<String>,
pub maybe_claim_generator: Option<String>,
pub maybe_claim_generator_version: Option<String>,
pub maybe_manifest_id: Option<String>,
pub maybe_instance_id: Option<String>,
pub maybe_cert_serial: Option<String>,
/// Issuing intermediate-CA common name, e.g. `"Google C2PA Media Services 1P ICA G3"`.
pub maybe_signer_ca: Option<String>,
/// Whether the manifest carries an RFC 3161 trusted timestamp.
pub is_timestamped: bool,
pub maybe_timestamp_authority: Option<String>,
}
/// OpenAI Sora provenance.
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct SoraVideoInfo {
/// Always `"OpenAI"`.
pub producer: String,
pub maybe_model_name: Option<String>,
pub maybe_created_description: Option<String>,
pub maybe_digital_source_type: Option<String>,
pub maybe_claim_generator: Option<String>,
pub maybe_manifest_id: Option<String>,
pub maybe_instance_id: Option<String>,
pub maybe_cert_serial: Option<String>,
}
/// Dreamina (ByteDance/CapCut app) provenance from `ilst` metadata.
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct DreaminaVideoInfo {
/// The product string, e.g. `"dreamina"`.
pub product: String,
pub maybe_export_type: Option<String>,
pub maybe_os: Option<String>,
pub maybe_source_info: Option<String>,
pub maybe_aigc_label_type: Option<i64>,
pub maybe_video_id: Option<String>,
pub has_c2pa: bool,
pub maybe_signer_org_id: Option<String>,
pub maybe_signer_country: Option<String>,
pub maybe_cert_serial: Option<String>,
}
/// Kling (Kuaishou) provenance from the AIGC label and/or in-stream watermark.
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct KlingVideoInfo {
/// Model version when a clean export embeds it (e.g. `"2.5-pro"`); usually `null`.
pub maybe_model_version: Option<String>,
/// Whether labeled AI-generated (`Label == "1"`, per China GB 45438).
pub is_ai_generated: bool,
pub maybe_label: Option<String>,
pub maybe_content_producer: Option<String>,
pub maybe_produce_id: Option<String>,
pub maybe_content_propagator: Option<String>,
pub maybe_propagate_id: Option<String>,
pub maybe_reserved_code_1: Option<String>,
pub maybe_reserved_code_2: Option<String>,
/// Whether the in-stream `kling-ai` SEI watermark is present.
pub has_stream_watermark: bool,
pub maybe_watermark_uuid: Option<String>,
}
@@ -0,0 +1,46 @@
//! Response type for `POST /v1/video_info/upload`.
//!
//! Same detected provenance as the read-only endpoint, plus the persisted
//! [`UploadedVideoToken`] (used for follow-up requests, e.g. attaching a note).
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::uploaded_videos::UploadedVideoToken;
use crate::video_info::read_only::{
DreaminaVideoInfo, KlingVideoInfo, SeedanceVideoInfo, SoraVideoInfo, VeoVideoInfo,
VideoProvenanceKind,
};
/// Response for `POST /v1/video_info/upload`. Mirrors
/// [`crate::video_info::read_only::VideoInfoReadOnlyResponse`] and adds the
/// persisted record's token.
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)]
pub struct VideoInfoUploadResponse {
pub success: bool,
/// The persisted `uploaded_videos` record token (e.g. `uvt_…`).
pub uploaded_video_token: UploadedVideoToken,
/// Which provenance family was detected (if any).
pub kind: VideoProvenanceKind,
/// The container `©too` encoder tag, e.g. `"Lavf60.16.100"` or `"Google"`.
pub maybe_encoder: Option<String>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Seedance`].
pub maybe_seedance: Option<SeedanceVideoInfo>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Veo`].
pub maybe_veo: Option<VeoVideoInfo>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Sora`].
pub maybe_sora: Option<SoraVideoInfo>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Dreamina`].
pub maybe_dreamina: Option<DreaminaVideoInfo>,
/// Present when [`kind`](Self::kind) is [`VideoProvenanceKind::Kling`].
pub maybe_kling: Option<KlingVideoInfo>,
}
@@ -0,0 +1,20 @@
[package]
name = "artcraft_api_keys"
edition = "2024"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "artcraft_api_keys"
path = "src/lib.rs"
[dependencies]
# Hackari workspace (faster compile times)
workspace-build-acceleration.workspace = true
@@ -0,0 +1,104 @@
use std::fmt;
use crate::parse_from_header_value::parse_from_header_value;
pub mod parse_from_header_value;
/// Number of leading characters shown when an [`ArtcraftApiKey`] is formatted. The remainder of
/// the secret is redacted so the full key never lands in logs or error messages.
const REDACTED_PREFIX_CHAR_COUNT: usize = 20;
/// An Artcraft API key (e.g. `artcraft_api_<entropy>`).
///
/// `Debug` and `Display` both redact the value, printing only the first
/// [`REDACTED_PREFIX_CHAR_COUNT`] characters followed by an ellipsis so the full secret is never
/// accidentally logged.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ArtcraftApiKey(pub String);
impl ArtcraftApiKey {
pub fn new_from_str(value: &str) -> ArtcraftApiKey {
ArtcraftApiKey(value.to_string())
}
/// Parse an [`ArtcraftApiKey`] from the value of an HTTP `Authorization` header, accepting the
/// `Bearer <key>`, `Key <key>`, and bare `<key>` forms. See [`parse_from_header_value`] for the
/// full parsing rules.
pub fn parse_from_authorization_header_value(header_value: &str) -> Option<ArtcraftApiKey> {
parse_from_header_value(header_value)
}
/// The full, unredacted key value as a borrowed `&str`.
///
/// NB: unlike `Display`/`Debug` (which redact), this exposes the complete secret — the
/// `_be_careful` suffix is a reminder to use it only where the real value is genuinely needed
/// (storage, the create-key response, tests), never in logs or error messages.
pub fn as_str_be_careful(&self) -> &str {
&self.0
}
/// The full, unredacted key value as an owned `String`. See [`Self::as_str_be_careful`] for the
/// caveat behind the `_be_careful` suffix.
pub fn to_string_be_careful(&self) -> String {
self.0.clone()
}
/// The redacted form used by both `Debug` and `Display`: the first
/// [`REDACTED_PREFIX_CHAR_COUNT`] characters followed by an ellipsis.
fn redacted(&self) -> String {
let prefix: String = self.0.chars().take(REDACTED_PREFIX_CHAR_COUNT).collect();
format!("{prefix}…")
}
}
impl fmt::Display for ArtcraftApiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.redacted())
}
}
impl fmt::Debug for ArtcraftApiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ArtcraftApiKey({:?})", self.redacted())
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_KEY: &str = "artcraft_api_55ax0zhd580m598r6n4n7szdwjb2b28sypapvawh";
#[test]
fn new_from_str_and_as_str_round_trip() {
let key = ArtcraftApiKey::new_from_str(SAMPLE_KEY);
assert_eq!(key.as_str_be_careful(), SAMPLE_KEY);
assert_eq!(key.0, SAMPLE_KEY);
}
#[test]
fn display_redacts_to_first_twenty_characters() {
let key = ArtcraftApiKey::new_from_str(SAMPLE_KEY);
assert_eq!(format!("{key}"), "artcraft_api_55ax0zh…");
}
#[test]
fn debug_redacts_to_first_twenty_characters() {
let key = ArtcraftApiKey::new_from_str(SAMPLE_KEY);
assert_eq!(format!("{key:?}"), "ArtcraftApiKey(\"artcraft_api_55ax0zh…\")");
}
#[test]
fn formatting_does_not_leak_the_full_secret() {
let key = ArtcraftApiKey::new_from_str(SAMPLE_KEY);
let suffix = &SAMPLE_KEY[REDACTED_PREFIX_CHAR_COUNT..];
assert!(!format!("{key}").contains(suffix));
assert!(!format!("{key:?}").contains(suffix));
}
#[test]
fn shorter_than_prefix_is_printed_in_full() {
let key = ArtcraftApiKey::new_from_str("abc");
assert_eq!(format!("{key}"), "abc…");
}
}
@@ -0,0 +1,125 @@
use crate::ArtcraftApiKey;
/// `Bearer <api_key>` — the widely-accepted style for most APIs.
const BEARER_SCHEME: &str = "bearer";
/// `Key <api_key>` — legacy GitHub-style / Fal-style.
const KEY_SCHEME: &str = "key";
/// Parse an [`ArtcraftApiKey`] from the value of an HTTP `Authorization` header.
///
/// The value is accepted in three forms:
///
/// - `Bearer <api_key>` (widely-accepted style for most APIs)
/// - `Key <api_key>` (legacy GitHub-style / Fal-style)
/// - `<api_key>` (AWS-style — the bare key, no scheme)
///
/// The scheme keyword is matched case-insensitively, and surrounding/inner whitespace is trimmed.
/// Returns `None` if the value is empty, carries an unsupported scheme (e.g. `Basic ...`), or names
/// a scheme with no credential.
pub fn parse_from_header_value(header_value: &str) -> Option<ArtcraftApiKey> {
let trimmed = header_value.trim();
if trimmed.is_empty() {
return None;
}
let key = match trimmed.split_once(char::is_whitespace) {
// A scheme word followed by a value: "Bearer <key>" / "Key <key>".
Some((scheme, rest)) => {
if is_supported_scheme(scheme) {
let key = rest.trim();
if key.is_empty() {
// A supported scheme with no credential.
return None;
}
key
} else {
// An unsupported scheme (e.g. "Basic ..."). A bare API key never
// contains whitespace, so a multi-token value we don't recognize is
// not a usable key.
return None;
}
}
// No whitespace: AWS-style bare API key ("<api_key>") — unless the value is
// just a bare scheme keyword (a malformed header carrying no credential).
None => {
if is_supported_scheme(trimmed) {
return None;
}
trimmed
}
};
Some(ArtcraftApiKey::new_from_str(key))
}
fn is_supported_scheme(word: &str) -> bool {
word.eq_ignore_ascii_case(BEARER_SCHEME) || word.eq_ignore_ascii_case(KEY_SCHEME)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_KEY: &str = "artcraft_api_55ax0zhd580m598r6n4n7szdwjb2b28sypapvawh";
fn parsed(header_value: &str) -> Option<String> {
parse_from_header_value(header_value).map(|key| key.to_string_be_careful())
}
#[test]
fn bearer_scheme_is_case_insensitive() {
assert_eq!(parsed("Bearer abc"), Some("abc".to_string()));
assert_eq!(parsed("bearer abc"), Some("abc".to_string()));
assert_eq!(parsed("BEARER abc"), Some("abc".to_string()));
assert_eq!(parsed("BeArEr abc"), Some("abc".to_string()));
}
#[test]
fn key_scheme_is_case_insensitive() {
assert_eq!(parsed("Key abc"), Some("abc".to_string()));
assert_eq!(parsed("key abc"), Some("abc".to_string()));
assert_eq!(parsed("KEY abc"), Some("abc".to_string()));
}
#[test]
fn bare_key_aws_style() {
assert_eq!(parsed("abc"), Some("abc".to_string()));
assert_eq!(parsed(SAMPLE_KEY), Some(SAMPLE_KEY.to_string()));
}
#[test]
fn surrounding_and_inner_whitespace_is_trimmed() {
assert_eq!(parsed(" Bearer abc "), Some("abc".to_string()));
assert_eq!(parsed("\tKey\tabc\t"), Some("abc".to_string()));
assert_eq!(parsed(" abc "), Some("abc".to_string()));
}
#[test]
fn empty_or_blank_is_none() {
assert_eq!(parsed(""), None);
assert_eq!(parsed(" "), None);
}
#[test]
fn scheme_with_no_credential_is_none() {
assert_eq!(parsed("Bearer"), None);
assert_eq!(parsed("Bearer "), None);
assert_eq!(parsed("Key"), None);
assert_eq!(parsed("key "), None);
}
#[test]
fn unsupported_scheme_is_none() {
assert_eq!(parsed("Basic dXNlcjpwYXNz"), None);
assert_eq!(parsed("Digest abc"), None);
// Two non-scheme tokens — a bare key never contains whitespace.
assert_eq!(parsed("abc def"), None);
}
#[test]
fn returns_an_artcraft_api_key() {
let key = parse_from_header_value(&format!("Bearer {SAMPLE_KEY}")).expect("should parse");
assert_eq!(key.as_str_be_careful(), SAMPLE_KEY);
}
}
@@ -3,6 +3,7 @@ pub mod character_list_ref;
pub mod image_list_ref;
pub mod image_ref;
pub mod router_aspect_ratio;
pub mod router_bitrate;
pub mod router_image_model;
pub mod router_provider;
pub mod router_quality;
@@ -0,0 +1,10 @@
use serde_derive::{Deserialize, Serialize};
/// Output bitrate levels you can specify when enqueuing a generation.
/// Not every model will use this; models that don't simply ignore it.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RouterBitrate {
Normal,
High,
}
@@ -1,6 +1,7 @@
use crate::api::audio_list_ref::AudioListRef;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_bitrate::RouterBitrate;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::image_list_ref::ImageListRef;
@@ -103,6 +104,10 @@ pub struct GenerateVideoRequestBuilder {
/// The aspect ratio to use
pub aspect_ratio: Option<RouterAspectRatio>,
/// The output bitrate to use.
/// Not all models support this; models that don't simply ignore it.
pub bitrate: Option<RouterBitrate>,
/// How many seconds to generate.
pub duration_seconds: Option<u16>,
@@ -139,6 +144,7 @@ impl Default for GenerateVideoRequestBuilder {
reference_character_tokens: None,
resolution: None,
aspect_ratio: None,
bitrate: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
@@ -1,9 +1,11 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_bitrate::RouterBitrate;
use crate::api::router_resolution::RouterResolution;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
@@ -49,6 +51,7 @@ pub fn build_artcraft_omni_video_request(
let resolution = plan_output_resolution(builder.resolution.take(), strategy, resolutions)?;
let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?;
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
let bitrate = plan_bitrate(builder.bitrate.take());
let prompt = builder.prompt.take();
let start_frame = resolve_image_ref(builder.start_frame.take())?;
@@ -71,6 +74,7 @@ pub fn build_artcraft_omni_video_request(
reference_character_tokens: reference_characters,
resolution,
aspect_ratio,
bitrate,
duration_seconds: duration_seconds.map(|d| d as u16),
video_batch_count: Some(batch_count),
negative_prompt: None,
@@ -234,6 +238,19 @@ pub fn plan_batch_count(
}
}
/// Translate the router-facing bitrate into the API `CommonBitrate`.
///
/// Bitrate does not affect cost. An unset value leaves the field `None` (the
/// API applies its default); `Normal` and `High` map to their `CommonBitrate`
/// counterparts.
fn plan_bitrate(bitrate: Option<RouterBitrate>) -> Option<CommonBitrateEnum> {
match bitrate {
None => None,
Some(RouterBitrate::Normal) => Some(CommonBitrateEnum::Normal),
Some(RouterBitrate::High) => Some(CommonBitrateEnum::High),
}
}
/// Duration: 4-15 seconds.
pub fn plan_duration(
duration_seconds: Option<u16>,
@@ -255,3 +272,27 @@ pub fn plan_duration(
},
}
}
#[cfg(test)]
mod tests {
use super::*;
mod bitrate_translation {
use super::*;
#[test]
fn none_stays_none() {
assert_eq!(plan_bitrate(None), None);
}
#[test]
fn normal_maps_to_common_normal() {
assert_eq!(plan_bitrate(Some(RouterBitrate::Normal)), Some(CommonBitrateEnum::Normal));
}
#[test]
fn high_maps_to_common_high() {
assert_eq!(plan_bitrate(Some(RouterBitrate::High)), Some(CommonBitrateEnum::High));
}
}
}
@@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0(builder: GenerateVideoRequestBuilder) -> Resu
mod tests {
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
use crate::api::router_bitrate::RouterBitrate;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -72,6 +74,12 @@ mod tests {
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
fn bitrate_passed_through() {
let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); }));
assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High));
}
}
// ── Resolution ──
@@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0_bp(builder: GenerateVideoRequestBuilder) -> R
mod tests {
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
use crate::api::router_bitrate::RouterBitrate;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -70,6 +72,12 @@ mod tests {
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
fn bitrate_passed_through() {
let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); }));
assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High));
}
}
mod resolution_tests {
@@ -24,6 +24,8 @@ pub fn build_artcraft_seedance_2p0_bp_fast(builder: GenerateVideoRequestBuilder)
mod tests {
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
use crate::api::router_bitrate::RouterBitrate;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -72,6 +74,12 @@ mod tests {
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
fn bitrate_passed_through() {
let req = unwrap_request(make_builder(|b| { b.bitrate = Some(RouterBitrate::High); }));
assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High));
}
}
mod resolution_tests {

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