Compare commits

..
Author SHA1 Message Date
Brandon Thomas a1795e2b94 move assets out of build 2026-07-02 01:10:15 -04:00
Brandon Thomas 9de8fb8979 tauri frontend - read from omni api (#1710) 2026-07-01 23:20:11 -04:00
Brandon Thomas a61fcd50ed tauri omni client (#1709) 2026-07-01 16:31:23 -04:00
Brandon Thomas c16db8793c dashboard - change icon; group sidebar (#1708) 2026-06-30 11:05:18 -04:00
Brandon Thomas b3311d1574 payment informatics 4 (#1707) 2026-06-30 10:40:28 -04:00
Brandon Thomas 35add74498 payment informatics 3 (#1706) 2026-06-30 08:19:52 -04:00
Brandon Thomas d99da56df0 payment informatics #2 (#1705) 2026-06-29 21:21:26 -04:00
Brandon Thomas 3e4f998599 payments informatics (part 1) (#1704) 2026-06-29 17:49:28 -04:00
Brandon Thomas 3bccac5b2e block usernames that look like tokens (#1703) 2026-06-27 19:47:56 -04:00
Brandon Thomas aa9e862ff2 grok fixes (#1702) 2026-06-27 13:52:51 -04:00
BFlat db871126df Webapp/website (promptbox): full editing controls in fullscreen focus mode (#1701)
Add a footer to the fullscreen prompt modal (desktop lib + webapp) so
focus mode is a complete editing surface, not just the textarea:

- Model selector, always-on reference-image row, and a Done button
  (closes the modal) instead of Generate.
- Video: also surfaces the input-mode (keyframe/reference) picker and
  the @Characters button (seedance_2p0). Shared image-row / character /
  input-mode elements are reused inline and in the modal (DRY).
- Webapp: model selector + leftToolbar + image row in the footer.

Layout: editor area is flex-1/min-h-0/overflow-hidden with the image row
and footer shrink-0, so the editor takes only the leftover space and
scrolls internally — resizing the window shorter no longer overflows.
MentionTextarea gains a `style` prop so the modal can cap resize/height.
2026-06-26 23:16:35 +07:00
BFlat aaff33955e Webapp/website: Promptbox fullscreen "focus mode" + 3D & tooltip fixes (#1700)
### Fullscreen focus mode (desktop + webapp)
- Added a top-right expand icon on every prompt editor that opens a
"Prompt" focus-mode overlay (blurred backdrop, `max-w-4xl` centered
card) for more prompting space.
- Same editor state — overlay binds to the same value + onChange, so
text/@-mentions persist. Auto-focuses with the caret at the end on open.
- Editor fills full height; @-mention dropdown no longer clipped; closes
on Esc / backdrop click.
- Reused `@storyteller/ui-modal`; shared `PromptFullscreenModal` +
button + `useFullscreenPrompt` hook across all 5 desktop promptboxes,
mirrored in the webapp's own PromptBox.

### 3D editor
- Model selector now renders inside the promptbox (was standalone on the
canvas).
- Pose button hides on deselect; pressing "Done" keeps it (flips back to
"Enter Pose Mode") while the character is still selected.
- Drag and drop into folders fix within gallery modal - doesn't close
immediately unless on 3D editor page.

### Tooltips & expand
- Fixed top-left menu tooltips getting stuck open after a tab change.
- Reworked image promptbox auto-grow/expand into a shared
`useAutoGrowEditorHeight` hook (no per-render style writes) with a
smooth height transition.
2026-06-26 21:54:29 +07:00
Tanish Manku ad5ae7968f Fix login modal: stop Reveal motion wrappers from stealing input focus (#1698)
Reveal/RevealGroup called `motion(as)` inside the render body. Unlike
the cached `motion.div` accessor, calling `motion()` as a function
returns a brand-new component type on every invocation. So each
re-render produced a different element type at that position, React
unmounted and remounted the whole subtree, and any focused <input>
inside (the login/signup/forgot-password forms) lost focus on every
keystroke — you could only type one character at a time.

Cache one motion component per element type via a module-level map so
the component reference stays stable across renders. React now
reconciles in place and focus is preserved.

Verified with a Playwright A/B test on the login form: before the fix
typing "test@example.com" left only "t" with focus on <body> (1 blur
mid-type); after the fix the full value is retained, focus stays on the
input, zero blurs.
2026-06-25 13:48:53 -07:00
Brandon Thomas fef83ad271 mini models in api gateway (#1697) 2026-06-25 15:33:51 -04:00
BFlatandClaude 864cfb5faf Fix image refs not attaching when selected from a folder in gallery modal (#1696)
handleUseSelected only searched groupedItems (root library), so picks
made
while browsing inside a folder — which live in folderGroupedItems —
resolved
to nothing and an empty array was passed to onUseSelected. Search both
sources, deduping by id, so folder selections attach correctly.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-25 20:38:53 +07:00
Brandon Thomas 2aed062790 seedance mini client (#1695) 2026-06-25 07:45:25 -04:00
Brandon Thomas 4d5f8e9c21 router 4k (#1693) 2026-06-24 17:18:37 -04:00
Brandon Thomas 98f9504bdd video models list refactor (#1692) 2026-06-24 13:56:56 -04:00
Brandon Thomas 7ff72d2ecc seedance 4k (#1691) 2026-06-24 12:45:11 -04:00
bflatastic 10eb062b6f seedance 2.1 -> 2.5 text + uncomment sidebar edit page 2026-06-24 22:52:32 +07:00
Brandon Thomas 3b225a7c6d omni api: status endpoint, api docs, etc (#1690) 2026-06-24 00:24:26 -04:00
Tanish MankuandBFlat 61a0880c80 Moodboard: Extensive feature support (#1661)
Co-authored-by: BFlat <bombayapirat@gmail.com>
2026-06-23 20:15:52 -07:00
Tanish Manku c5c68d301a Webapp: Premium motion pass across app (#1662)
Motion foundation + expressive entrance/interaction polish for the
webapp, following a "balanced" direction (restrained chrome, expressive
canvas).

Foundation
- Motion tokens: --ease-*/--dur-* CSS vars (styles.css) mirrored as JS
constants/presets in lib/motion.ts; prefers-reduced-motion respected.
- Shared Reveal/RevealGroup primitive (components/motion) — fade-up +
stagger.

Chrome & navigation
- Global route fade (components/motion/page-transition) keyed by
section.
- Sidebar active-indicator slide via framer layoutId + icon hover.

Pages
- Home: hero/eyebrow reveal, staggered launcher cards, spring lift/press
hover, double-bezel cards, nested arrow chip.
- Auth: AuthHeader staggers in (login+signup); form blocks cascade
after.
- Pricing: wire the orphaned data-reveal hero through the real Reveal.
- Library: sliding active indicator on tab/filter switchers (layoutId),
shimmer-sweep loading skeleton.

Pagescene entrance animations (user-drop only)
- EntranceAnimator: meshes get a transform-based pop/rise/fall drop with
a rubbery squash overshoot; splats get a per-gaussian radial settle
(Spark dyno) with center-out fade + height drop. Dev-only speed slider.

Gallery + asset drag-under UX
- On drag-start the gallery/asset modal becomes pointer-transparent so
the drag passes under onto the canvas; steps back to translucent (reopen
on) or eases fully out (reopen off), all interpolated. Shared DragGhost
(lift/tilt/follow) feeds both DnD systems. Media fades to placeholders
so thumbnails don't block the canvas. Asset modal always reopens fully
visible.

PageDraw: entrance fade on add + workspace vignette.

Dev-only (local testing): drop setDevelopment() API host pin; add /__cdn
proxy + maybeProxyCdnUrl so CDN splat/GLB assets dodge CORS.
2026-06-23 20:14:06 -07:00
Brandon Thomas 81de7838d3 omni api url support (#1689) 2026-06-23 23:10:14 -04:00
Brandon Thomas 9653e9082e minor change to test build works 2026-06-23 19:40:13 -04:00
Brandon ThomasandClaude Opus 4.8 ee6c064f4f dashboard netlify build script (#1687)
The netlify_build.sh referenced by the Netlify build command was never
committed, so the build failed with "No such file or directory".

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:36:19 -04:00
Brandon Thomas 630b132ae7 modify netlify configs for dashboard 2026-06-23 19:16:06 -04:00
Brandon Thomas 5742e638ed attempt to re-add the dashboard (#1686) 2026-06-23 19:04:53 -04:00
Tanish Mankuandbflatastic dc33a99d75 pagedraw: replace hardcoded chrome colors with semantic tokens (#1651)
Implements the P0 color item from the PageDraw design review. Swap raw
hex/palette chrome colors for the semantic tokens shared by both lib
hosts (artcraft desktop + artcraft-webapp):

- ContextMenu: #1A1A1A/#333333/#ECECEC -> bg-ui-panel, white/10
hairlines, text-base-fg/90
- SideToolbar: zinc swatch + #404040/#303030 popovers -> ui-controls /
bg-ui-panel + white/10 borders
- SliderWithIndicator: #5F5F68 tooltip -> bg-ui-controls
- SplitPane: gray-500/600 -> white/20-30
- HistoryStack: text-gray-400 -> text-base-fg/50
- PageDraw + base-image-selector: blue-500/600 accents -> primary token
(so the editor accent matches the product's brand-primary)
- pagedraw.css pegboard: #1b1b1b -> var(--st-canvas, #1b1b1b)

Only tokens defined in both host themes are used. Konva canvas-paint
colors (node strokes, brush defaults, selection fills) are intentionally
left as literals — they're canvas content, not CSS chrome. Oversized
modules deferred per scope.

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-06-23 15:15:28 -07:00
Brandon Thomas f6c5ea94eb fix staff logs null column (#1685) 2026-06-23 18:15:15 -04:00
Brandon Thomas 123fe83a3d better logging (#1684) 2026-06-23 17:21:33 -04:00
Brandon Thomas 654f9164d3 typo 2026-06-23 16:57:27 -04:00
Brandon Thomas 539824564b better job logging per poll iter (#1683) 2026-06-23 16:31:20 -04:00
Brandon Thomas fef420129d fix seedance client again - nullable width/height (#1682) 2026-06-23 16:13:54 -04:00
BFlat 39dd4dee9a revert dashboard merge (#1681) 2026-06-23 23:31:29 +07:00
bflatastic 02ac2319a1 temporarily commented out dashboard in tsconfig 2026-06-23 21:14:59 +07:00
bflatastic 6af1493c1c build fix react version nx 2026-06-23 21:05:49 +07:00
bflatastic 7c7e050d02 revert package json/lock file 2026-06-23 20:51:19 +07:00
Brandon Thomas fb93d297d3 development ports (#1680) 2026-06-23 09:22:25 -04:00
BFlat 38d0642904 Dashboard: ported over dashboard to monorepo (#1679) 2026-06-23 20:18:49 +07: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
1200 changed files with 71875 additions and 3827 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)`
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT token AS `token: UserToken`\nFROM users\nWHERE email_address = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "0d2eadc64240b9ce5348b3b238b0ae6229df21697c952b08e930d03f97414cf4"
}
@@ -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"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n jobs.platform_type as `maybe_platform_type: enums::common::platform_type::PlatformType`,\n jobs.created_at\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -92,6 +92,16 @@
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 9,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
@@ -106,8 +116,9 @@
false,
true,
true,
true
true,
false
]
},
"hash": "ddbbd3886c00268edd3993508f635549fc8ddc877bb059fd58f14b8c07586fc9"
"hash": "1703bc63ce65d98cbcad81d515cbcf211a943c533e38e887d2c12713c78b687d"
}
@@ -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"
}
@@ -0,0 +1,58 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payment_occurred_at AS `payment_occurred_at!: DateTime<Utc>`,\n amount_usd_cents,\n event_type AS `event_type!: PaymentEventType`,\n maybe_credits_granted AS `maybe_credits_granted: u32`\nFROM user_spend_events\nWHERE maybe_user_token = ?\n AND payments_namespace = ?\n AND is_production = TRUE\nORDER BY payment_occurred_at ASC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payment_occurred_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 1,
"name": "amount_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 2,
"name": "event_type!: PaymentEventType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_credits_granted: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "1c997019b7ed18059bc08d857918c3ce3f5e6ed56d358894cbc496d345566519"
}
@@ -0,0 +1,36 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n stripe_customer_id AS `stripe_customer_id!`,\n user_token AS `user_token!: UserToken`\nFROM user_stripe_customer_links\nWHERE payments_namespace = 'artcraft'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "stripe_customer_id!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false
]
},
"hash": "2d49cfbafc29bce35f3e03135054c7521fb0182663aefa971245f91f33a6fc49"
}
@@ -48,7 +48,7 @@
"name": "maybe_entity_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
@@ -132,7 +132,7 @@
false,
false,
true,
false,
true,
true,
true,
true,
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_spend_events\nSET\n token = ?,\n payments_namespace = ?,\n maybe_user_token = ?,\n event_type = ?,\n amount_usd_cents = ?,\n maybe_credits_granted = ?,\n maybe_user_subscription_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n payment_source = ?,\n source_object_id = ?,\n maybe_stripe_customer_id = ?,\n maybe_stripe_invoice_id = ?,\n maybe_stripe_payment_intent_id = ?,\n maybe_stripe_charge_id = ?,\n maybe_stripe_event_id = ?,\n is_production = ?,\n payment_occurred_at = ?\nON DUPLICATE KEY UPDATE token = token\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 17
},
"nullable": []
},
"hash": "3e5a71cedc31cc656177c8331c2de1d1d2d2eb310aabeb7e6238943b073f6998"
}
@@ -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"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO user_spend_summaries\nSET\n payments_namespace = ?,\n user_token = ?,\n lifetime_gross_spend_usd_cents = ?,\n lifetime_subscription_spend_usd_cents = ?,\n lifetime_credits_spend_usd_cents = ?,\n lifetime_refund_usd_cents = ?,\n lifetime_net_spend_usd_cents = ?,\n lifetime_payment_count = ?,\n lifetime_refund_count = ?,\n first_payment_at = ?,\n first_spend_usd_cents = ?,\n last_payment_at = ?,\n last_spend_usd_cents = ?,\n days_since_first_payment = ?,\n days_since_last_payment = ?,\n net_spend_7d_usd_cents = ?,\n net_spend_prev_7d_usd_cents = ?,\n net_spend_14d_usd_cents = ?,\n net_spend_prev_14d_usd_cents = ?,\n net_spend_30d_usd_cents = ?,\n net_spend_prev_30d_usd_cents = ?,\n net_spend_60d_usd_cents = ?,\n net_spend_90d_usd_cents = ?,\n net_spend_this_year_usd_cents = ?,\n avg_weekly_net_spend_4w_usd_cents = ?,\n avg_weekly_net_spend_12w_usd_cents = ?,\n active_weeks_in_last_4 = ?,\n active_weeks_in_last_8 = ?,\n active_weeks_in_last_12 = ?,\n active_weeks_in_last_24 = ?,\n active_weeks_in_last_52 = ?,\n consecutive_active_weeks = ?,\n consecutive_inactive_weeks = ?,\n weeks_since_last_spend = ?,\n is_active_subscriber = ?,\n maybe_subscription_interval = ?,\n maybe_reengagement_score = ?\nON DUPLICATE KEY UPDATE\n lifetime_gross_spend_usd_cents = VALUES(lifetime_gross_spend_usd_cents),\n lifetime_subscription_spend_usd_cents = VALUES(lifetime_subscription_spend_usd_cents),\n lifetime_credits_spend_usd_cents = VALUES(lifetime_credits_spend_usd_cents),\n lifetime_refund_usd_cents = VALUES(lifetime_refund_usd_cents),\n lifetime_net_spend_usd_cents = VALUES(lifetime_net_spend_usd_cents),\n lifetime_payment_count = VALUES(lifetime_payment_count),\n lifetime_refund_count = VALUES(lifetime_refund_count),\n first_payment_at = VALUES(first_payment_at),\n first_spend_usd_cents = VALUES(first_spend_usd_cents),\n last_payment_at = VALUES(last_payment_at),\n last_spend_usd_cents = VALUES(last_spend_usd_cents),\n days_since_first_payment = VALUES(days_since_first_payment),\n days_since_last_payment = VALUES(days_since_last_payment),\n net_spend_7d_usd_cents = VALUES(net_spend_7d_usd_cents),\n net_spend_prev_7d_usd_cents = VALUES(net_spend_prev_7d_usd_cents),\n net_spend_14d_usd_cents = VALUES(net_spend_14d_usd_cents),\n net_spend_prev_14d_usd_cents = VALUES(net_spend_prev_14d_usd_cents),\n net_spend_30d_usd_cents = VALUES(net_spend_30d_usd_cents),\n net_spend_prev_30d_usd_cents = VALUES(net_spend_prev_30d_usd_cents),\n net_spend_60d_usd_cents = VALUES(net_spend_60d_usd_cents),\n net_spend_90d_usd_cents = VALUES(net_spend_90d_usd_cents),\n net_spend_this_year_usd_cents = VALUES(net_spend_this_year_usd_cents),\n avg_weekly_net_spend_4w_usd_cents = VALUES(avg_weekly_net_spend_4w_usd_cents),\n avg_weekly_net_spend_12w_usd_cents = VALUES(avg_weekly_net_spend_12w_usd_cents),\n active_weeks_in_last_4 = VALUES(active_weeks_in_last_4),\n active_weeks_in_last_8 = VALUES(active_weeks_in_last_8),\n active_weeks_in_last_12 = VALUES(active_weeks_in_last_12),\n active_weeks_in_last_24 = VALUES(active_weeks_in_last_24),\n active_weeks_in_last_52 = VALUES(active_weeks_in_last_52),\n consecutive_active_weeks = VALUES(consecutive_active_weeks),\n consecutive_inactive_weeks = VALUES(consecutive_inactive_weeks),\n weeks_since_last_spend = VALUES(weeks_since_last_spend),\n is_active_subscriber = VALUES(is_active_subscriber),\n maybe_subscription_interval = VALUES(maybe_subscription_interval),\n maybe_reengagement_score = VALUES(maybe_reengagement_score)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 37
},
"nullable": []
},
"hash": "41cf2f8d64aa36fb930cc1bf8a46d37e0987b743438b44e1c55859fc6b6ee6a0"
}
@@ -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"
}
@@ -0,0 +1,124 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n s.user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n s.maybe_reengagement_score AS `reengagement_score!: u32`,\n s.lifetime_net_spend_usd_cents,\n s.last_payment_at AS `maybe_last_payment_at: DateTime<Utc>`,\n s.days_since_last_payment AS `maybe_days_since_last_payment: u32`,\n s.weeks_since_last_spend AS `maybe_weeks_since_last_spend: u32`,\n s.is_active_subscriber AS `is_active_subscriber: bool`\nFROM user_spend_summaries s\nJOIN users u ON u.token = s.user_token\nWHERE s.payments_namespace = ?\n AND s.maybe_reengagement_score > 0\nORDER BY s.maybe_reengagement_score DESC, s.id ASC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_gravatar_hash!",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "reengagement_score!: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "maybe_last_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 7,
"name": "maybe_days_since_last_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 8,
"name": "maybe_weeks_since_last_spend: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
true,
false,
true,
true,
true,
false
]
},
"hash": "5b1954301fc16703a04e2f63fd85c28bf2283bc1ecb5624107686dad0dfe364f"
}
@@ -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"
}
@@ -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"
}
@@ -0,0 +1,146 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n user_token AS `user_token!: UserToken`,\n spend_date AS `spend_date!: NaiveDate`,\n subscription_spend_usd_cents,\n credits_spend_usd_cents,\n gross_spend_usd_cents,\n refund_usd_cents,\n net_spend_usd_cents,\n payment_count,\n credits_granted,\n created_at AS `created_at: DateTime<Utc>`,\n updated_at AS `updated_at: DateTime<Utc>`\nFROM user_daily_spends\nWHERE user_token = ?\n AND payments_namespace = ?\nORDER BY spend_date DESC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "spend_date!: NaiveDate",
"type_info": {
"type": "Date",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 3,
"name": "subscription_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 4,
"name": "credits_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "gross_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "refund_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "payment_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "credits_granted",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 10,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 11,
"name": "updated_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "77dca63cffdd18957cbc4be6b7d982a2599a2c06df07f8e34501a032cd688ead"
}
@@ -0,0 +1,113 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_user_token AS `user_token!: UserToken`,\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0\n AND event_type IN ('subscription_initial', 'subscription_renewal', 'subscription_proration_upgrade')\n THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `subscription_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 AND event_type = 'credit_pack_purchase'\n THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `credits_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 THEN amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `gross_spend_usd_cents!: u64`,\n CAST(SUM(CASE WHEN amount_usd_cents < 0 THEN -amount_usd_cents ELSE 0 END) AS UNSIGNED) AS `refund_usd_cents!: u64`,\n CAST(SUM(amount_usd_cents) AS SIGNED) AS `net_spend_usd_cents!: i64`,\n CAST(SUM(CASE WHEN amount_usd_cents > 0 THEN 1 ELSE 0 END) AS UNSIGNED) AS `payment_count!: u64`,\n CAST(COALESCE(SUM(maybe_credits_granted), 0) AS UNSIGNED) AS `credits_granted!: u64`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n AND payment_occurred_at >= ?\n AND payment_occurred_at < ?\nGROUP BY maybe_user_token, payments_namespace\nHAVING SUM(CASE WHEN amount_usd_cents <> 0 THEN 1 ELSE 0 END) > 0\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "subscription_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 3,
"name": "credits_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 4,
"name": "gross_spend_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 5,
"name": "refund_usd_cents!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 6,
"name": "net_spend_usd_cents!: i64",
"type_info": {
"type": "LongLong",
"flags": "BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 7,
"name": "payment_count!: u64",
"type_info": {
"type": "LongLong",
"flags": "UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
},
{
"ordinal": 8,
"name": "credits_granted!: u64",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED | BINARY",
"char_set": 63,
"max_size": 21
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
false,
true,
true,
true,
true,
true,
true,
false
]
},
"hash": "83fc05a3594b379482745f851cd5a22786adde9968dd70490c91100cf21f8023"
}
@@ -48,7 +48,7 @@
"name": "maybe_entity_token",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
@@ -132,7 +132,7 @@
false,
false,
true,
false,
true,
true,
true,
true,
@@ -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"
}
@@ -0,0 +1,443 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n payments_namespace AS `payments_namespace!: PaymentsNamespace`,\n user_token AS `user_token!: UserToken`,\n lifetime_gross_spend_usd_cents,\n lifetime_subscription_spend_usd_cents,\n lifetime_credits_spend_usd_cents,\n lifetime_refund_usd_cents,\n lifetime_net_spend_usd_cents,\n lifetime_payment_count,\n lifetime_refund_count,\n first_payment_at AS `maybe_first_payment_at: DateTime<Utc>`,\n first_spend_usd_cents,\n last_payment_at AS `maybe_last_payment_at: DateTime<Utc>`,\n last_spend_usd_cents,\n days_since_first_payment AS `maybe_days_since_first_payment: u32`,\n days_since_last_payment AS `maybe_days_since_last_payment: u32`,\n net_spend_7d_usd_cents,\n net_spend_prev_7d_usd_cents,\n net_spend_14d_usd_cents,\n net_spend_prev_14d_usd_cents,\n net_spend_30d_usd_cents,\n net_spend_prev_30d_usd_cents,\n net_spend_60d_usd_cents,\n net_spend_90d_usd_cents,\n net_spend_this_year_usd_cents,\n avg_weekly_net_spend_4w_usd_cents,\n avg_weekly_net_spend_12w_usd_cents,\n active_weeks_in_last_4,\n active_weeks_in_last_8,\n active_weeks_in_last_12,\n active_weeks_in_last_24,\n active_weeks_in_last_52,\n consecutive_active_weeks,\n consecutive_inactive_weeks,\n weeks_since_last_spend AS `maybe_weeks_since_last_spend: u32`,\n is_active_subscriber AS `is_active_subscriber: bool`,\n maybe_subscription_interval,\n maybe_reengagement_score AS `maybe_reengagement_score: u32`,\n created_at AS `created_at: DateTime<Utc>`,\n updated_at AS `updated_at: DateTime<Utc>`\nFROM user_spend_summaries\nWHERE payments_namespace = ?\n AND user_token = ?\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "lifetime_gross_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 3,
"name": "lifetime_subscription_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 4,
"name": "lifetime_credits_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "lifetime_refund_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "lifetime_payment_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 8,
"name": "lifetime_refund_count",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "maybe_first_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 10,
"name": "first_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 11,
"name": "maybe_last_payment_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 12,
"name": "last_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 13,
"name": "maybe_days_since_first_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 14,
"name": "maybe_days_since_last_payment: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 15,
"name": "net_spend_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 16,
"name": "net_spend_prev_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 17,
"name": "net_spend_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 18,
"name": "net_spend_prev_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 19,
"name": "net_spend_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 20,
"name": "net_spend_prev_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 21,
"name": "net_spend_60d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 22,
"name": "net_spend_90d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 23,
"name": "net_spend_this_year_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 24,
"name": "avg_weekly_net_spend_4w_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 25,
"name": "avg_weekly_net_spend_12w_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 26,
"name": "active_weeks_in_last_4",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 27,
"name": "active_weeks_in_last_8",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 28,
"name": "active_weeks_in_last_12",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 29,
"name": "active_weeks_in_last_24",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 30,
"name": "active_weeks_in_last_52",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 31,
"name": "consecutive_active_weeks",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 32,
"name": "consecutive_inactive_weeks",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 33,
"name": "maybe_weeks_since_last_spend: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 34,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 35,
"name": "maybe_subscription_interval",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 36,
"name": "maybe_reengagement_score: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 37,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 38,
"name": "updated_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
true,
false,
false
]
},
"hash": "9715e4dc2ec6e596fdb91ecc50a2b80f63494bbc0c9df6c0a57ede2360716869"
}
@@ -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": "\nINSERT INTO user_daily_spends\nSET\n user_token = ?,\n payments_namespace = ?,\n spend_date = ?,\n subscription_spend_usd_cents = ?,\n credits_spend_usd_cents = ?,\n gross_spend_usd_cents = ?,\n refund_usd_cents = ?,\n net_spend_usd_cents = ?,\n payment_count = ?,\n credits_granted = ?\nON DUPLICATE KEY UPDATE\n subscription_spend_usd_cents = VALUES(subscription_spend_usd_cents),\n credits_spend_usd_cents = VALUES(credits_spend_usd_cents),\n gross_spend_usd_cents = VALUES(gross_spend_usd_cents),\n refund_usd_cents = VALUES(refund_usd_cents),\n net_spend_usd_cents = VALUES(net_spend_usd_cents),\n payment_count = VALUES(payment_count),\n credits_granted = VALUES(credits_granted)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "b7a5fea76da70c03b8744c985940a5bcdabdb3316aa407c5ca883bca863ee633"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT DISTINCT maybe_user_token AS `user_token!: UserToken`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n AND payments_namespace = ?\n AND maybe_user_token > ?\nORDER BY maybe_user_token\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
true
]
},
"hash": "bcd07050d5557bf59d6b2691478763b5b067447d295b002f6e766209ea077f42"
}
@@ -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"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "\nSELECT DISTINCT payments_namespace AS `payments_namespace!: PaymentsNamespace`\nFROM user_spend_events\nWHERE maybe_user_token IS NOT NULL\n AND is_production = TRUE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "payments_namespace!: PaymentsNamespace",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
"Right": 0
},
"nullable": [
false
]
},
"hash": "cf8c702126bba73cd3abc2ed7202962b6da773460505222a75408dab1d26b70f"
}
@@ -0,0 +1,212 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n e.token AS `token!: UserSpendEventToken`,\n e.payments_namespace AS `payments_namespace!`,\n e.maybe_user_token AS `maybe_user_token: UserToken`,\n u.username AS `maybe_username?`,\n u.display_name AS `maybe_display_name?`,\n u.email_gravatar_hash AS `maybe_email_gravatar_hash?`,\n e.event_type AS `event_type!`,\n e.amount_usd_cents,\n e.maybe_credits_granted AS `maybe_credits_granted: u32`,\n e.payment_source AS `payment_source!`,\n e.source_object_id AS `maybe_source_object_id`,\n e.maybe_stripe_invoice_id,\n e.maybe_stripe_payment_intent_id,\n e.maybe_stripe_charge_id,\n e.maybe_stripe_customer_id,\n e.is_production AS `is_production: bool`,\n e.payment_occurred_at AS `payment_occurred_at!: DateTime<Utc>`,\n e.created_at AS `created_at!: DateTime<Utc>`\nFROM user_spend_events e\nLEFT JOIN users u ON u.token = e.maybe_user_token\nWHERE (? IS NULL OR e.payments_namespace = ?)\nORDER BY e.payment_occurred_at DESC, e.id DESC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token!: UserSpendEventToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "payments_namespace!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "maybe_user_token: UserToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "maybe_username?",
"type_info": {
"type": "VarString",
"flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 4,
"name": "maybe_display_name?",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 5,
"name": "maybe_email_gravatar_hash?",
"type_info": {
"type": "String",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "event_type!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "amount_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "maybe_credits_granted: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 9,
"name": "payment_source!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 10,
"name": "maybe_source_object_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 11,
"name": "maybe_stripe_invoice_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 12,
"name": "maybe_stripe_payment_intent_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 13,
"name": "maybe_stripe_charge_id",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 14,
"name": "maybe_stripe_customer_id",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 15,
"name": "is_production: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 16,
"name": "payment_occurred_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 17,
"name": "created_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
true,
true,
true,
true,
false,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "d5d3a59ea4885868b26168cd305f6168a1e62fe9f8fa663063591335c645b927"
}
@@ -0,0 +1,80 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n wle.maybe_entity_ref AS `stripe_object_id!`,\n wle.entry_type AS `entry_type!`,\n wle.token AS `ledger_token!: WalletLedgerEntryToken`,\n w.owner_user_token AS `owner_user_token!: UserToken`,\n wle.credits_delta AS `credits_delta!`,\n wle.created_at AS `created_at!: DateTime<Utc>`\nFROM wallet_ledger_entries wle\nJOIN wallets w ON w.token = wle.wallet_token\nWHERE w.wallet_namespace = 'artcraft'\n AND wle.entry_type IN ('credit_banked', 'credit_monthly')\n AND wle.created_at >= ?\n AND (wle.maybe_entity_ref LIKE 'pi\\_%' OR wle.maybe_entity_ref LIKE 'in\\_%')\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "stripe_object_id!",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 1,
"name": "entry_type!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "ledger_token!: WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 3,
"name": "owner_user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "credits_delta!",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 5,
"name": "created_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
false,
false,
false
]
},
"hash": "de69f3d564830f8fff90f754d13982de623fcda50118d261466eeaf034de5ecb"
}
@@ -0,0 +1,157 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n s.user_token AS `user_token!: UserToken`,\n u.username AS `username!`,\n u.display_name AS `display_name!`,\n u.email_gravatar_hash AS `email_gravatar_hash!`,\n s.lifetime_net_spend_usd_cents,\n s.net_spend_7d_usd_cents,\n s.net_spend_14d_usd_cents,\n s.net_spend_30d_usd_cents,\n s.net_spend_60d_usd_cents,\n s.net_spend_90d_usd_cents,\n s.net_spend_this_year_usd_cents,\n s.is_active_subscriber AS `is_active_subscriber: bool`,\n s.maybe_reengagement_score AS `maybe_reengagement_score: u32`\nFROM user_spend_summaries s\nJOIN users u ON u.token = s.user_token\nWHERE s.payments_namespace = ?\nORDER BY\n (CASE ?\n WHEN '7d' THEN s.net_spend_7d_usd_cents\n WHEN '14d' THEN s.net_spend_14d_usd_cents\n WHEN '30d' THEN s.net_spend_30d_usd_cents\n WHEN '60d' THEN s.net_spend_60d_usd_cents\n WHEN '90d' THEN s.net_spend_90d_usd_cents\n WHEN '1y' THEN s.net_spend_this_year_usd_cents\n ELSE CAST(s.lifetime_net_spend_usd_cents AS SIGNED)\n END) DESC,\n s.id ASC\nLIMIT ? OFFSET ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_token!: UserToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "username!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 2,
"name": "display_name!",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 80
}
},
{
"ordinal": 3,
"name": "email_gravatar_hash!",
"type_info": {
"type": "String",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "lifetime_net_spend_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 5,
"name": "net_spend_7d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 6,
"name": "net_spend_14d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 7,
"name": "net_spend_30d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 8,
"name": "net_spend_60d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 9,
"name": "net_spend_90d_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 10,
"name": "net_spend_this_year_usd_cents",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 20
}
},
{
"ordinal": 11,
"name": "is_active_subscriber: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 12,
"name": "maybe_reengagement_score: u32",
"type_info": {
"type": "Long",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 10
}
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true
]
},
"hash": "f6db2f3bf8695fa8431db209576328154b8ac20aa6e899739095b6206bb38944"
}
@@ -0,0 +1,47 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n maybe_stripe_subscription_status,\n maybe_stripe_recurring_interval,\n subscription_expires_at AS `subscription_expires_at!: DateTime<Utc>`\nFROM user_subscriptions\nWHERE user_token = ?\n AND subscription_namespace = ?\nORDER BY subscription_expires_at DESC\nLIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "maybe_stripe_subscription_status",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "maybe_stripe_recurring_interval",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "subscription_expires_at!: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
true,
false
]
},
"hash": "f74615faa4321ec4d583829997e12cb8ae152afefd2574385c120c63278893d2"
}
+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
+60
View File
@@ -1730,6 +1730,14 @@ dependencies = [
"serde",
]
[[package]]
name = "bucket_client"
version = "0.0.1"
dependencies = [
"rust-s3",
"workspace-build-acceleration",
]
[[package]]
name = "bucket_paths"
version = "0.0.1"
@@ -3041,6 +3049,8 @@ dependencies = [
"futures 0.3.32",
"log",
"mysql_queries",
"reqwest 0.12.23",
"rustls 0.23.36",
"serde",
"serde_derive",
"serde_json",
@@ -10508,6 +10518,7 @@ dependencies = [
"log",
"mysql_queries",
"pager",
"rand 0.9.2",
"reqwest 0.12.23",
"rootly_client",
"rootly_config",
@@ -10996,6 +11007,14 @@ dependencies = [
"digest 0.11.2",
]
[[package]]
name = "sha1_hash"
version = "0.0.1"
dependencies = [
"sha1 0.10.6",
"workspace-build-acceleration",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
@@ -11573,6 +11592,7 @@ dependencies = [
"billing_artcraft_component",
"billing_component",
"bootstrap",
"bucket_client",
"bucket_paths",
"bytes",
"chrono",
@@ -11644,6 +11664,7 @@ dependencies = [
"serde_derive",
"serde_json",
"server_environment",
"sha1_hash",
"shared_env_var_config",
"speculoos",
"sqlx",
@@ -11667,6 +11688,7 @@ dependencies = [
"utoipa",
"utoipa-swagger-ui",
"uuid_utils",
"video_info",
"videos",
"workspace-build-acceleration",
"worldlabs_api_client",
@@ -13682,6 +13704,36 @@ dependencies = [
"url",
]
[[package]]
name = "user-spend-analytics-job"
version = "0.0.1"
dependencies = [
"actix-web",
"actix_helpers",
"anyhow",
"bootstrap",
"chrono",
"concurrency",
"easyenv",
"enums",
"errors",
"jobs_common",
"log",
"mysql_queries",
"pager",
"rootly_client",
"rootly_config",
"serde",
"serde_derive",
"serde_json",
"server_environment",
"shared_env_var_config",
"sqlx",
"tokens",
"tokio",
"workspace-build-acceleration",
]
[[package]]
name = "user_input_common"
version = "0.0.1"
@@ -13896,6 +13948,14 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "video_info"
version = "0.0.1"
dependencies = [
"chrono",
"workspace-build-acceleration",
]
[[package]]
name = "videos"
version = "0.0.1"
+7
View File
@@ -10,6 +10,7 @@ members = [
"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",
@@ -87,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",
@@ -94,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",
@@ -133,6 +136,7 @@ members = [
"crates/service/job/grok_api_job",
"crates/service/job/seedance2_pro_job",
"crates/service/job/video_thumbnail_job",
"crates/service/job/user_spend_analytics_job",
"crates/service/job/worldlabs_job",
"crates/service/web/dummy_service",
"crates/service/web/storyteller_web",
@@ -237,6 +241,7 @@ 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" }
@@ -281,6 +286,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" }
@@ -290,6 +296,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" }
@@ -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,8 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Revert `maybe_entity_token` back to NOT NULL.
-- NB: This will fail if any rows have a null `maybe_entity_token`.
ALTER TABLE staff_audit_logs
MODIFY COLUMN maybe_entity_token VARCHAR(32) NOT NULL;
@@ -0,0 +1,9 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- `maybe_entity_token` is optional (many audited actions have no entity), but it
-- was mistakenly created as NOT NULL, so inserts with a null entity token fail
-- with "Column 'maybe_entity_token' cannot be null". Make it nullable.
ALTER TABLE staff_audit_logs
MODIFY COLUMN maybe_entity_token VARCHAR(32) DEFAULT NULL;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_spend_events;
@@ -0,0 +1,103 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Append-only ledger of real money movements (USD cents), one row per settled
-- event. Derived from settled Stripe webhooks (invoice.paid, payment_intent.
-- succeeded / checkout.session.completed, charge.refunded, charge.dispute.*),
-- NOT from every webhook. Idempotent via uq_source_object so retries never
-- double-count. We always work in USD via Stripe, so there are no currency or
-- fee columns.
CREATE TABLE user_spend_events (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective public primary key. Token prefix "spend_".
token VARCHAR(32) NOT NULL,
-- Multi-site segregation. Reuses the existing PaymentsNamespace enum.
payments_namespace VARCHAR(32) NOT NULL,
-- Who paid. Nullable so we can record money we can't yet attribute and
-- backfill -- but expect this set ~always.
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- PaymentEventType enum. Splits subscription vs credit-pack revenue:
-- 'subscription_initial' -- first paid invoice of a new subscription
-- 'subscription_renewal' -- recurring paid invoice (monthly, or annual once/yr)
-- 'subscription_proration_upgrade' -- mid-cycle upgrade proration charge (positive)
-- 'subscription_proration_downgrade' -- mid-cycle downgrade proration credit (rare as a settled event)
-- 'credit_pack_purchase' -- one-time credit pack (the ~10x-volume case)
-- 'refund' -- refund (amount negative)
-- 'chargeback' -- dispute/chargeback (amount negative)
-- 'manual_adjustment' -- staff/console correction (payment_source = 'manual')
-- 'subscription_monthly_refill' -- OPTIONAL non-revenue event for annual-plan
-- monthly credit refills (amount 0)
event_type VARCHAR(32) NOT NULL,
-- ========== MONEY ==========
-- USD cents. SIGNED: refunds/chargebacks negative; refills are 0. BIGINT so
-- downstream lifetime sums never overflow.
amount_usd_cents BIGINT NOT NULL,
-- For credit-pack purchases / subscription grants/refills: how many of OUR
-- credits this granted. Lets us correlate dollars-in vs credits-granted vs
-- credits-used. NULL for refunds/chargebacks.
maybe_credits_granted INTEGER UNSIGNED DEFAULT NULL,
-- ========== INTERNAL LINKAGE ==========
-- The subscription this payment belongs to, if any (-> user_subscriptions.token).
maybe_user_subscription_token VARCHAR(32) DEFAULT NULL,
-- The credit-grant ledger entry this produced, if any
-- (-> wallet_ledger_entries.token). Ties a dollar event to its credit effect.
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL,
-- ========== SOURCE / PROVIDER ==========
-- PaymentSource enum: 'stripe' or 'manual'. (USD/Stripe only for the
-- foreseeable future; 'manual' covers staff/console adjustments.)
payment_source VARCHAR(16) NOT NULL,
-- The authoritative object this row represents -- the dedup anchor. NULLABLE.
-- For Stripe: invoice id (subscriptions), payment_intent id (one-time), or
-- refund/dispute id (refunds). For payment_source='manual' (no provider
-- object) leave it NULL. MySQL allows multiple NULLs in a unique index, so
-- manual rows freely duplicate, while any present id is still deduped.
source_object_id VARCHAR(255) DEFAULT NULL,
-- Convenience copies for joins/forensics (255 + utf8mb4_bin for Stripe IDs).
maybe_stripe_customer_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_invoice_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_payment_intent_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_charge_id VARCHAR(255) DEFAULT NULL,
-- The webhook event that caused this row (provenance only).
maybe_stripe_event_id VARCHAR(255) DEFAULT NULL,
-- Stripe livemode. Keep test-mode money out of dashboards.
is_production BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
-- When the money actually moved (Stripe settlement time). Bin/aggregate on
-- THIS, not created_at (which is when we ingested the webhook).
payment_occurred_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
-- Idempotency: one row per settled object. NULL source_object_id rows (manual)
-- are exempt -- MySQL treats each NULL in a unique index as distinct.
UNIQUE KEY uq_source_object (payment_source, source_object_id),
KEY idx_user_occurred (maybe_user_token, payment_occurred_at),
KEY idx_occurred (payment_occurred_at),
KEY idx_namespace_occurred (payments_namespace, payment_occurred_at),
KEY idx_event_type (event_type),
KEY idx_stripe_customer (maybe_stripe_customer_id),
KEY idx_stripe_invoice (maybe_stripe_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_daily_spends;
@@ -0,0 +1,44 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Per-user, per-day spend rollup (L1). Net = gross - refunds. Subscription vs
-- credit-pack splits expose the revenue mix per day without touching the event
-- table. Sparsely populated -- no row for days with zero spend and zero refunds.
-- Derived from (and fully rebuildable from) user_spend_events.
CREATE TABLE user_daily_spends (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_token VARCHAR(32) NOT NULL,
payments_namespace VARCHAR(32) NOT NULL,
-- Reporting day. Bins user_spend_events.payment_occurred_at in a FIXED tz
-- (UTC). DATE, not TIMESTAMP.
spend_date DATE NOT NULL,
-- Revenue mix (subset of gross). Category totals can never go negative, so
-- they're UNSIGNED.
subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- sum of positive amounts (UNSIGNED)
refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- refund/chargeback magnitude (UNSIGNED)
-- NET is the ONE field here that must be SIGNED: a single day can net negative
-- when refunds (often of an earlier day's purchase) exceed that day's purchases.
net_spend_usd_cents BIGINT NOT NULL DEFAULT 0, -- gross - refund (SIGNED)
payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # positive money events
credits_granted BIGINT UNSIGNED NOT NULL DEFAULT 0,
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 uq_user_day (user_token, payments_namespace, spend_date),
-- "Top spenders / spend in a date range": filter by date, then group by user.
KEY idx_day_user_net (user_token, payments_namespace, spend_date, net_spend_usd_cents),
KEY idx_user_day (user_token, spend_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_spend_summaries;
@@ -0,0 +1,95 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Per-user spend summary (L2). Lifetime/recency facts are event-maintained (they
-- only grow); sliding windows and weekly-rhythm metrics are job-maintained (they
-- shift even on days a user doesn't pay). Powers churn / dip / re-engagement
-- dashboards in O(1). Derived from (and fully rebuildable from) user_spend_events
-- and user_daily_spends.
CREATE TABLE user_spend_summaries (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
payments_namespace VARCHAR(32) NOT NULL,
user_token VARCHAR(32) NOT NULL,
-- ===== Lifetime (event-maintained) =====
-- Gross / category / refund totals are UNSIGNED. Lifetime net is UNSIGNED too:
-- over a lifetime you can never refund more than was paid.
lifetime_gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_net_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- gross - refund
-- Event COUNTS (not money): number of payment events vs refund/chargeback
-- events over the user's lifetime. Both event-maintained.
lifetime_payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of positive money events
lifetime_refund_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of refund/chargeback events
first_payment_at TIMESTAMP NULL DEFAULT NULL,
first_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the first payment
last_payment_at TIMESTAMP NULL DEFAULT NULL,
last_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the most recent payment
days_since_first_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
days_since_last_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
-- ===== Sliding NET spend windows -- SIGNED, a window can net negative
-- (job-refreshed from user_daily_spends) =====
-- "prev" = the equally-sized window immediately before, for WoW/MoM deltas.
net_spend_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_14d_usd_cents BIGINT NOT NULL DEFAULT 0, -- bi-weekly
net_spend_prev_14d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_60d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_90d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_this_year_usd_cents BIGINT NOT NULL DEFAULT 0,
-- ===== Weekly cadence / rhythm (job-refreshed) -- for the sales team =====
-- A "week" is Mon-Sun UTC. These let sales spot users breaking their habit.
-- Per-user weekly baselines at two horizons: 4w is responsive (recent norm),
-- 12w is stable (established norm). 4w well below 12w is itself a cooling-off
-- signal. Mean weekly NET spend over the trailing window (calendar weeks,
-- zero-spend weeks included). SIGNED, like the window nets.
avg_weekly_net_spend_4w_usd_cents BIGINT NOT NULL DEFAULT 0,
avg_weekly_net_spend_12w_usd_cents BIGINT NOT NULL DEFAULT 0,
-- How many of the last N calendar weeks had any net spend -- a regularity
-- measure at several horizons.
active_weeks_in_last_4 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..4
active_weeks_in_last_8 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..8
active_weeks_in_last_12 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..12
active_weeks_in_last_24 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..24
active_weeks_in_last_52 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..52
consecutive_active_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "streak"
consecutive_inactive_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "dip" length
-- Recency in weeks. Subsumes the entire "is_active_spender_last_N_weeks"
-- family: "active in the last N weeks" == (weeks_since_last_spend < N). So we
-- intentionally store NO is_active_* booleans (redundant + poor index
-- selectivity); we index this field instead and range-query any N. NULL = never spent.
weeks_since_last_spend INT UNSIGNED DEFAULT NULL,
-- ===== Flags =====
is_active_subscriber BOOLEAN NOT NULL DEFAULT FALSE, -- from user_subscriptions
maybe_subscription_interval VARCHAR(16) DEFAULT NULL, -- 'month' / 'year' (annual plans behave differently)
-- Optional precomputed re-engagement priority (job-set).
maybe_reengagement_score INT UNSIGNED DEFAULT NULL,
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 uq_user (payments_namespace, user_token),
KEY idx_lifetime (payments_namespace, lifetime_net_spend_usd_cents),
KEY idx_last_payment_at (payments_namespace, last_payment_at),
KEY idx_first_payment_at (payments_namespace, first_payment_at),
KEY idx_spend_7d (payments_namespace, net_spend_7d_usd_cents),
KEY idx_spend_30d (payments_namespace, net_spend_30d_usd_cents),
KEY idx_reengagement (payments_namespace, maybe_reengagement_score),
-- Sort / range-filter by recency. idx_weeks_since_last_spend also powers every
-- "active in the last N weeks" query (weeks_since_last_spend < N).
KEY idx_weeks_since_last_spend (payments_namespace, weeks_since_last_spend),
KEY idx_days_since_last_payment (payments_namespace, days_since_last_payment)
) 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,47 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Per-user, per-day spend rollup (L1). Net = gross - refunds. Subscription vs
-- credit-pack splits expose the revenue mix per day without touching the event
-- table. Sparsely populated -- no row for days with zero spend and zero refunds.
-- Derived from (and fully rebuildable from) user_spend_events.
CREATE TABLE user_daily_spends (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
user_token VARCHAR(32) NOT NULL,
payments_namespace VARCHAR(32) NOT NULL,
-- Reporting day. Bins user_spend_events.payment_occurred_at in a FIXED tz
-- (UTC). DATE, not TIMESTAMP.
spend_date DATE NOT NULL,
-- Revenue mix (subset of gross). Category totals can never go negative, so
-- they're UNSIGNED.
subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- sum of positive amounts (UNSIGNED)
refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- refund/chargeback magnitude (UNSIGNED)
-- NET is the ONE field here that must be SIGNED: a single day can net negative
-- when refunds (often of an earlier day's purchase) exceed that day's purchases.
net_spend_usd_cents BIGINT NOT NULL DEFAULT 0, -- gross - refund (SIGNED)
payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # positive money events
credits_granted BIGINT UNSIGNED NOT NULL DEFAULT 0,
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 uq_user_day (user_token, payments_namespace, spend_date),
-- "Top spenders / spend in a date range": filter by date, then group by user.
KEY idx_day_user_net (user_token, payments_namespace, spend_date, net_spend_usd_cents),
KEY idx_user_day (user_token, spend_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,106 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Append-only ledger of real money movements (USD cents), one row per settled
-- event. Derived from settled Stripe webhooks (invoice.paid, payment_intent.
-- succeeded / checkout.session.completed, charge.refunded, charge.dispute.*),
-- NOT from every webhook. Idempotent via uq_source_object so retries never
-- double-count. We always work in USD via Stripe, so there are no currency or
-- fee columns.
CREATE TABLE user_spend_events (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Effective public primary key. Token prefix "spend_".
token VARCHAR(32) NOT NULL,
-- Multi-site segregation. Reuses the existing PaymentsNamespace enum.
payments_namespace VARCHAR(32) NOT NULL,
-- Who paid. Nullable so we can record money we can't yet attribute and
-- backfill -- but expect this set ~always.
maybe_user_token VARCHAR(32) DEFAULT NULL,
-- PaymentEventType enum. Splits subscription vs credit-pack revenue:
-- 'subscription_initial' -- first paid invoice of a new subscription
-- 'subscription_renewal' -- recurring paid invoice (monthly, or annual once/yr)
-- 'subscription_proration_upgrade' -- mid-cycle upgrade proration charge (positive)
-- 'subscription_proration_downgrade' -- mid-cycle downgrade proration credit (rare as a settled event)
-- 'credit_pack_purchase' -- one-time credit pack (the ~10x-volume case)
-- 'refund' -- refund (amount negative)
-- 'chargeback' -- dispute/chargeback (amount negative)
-- 'manual_adjustment' -- staff/console correction (payment_source = 'manual')
-- 'subscription_monthly_refill' -- OPTIONAL non-revenue event for annual-plan
-- monthly credit refills (amount 0)
event_type VARCHAR(32) NOT NULL,
-- ========== MONEY ==========
-- USD cents. SIGNED: refunds/chargebacks negative; refills are 0. BIGINT so
-- downstream lifetime sums never overflow.
amount_usd_cents BIGINT NOT NULL,
-- For credit-pack purchases / subscription grants/refills: how many of OUR
-- credits this granted. Lets us correlate dollars-in vs credits-granted vs
-- credits-used. NULL for refunds/chargebacks.
maybe_credits_granted INTEGER UNSIGNED DEFAULT NULL,
-- ========== INTERNAL LINKAGE ==========
-- The subscription this payment belongs to, if any (-> user_subscriptions.token).
maybe_user_subscription_token VARCHAR(32) DEFAULT NULL,
-- The credit-grant ledger entry this produced, if any
-- (-> wallet_ledger_entries.token). Ties a dollar event to its credit effect.
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL,
-- ========== SOURCE / PROVIDER ==========
-- PaymentSource enum: 'stripe' or 'manual'. (USD/Stripe only for the
-- foreseeable future; 'manual' covers staff/console adjustments.)
payment_source VARCHAR(16) NOT NULL,
-- The authoritative object this row represents -- the dedup anchor. NULLABLE.
-- For Stripe: invoice id (subscriptions), payment_intent id (one-time), or
-- refund/dispute id (refunds). For payment_source='manual' (no provider
-- object) leave it NULL. MySQL allows multiple NULLs in a unique index, so
-- manual rows freely duplicate, while any present id is still deduped.
source_object_id VARCHAR(255) DEFAULT NULL,
-- Convenience copies for joins/forensics (255 + utf8mb4_bin for Stripe IDs).
maybe_stripe_customer_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_invoice_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_payment_intent_id VARCHAR(255) DEFAULT NULL,
maybe_stripe_charge_id VARCHAR(255) DEFAULT NULL,
-- The webhook event that caused this row (provenance only).
maybe_stripe_event_id VARCHAR(255) DEFAULT NULL,
-- Stripe livemode. Keep test-mode money out of dashboards.
is_production BOOLEAN NOT NULL DEFAULT FALSE,
-- ========== TIMESTAMPS ==========
-- When the money actually moved (Stripe settlement time). Bin/aggregate on
-- THIS, not created_at (which is when we ingested the webhook).
payment_occurred_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ========== INDICES ==========
PRIMARY KEY (id),
UNIQUE KEY (token),
-- Idempotency: one row per settled object. NULL source_object_id rows (manual)
-- are exempt -- MySQL treats each NULL in a unique index as distinct.
UNIQUE KEY uq_source_object (payment_source, source_object_id),
KEY idx_user_occurred (maybe_user_token, payment_occurred_at),
KEY idx_occurred (payment_occurred_at),
KEY idx_namespace_occurred (payments_namespace, payment_occurred_at),
KEY idx_event_type (event_type),
KEY idx_stripe_customer (maybe_stripe_customer_id),
KEY idx_stripe_invoice (maybe_stripe_invoice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,98 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- Per-user spend summary (L2). Lifetime/recency facts are event-maintained (they
-- only grow); sliding windows and weekly-rhythm metrics are job-maintained (they
-- shift even on days a user doesn't pay). Powers churn / dip / re-engagement
-- dashboards in O(1). Derived from (and fully rebuildable from) user_spend_events
-- and user_daily_spends.
CREATE TABLE user_spend_summaries (
-- Replication only.
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
payments_namespace VARCHAR(32) NOT NULL,
user_token VARCHAR(32) NOT NULL,
-- ===== Lifetime (event-maintained) =====
-- Gross / category / refund totals are UNSIGNED. Lifetime net is UNSIGNED too:
-- over a lifetime you can never refund more than was paid.
lifetime_gross_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_subscription_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_credits_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- revenue mix split
lifetime_refund_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
lifetime_net_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- gross - refund
-- Event COUNTS (not money): number of payment events vs refund/chargeback
-- events over the user's lifetime. Both event-maintained.
lifetime_payment_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of positive money events
lifetime_refund_count INT UNSIGNED NOT NULL DEFAULT 0, -- # of refund/chargeback events
first_payment_at TIMESTAMP NULL DEFAULT NULL,
first_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the first payment
last_payment_at TIMESTAMP NULL DEFAULT NULL,
last_spend_usd_cents BIGINT UNSIGNED NOT NULL DEFAULT 0, -- amount of the most recent payment
days_since_first_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
days_since_last_payment INT UNSIGNED DEFAULT NULL, -- job-refreshed
-- ===== Sliding NET spend windows -- SIGNED, a window can net negative
-- (job-refreshed from user_daily_spends) =====
-- "prev" = the equally-sized window immediately before, for WoW/MoM deltas.
net_spend_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_7d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_14d_usd_cents BIGINT NOT NULL DEFAULT 0, -- bi-weekly
net_spend_prev_14d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_prev_30d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_60d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_90d_usd_cents BIGINT NOT NULL DEFAULT 0,
net_spend_this_year_usd_cents BIGINT NOT NULL DEFAULT 0,
-- ===== Weekly cadence / rhythm (job-refreshed) -- for the sales team =====
-- A "week" is Mon-Sun UTC. These let sales spot users breaking their habit.
-- Per-user weekly baselines at two horizons: 4w is responsive (recent norm),
-- 12w is stable (established norm). 4w well below 12w is itself a cooling-off
-- signal. Mean weekly NET spend over the trailing window (calendar weeks,
-- zero-spend weeks included). SIGNED, like the window nets.
avg_weekly_net_spend_4w_usd_cents BIGINT NOT NULL DEFAULT 0,
avg_weekly_net_spend_12w_usd_cents BIGINT NOT NULL DEFAULT 0,
-- How many of the last N calendar weeks had any net spend -- a regularity
-- measure at several horizons.
active_weeks_in_last_4 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..4
active_weeks_in_last_8 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..8
active_weeks_in_last_12 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..12
active_weeks_in_last_24 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..24
active_weeks_in_last_52 TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0..52
consecutive_active_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "streak"
consecutive_inactive_weeks INT UNSIGNED NOT NULL DEFAULT 0, -- current "dip" length
-- Recency in weeks. Subsumes the entire "is_active_spender_last_N_weeks"
-- family: "active in the last N weeks" == (weeks_since_last_spend < N). So we
-- intentionally store NO is_active_* booleans (redundant + poor index
-- selectivity); we index this field instead and range-query any N. NULL = never spent.
weeks_since_last_spend INT UNSIGNED DEFAULT NULL,
-- ===== Flags =====
is_active_subscriber BOOLEAN NOT NULL DEFAULT FALSE, -- from user_subscriptions
maybe_subscription_interval VARCHAR(16) DEFAULT NULL, -- 'month' / 'year' (annual plans behave differently)
-- Optional precomputed re-engagement priority (job-set).
maybe_reengagement_score INT UNSIGNED DEFAULT NULL,
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 uq_user (payments_namespace, user_token),
KEY idx_lifetime (payments_namespace, lifetime_net_spend_usd_cents),
KEY idx_last_payment_at (payments_namespace, last_payment_at),
KEY idx_first_payment_at (payments_namespace, first_payment_at),
KEY idx_spend_7d (payments_namespace, net_spend_7d_usd_cents),
KEY idx_spend_30d (payments_namespace, net_spend_30d_usd_cents),
KEY idx_reengagement (payments_namespace, maybe_reengagement_score),
-- Sort / range-filter by recency. idx_weeks_since_last_spend also powers every
-- "active in the last N weeks" query (weeks_since_last_spend < N).
KEY idx_weeks_since_last_spend (payments_namespace, weeks_since_last_spend),
KEY idx_days_since_last_payment (payments_namespace, days_since_last_payment)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -17,7 +17,7 @@ CREATE TABLE staff_audit_logs (
maybe_entity_type VARCHAR(16) DEFAULT NULL,
-- The token of the entity mutated (if any)
maybe_entity_token VARCHAR(32) NOT NULL,
maybe_entity_token VARCHAR(32) DEFAULT NULL,
-- The staff user who performed the action.
staff_user_token VARCHAR(32) 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;
+625
View File
@@ -0,0 +1,625 @@
# ArtCraft Omni API
> # 📚 Full API reference: **<https://storyteller-docs.netlify.app/>**
>
> **Every ArtCraft API endpoint is documented there.** This guide covers only the Omni API video
> endpoints — for everything else, start at the link above.
The **Omni API** is the API-key–authenticated surface for ArtCraft generation. It mirrors the
in-app "Omni Gen" endpoints but is designed for programmatic use: you authenticate with an API key
in the `Authorization` header instead of a browser session cookie.
This guide covers **video generation from URL inputs** — passing image, video, and audio reference
URLs directly. The server downloads each URL, stores it as a media file on your account, and then
runs generation against it. You never have to upload files yourself.
> All examples send **no cookies**. The Omni API is authenticated purely by your API key. If you
> send session cookies they are ignored.
---
## Base URLs
| Environment | Base URL |
| ------------- | ------------------------------ |
| Development | `http://localhost:12345` |
| Production | `https://api.storyteller.ai` |
All paths below are relative to the base URL (e.g. `POST /v1/omni_api/generate/video`).
The examples in this guide use the **development** base URL. To run against production, swap
`http://localhost:12345` for `https://api.storyteller.ai`.
Note: Our APIs are hosted on "`api.storyteller.ai`", not "`api.getartcraft.com`". This is not a
typo. Our Cloudflare CDN URLs are `cdn-2.fakeyou.com`.
---
## Getting an API key
Create and manage API keys from the ArtCraft web app:
1. Go to **<https://app.getartcraft.com/>**
2. Click **Account → Settings → API Keys** (your account must have API keys enabled from our staff)
3. Create a key and copy it. The secret is shown **once** — store it somewhere safe.
> **Don't see the API Keys section?** API access is gated per account. Contact the Artcraft team
> to enable it.
### What an API key looks like
- Prefix: `artcraft_api_`
- Followed by 40 random lowercase Crockford-base32 characters (`0-9 a-z`, excluding `i l o u`)
- Total length: **53 characters**, e.g. `artcraft_api_3k7q9w0xv2hs5n8m4d6r1t8y0p2s4f6h8j0k2m4n6`
Treat the key like a password: never commit it to source control or paste it into logs.
---
## Authentication
Send the secret key in the `Authorization` header. Three forms are accepted:
```
Authorization: Bearer artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: Key artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
`Bearer` is recommended. **Do not send cookies.**
---
## Video generation — `POST /v1/omni_api/generate/video`
Generates a video and returns an inference job token you can poll for results.
### Request body
| Field | Type | Notes |
| ---------------------------- | --------------- | ----- |
| `model` | string | Generation model, e.g. `seedance_2p0`. **Required.** |
| `prompt` | string | Text prompt. |
| `duration_seconds` | integer | Length of the output video, e.g. `6`. |
| `aspect_ratio` | string | e.g. `wide_sixteen_by_nine`, `tall_nine_by_sixteen`, `square`. |
| `idempotency_token` | string (UUID) | **Required.** A fresh UUID per request; prevents accidental duplicates. |
| `negative_prompt` | string | Optional. |
| `resolution` | string | Optional, e.g. `seven_twenty_p`, `ten_eighty_p`. |
| `generate_audio` | boolean | Optional. |
| **URL inputs** (this guide) | | |
| `start_frame_image_url` | string (URL) | Starting keyframe image. |
| `end_frame_image_url` | string (URL) | Ending keyframe image. |
| `reference_image_urls` | array of URL | Reference images. |
| `reference_video_urls` | array of URL | Reference videos (`mp4`). |
| `reference_audio_urls` | array of URL | Reference audio (`wav`, `mp3`, `ogg`, …). |
| **Media-token inputs** | | |
| `start_frame_image_media_token`, `end_frame_image_media_token`, `reference_image_media_tokens`, `reference_video_media_tokens`, `reference_audio_media_tokens` | token(s) | Use these instead if you've already uploaded media. |
#### Rules for URL inputs
- Each URL field and its media-token counterpart are **mutually exclusive**. Sending both
`reference_image_urls` and `reference_image_media_tokens`, for example, returns:
`Either reference_image_media_tokens or reference_image_urls must be set, not both`.
- Every URL must start with `http://` or `https://`, otherwise:
`URL must start with http:// or https://, bad URL: <url>`.
- Redirects are followed (up to 10), so CDN/redirecting URLs work.
- The file type is detected from the downloaded bytes. Images must be a real image
(`jpeg`/`png`/`gif`/`webp`), reference videos must be `mp4`, reference audio must be a
supported audio type (`wav`, `mp3`, `aac`, `ogg`, `flac`, …).
### Response
```json
{
"success": true,
"inference_job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
"all_job_tokens": ["jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"]
}
```
Use `inference_job_token` to poll for the finished video. `all_job_tokens` lists every job created
(batch requests can create more than one).
---
## Examples
The examples reuse these public sample URLs:
- Images (Wikimedia Commons): an Atlantic puffin, the Matterhorn, a toco toucan
- Video: a short sample `mp4`
- Audio: a short sample `wav`
Replace `YOUR_API_KEY` with your `artcraft_api_…` secret, and generate a fresh `idempotency_token`
(UUID) for every request.
---
### Example 1 — Starting-frame Seedance (`start_frame_image_url`)
Animate outward from a single starting image.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900"
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
start_frame_image_url:
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
}),
});
console.log(resp.status, await resp.json());
```
---
### Example 2 — Image-reference Seedance (`reference_image_urls`)
Guide the generation with one or more reference images.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
]
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
reference_image_urls: [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
}),
});
console.log(resp.status, await resp.json());
```
---
### Example 3 — Image + video reference Seedance (`reference_image_urls` + `reference_video_urls`)
Combine reference images with a reference video (and, optionally, reference audio). This is the
fullest URL-driven request.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
],
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"]
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"],
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
reference_image_urls: [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
reference_video_urls: ["https://download.samplelib.com/mp4/sample-5s.mp4"],
reference_audio_urls: ["https://download.samplelib.com/wav/sample-6s.wav"],
}),
});
console.log(resp.status, await resp.json());
```
---
## Checking job status
Generation is asynchronous. The generate call returns an `inference_job_token`; poll the job-status
endpoint until the job reaches a terminal state, then read the result URL.
### Endpoints
- **Single job:** `GET /v1/omni_api/job_status/job/{token}`
- **Batch (many jobs):** `GET /v1/omni_api/job_status/batch?tokens=jinf_aaa&tokens=jinf_bbb`
Both require the same `Authorization: Bearer …` header (no cookies).
### Response shape
```json
{
"success": true,
"state": {
"job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
"request": {
"inference_category": "video_generation",
"maybe_prompt_token": "prompt_xxxxxxxxxxxxxxxxxxxxxxxxx",
"maybe_model_type": "seedance_2p0",
"maybe_model_token": null
},
"status": {
"status": "pending",
"maybe_first_started_at": null,
"maybe_failure_category": null,
"progress_percentage": 0
},
"maybe_result": null,
"created_at": "2026-06-24T04:06:14Z",
"updated_at": "2026-06-24T04:06:14Z"
}
}
```
- `state.status.status` — the job state. Keep polling while it's `pending`, `started`, or
`attempt_failed`. **Terminal** states are `complete_success`, `complete_failure`, `dead`,
`cancelled_by_user`, and `cancelled_by_system`.
- `state.status.progress_percentage` — an integer 0–100.
- `state.maybe_result` — `null` until the job succeeds. On `complete_success` it contains the
finished media; the playable/downloadable URL is **`state.maybe_result.media_links.cdn_url`**.
For video, `media_links.maybe_video_previews` also carries still/animated preview URLs.
The batch endpoint returns `{ "success": true, "job_states": [ … ] }` where each entry is the same
`state` payload as above.
### Where the result video / image lives
When `state.status.status` is `complete_success`, the finished media is under `state.maybe_result`:
- **`state.maybe_result.media_links.cdn_url`** — the primary, playable/downloadable URL of the
finished asset. **This is the field you want.** For a video job it's the `.mp4`; for an image job
it's the image file (`.jpg` / `.png` / etc.).
- `state.maybe_result.entity_token` — the `m_…` media-file token for the result.
- `state.maybe_result.media_links.maybe_video_previews` — **video jobs only**. Contains a `still`
(poster `.jpg`) and an `animated` (`.gif`) preview URL, plus `{WIDTH}` thumbnail templates. This
is `null` for image jobs.
- `state.maybe_result.media_links.maybe_thumbnail_template` — **image jobs only**. A URL template;
replace `{WIDTH}` with a pixel width to fetch a resized thumbnail. `null` for video jobs.
**Example — completed video job:**
```json
{
"success": true,
"state": {
"job_token": "jinf_9fcn2qv4dv3vvbrrxbnmfgen06v",
"request": {
"inference_category": "video_generation",
"maybe_prompt_token": "prompt_q53tp4rh97qh8q159p21tazzt",
"maybe_model_type": "seedance_2p0",
"maybe_model_token": null
},
"status": {
"status": "complete_success",
"maybe_first_started_at": "2026-06-24T04:07:11Z",
"maybe_failure_category": null,
"progress_percentage": 100
},
"maybe_result": {
"entity_type": "media_file",
"entity_token": "m_ss682134tx19yhqs5y0nbnd0bd59a6",
"media_links": {
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4",
"maybe_thumbnail_template": null,
"maybe_video_previews": {
"still": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.jpg",
"animated": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.gif",
"still_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.jpg",
"animated_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.gif"
}
},
"maybe_successfully_completed_at": "2026-06-24T04:17:30Z"
},
"created_at": "2026-06-24T04:06:14Z",
"updated_at": "2026-06-24T04:17:30Z"
}
}
```
→ The video is at
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4`
(example, not a live URL).
**Example — completed image job** (from `POST /v1/omni_api/generate/image`):
```json
{
"maybe_result": {
"entity_type": "media_file",
"entity_token": "m_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6",
"media_links": {
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg",
"maybe_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg-thumb-{WIDTH}.jpg",
"maybe_video_previews": null
},
"maybe_successfully_completed_at": "2026-06-24T04:09:02Z"
}
}
```
→ The image is at
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg`
(example, not a live URL).
### Poll until complete
#### bash
```bash
JOB_TOKEN="jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
while :; do
resp=$(curl -s "http://localhost:12345/v1/omni_api/job_status/job/$JOB_TOKEN" \
-H "Authorization: Bearer YOUR_API_KEY")
status=$(echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['status']['status'])")
echo "status=$status"
case "$status" in
complete_success)
echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['maybe_result']['media_links']['cdn_url'])"
break ;;
complete_failure|dead|cancelled_by_user|cancelled_by_system)
echo "job ended without a result: $status"; break ;;
esac
sleep 5
done
```
#### python
```python
import time
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
TERMINAL_FAILURES = {"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system"}
while True:
resp = requests.get(
f"{BASE_URL}/v1/omni_api/job_status/job/{JOB_TOKEN}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
resp.raise_for_status()
state = resp.json()["state"]
status = state["status"]["status"]
print("status:", status, state["status"]["progress_percentage"], "%")
if status == "complete_success":
print("result URL:", state["maybe_result"]["media_links"]["cdn_url"])
break
if status in TERMINAL_FAILURES:
print("job ended without a result:", status)
break
time.sleep(5)
```
#### javascript
```javascript
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx";
const TERMINAL_FAILURES = new Set([
"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system",
]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
while (true) {
const resp = await fetch(`${BASE_URL}/v1/omni_api/job_status/job/${JOB_TOKEN}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { state } = await resp.json();
const status = state.status.status;
console.log("status:", status, state.status.progress_percentage, "%");
if (status === "complete_success") {
console.log("result URL:", state.maybe_result.media_links.cdn_url);
break;
}
if (TERMINAL_FAILURES.has(status)) {
console.log("job ended without a result:", status);
break;
}
await sleep(5000);
}
```
---
## Notes & troubleshooting
- **Always send a fresh `idempotency_token`** (UUID). Reusing one is rejected as a duplicate.
- **No cookies.** The endpoint authenticates only via the `Authorization` header.
- **URL must be `http(s)`** and reachable by the server; redirects are followed.
- **Type mismatches** (e.g. a `.webm` passed as a reference video, which requires `mp4`) are
rejected with a `400` describing the unpermitted type.
- **`401 Unauthorized`** — missing/invalid API key, or the key's owner is banned.
- **`402 Payment required`** — insufficient credits/balance.
- There is a companion image endpoint, `POST /v1/omni_api/generate/image`, which accepts
`image_urls` (mutually exclusive with `image_media_tokens`) following the same rules.
+2
View File
@@ -138,6 +138,7 @@ RUN SQLX_OFFLINE=true \
$HOME/.cargo/bin/cargo build \
--release \
--bin storyteller-web \
--bin user-spend-analytics-job \
--bin seedance2-pro-job \
--bin worldlabs-job \
--bin video-thumbnail-job \
@@ -190,6 +191,7 @@ COPY --from=builder /tmp/target/release/analytics-job /
COPY --from=builder /tmp/target/release/email-sender-job /
COPY --from=builder /tmp/target/release/es-update-job /
COPY --from=builder /tmp/target/release/grok-api-job /
COPY --from=builder /tmp/target/release/user-spend-analytics-job /
# Legacy apps:
# COPY --from=builder /tmp/target/release/tts-download-job /
@@ -16,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;
@@ -5,3 +5,6 @@ pub mod user_referrals;
pub mod user_stripe_data;
pub mod wallet_ledger_entries;
pub mod wallets;
pub mod user_daily_spends;
pub mod user_spend_summaries;
pub mod user_spend_events;
@@ -0,0 +1 @@
pub mod user_daily_spends_list;
@@ -0,0 +1,41 @@
use chrono::{DateTime, NaiveDate, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_USER_DAILY_SPENDS_PATH: &str =
"/v1/moderation/user_daily_spends/user/{user_token}";
#[derive(Deserialize, IntoParams)]
pub struct ModeratorUserDailySpendsQueryParams {
/// Page size (default 200, max 5000).
pub limit: Option<u32>,
/// Row offset for pagination. Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Payments namespace. Defaults to `artcraft`.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorUserDailySpendsResponse {
pub success: bool,
pub user_token: UserToken,
pub records: Vec<UserDailySpendEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct UserDailySpendEntry {
pub payments_namespace: String,
pub spend_date: NaiveDate,
pub subscription_spend_usd_cents: u64,
pub credits_spend_usd_cents: u64,
pub gross_spend_usd_cents: u64,
pub refund_usd_cents: u64,
pub net_spend_usd_cents: i64,
pub payment_count: u32,
pub credits_granted: u64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1,46 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::user_spend_events::UserSpendEventToken;
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_LIST_USER_SPEND_EVENTS_PATH: &str =
"/v1/moderation/user_spend_events/list";
#[derive(Deserialize, IntoParams)]
pub struct ModeratorListUserSpendEventsQueryParams {
/// Row offset for pagination (page size is fixed at 200). Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Optional namespace filter (e.g. `artcraft`). Omit to return all namespaces.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorListUserSpendEventsResponse {
pub success: bool,
pub events: Vec<UserSpendEventListEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct UserSpendEventListEntry {
pub token: UserSpendEventToken,
pub payments_namespace: String,
pub maybe_user_token: Option<UserToken>,
pub maybe_username: Option<String>,
pub maybe_display_name: Option<String>,
pub maybe_email_gravatar_hash: Option<String>,
pub event_type: String,
pub amount_usd_cents: i64,
pub maybe_credits_granted: Option<u32>,
pub payment_source: String,
pub maybe_source_object_id: Option<String>,
pub maybe_stripe_invoice_id: Option<String>,
pub maybe_stripe_payment_intent_id: Option<String>,
pub maybe_stripe_charge_id: Option<String>,
pub maybe_stripe_customer_id: Option<String>,
pub is_production: bool,
pub payment_occurred_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
@@ -0,0 +1,69 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_GET_USER_SPEND_SUMMARY_PATH: &str =
"/v1/moderation/user_spend_summaries/summary/{user_token}";
#[derive(Deserialize, IntoParams)]
pub struct ModeratorGetUserSpendSummaryQueryParams {
/// Payments namespace. Defaults to `artcraft`.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorGetUserSpendSummaryResponse {
pub success: bool,
/// `None` if the user has no spend summary row.
pub maybe_summary: Option<UserSpendSummaryView>,
}
/// The full `user_spend_summaries` record for a user.
#[derive(Serialize, ToSchema)]
pub struct UserSpendSummaryView {
pub payments_namespace: String,
pub user_token: UserToken,
pub lifetime_gross_spend_usd_cents: u64,
pub lifetime_subscription_spend_usd_cents: u64,
pub lifetime_credits_spend_usd_cents: u64,
pub lifetime_refund_usd_cents: u64,
pub lifetime_net_spend_usd_cents: u64,
pub lifetime_payment_count: u32,
pub lifetime_refund_count: u32,
pub maybe_first_payment_at: Option<DateTime<Utc>>,
pub first_spend_usd_cents: u64,
pub maybe_last_payment_at: Option<DateTime<Utc>>,
pub last_spend_usd_cents: u64,
pub maybe_days_since_first_payment: Option<u32>,
pub maybe_days_since_last_payment: Option<u32>,
pub net_spend_7d_usd_cents: i64,
pub net_spend_prev_7d_usd_cents: i64,
pub net_spend_14d_usd_cents: i64,
pub net_spend_prev_14d_usd_cents: i64,
pub net_spend_30d_usd_cents: i64,
pub net_spend_prev_30d_usd_cents: i64,
pub net_spend_60d_usd_cents: i64,
pub net_spend_90d_usd_cents: i64,
pub net_spend_this_year_usd_cents: i64,
pub avg_weekly_net_spend_4w_usd_cents: i64,
pub avg_weekly_net_spend_12w_usd_cents: i64,
pub active_weeks_in_last_4: u8,
pub active_weeks_in_last_8: u8,
pub active_weeks_in_last_12: u8,
pub active_weeks_in_last_24: u8,
pub active_weeks_in_last_52: u8,
pub consecutive_active_weeks: u32,
pub consecutive_inactive_weeks: u32,
pub maybe_weeks_since_last_spend: Option<u32>,
pub is_active_subscriber: bool,
pub maybe_subscription_interval: Option<String>,
pub maybe_reengagement_score: Option<u32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1,3 @@
pub mod get_summary;
pub mod reengagement_list;
pub mod top_users_list;
@@ -0,0 +1,37 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_REENGAGEMENT_LIST_PATH: &str =
"/v1/moderation/user_spend_summaries/reengagement_list";
#[derive(Deserialize, IntoParams)]
pub struct ModeratorReengagementListQueryParams {
/// Row offset for pagination (page size is fixed at 200). Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Payments namespace. Defaults to `artcraft`.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorReengagementListResponse {
pub success: bool,
pub candidates: Vec<ReengagementCandidateEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct ReengagementCandidateEntry {
pub user_token: UserToken,
pub username: String,
pub display_name: String,
pub email_gravatar_hash: String,
pub reengagement_score: u32,
pub lifetime_net_spend_usd_cents: u64,
pub maybe_last_payment_at: Option<DateTime<Utc>>,
pub maybe_days_since_last_payment: Option<u32>,
pub maybe_weeks_since_last_spend: Option<u32>,
pub is_active_subscriber: bool,
}
@@ -0,0 +1,77 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::users::UserToken;
use utoipa::{IntoParams, ToSchema};
pub const MODERATOR_TOP_USERS_LIST_PATH: &str =
"/v1/moderation/user_spend_summaries/top_users_list";
/// Net-spend window that selects which column `top_users_list` sorts by.
#[derive(Deserialize, Serialize, Clone, Copy, ToSchema)]
pub enum TopUsersWindow {
#[serde(rename = "7d")]
SevenDays,
#[serde(rename = "14d")]
FourteenDays,
#[serde(rename = "30d")]
ThirtyDays,
#[serde(rename = "60d")]
SixtyDays,
#[serde(rename = "90d")]
NinetyDays,
#[serde(rename = "1y")]
OneYear,
#[serde(rename = "lifetime")]
Lifetime,
}
impl TopUsersWindow {
/// The `window` key the `list_top_users_by_net_spend` query matches on.
pub fn as_query_key(self) -> &'static str {
match self {
TopUsersWindow::SevenDays => "7d",
TopUsersWindow::FourteenDays => "14d",
TopUsersWindow::ThirtyDays => "30d",
TopUsersWindow::SixtyDays => "60d",
TopUsersWindow::NinetyDays => "90d",
TopUsersWindow::OneYear => "1y",
TopUsersWindow::Lifetime => "lifetime",
}
}
}
#[derive(Deserialize, IntoParams)]
pub struct ModeratorTopUsersListQueryParams {
/// Sort window: `7d`, `14d`, `30d`, `60d`, `90d`, `1y`, or `lifetime` (default).
pub window: Option<TopUsersWindow>,
/// Row offset for pagination (page size is fixed at 200). Pass `next_offset` from the previous page.
pub offset: Option<u64>,
/// Payments namespace. Defaults to `artcraft`.
pub payments_namespace: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ModeratorTopUsersListResponse {
pub success: bool,
/// The window that was sorted by (echo of the request).
pub window: String,
pub users: Vec<TopUserEntry>,
/// Offset for the next page, or `None` if this was the last page.
pub maybe_next_offset: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct TopUserEntry {
pub user_token: UserToken,
pub username: String,
pub display_name: String,
pub email_gravatar_hash: String,
pub lifetime_net_spend_usd_cents: u64,
pub net_spend_7d_usd_cents: i64,
pub net_spend_14d_usd_cents: i64,
pub net_spend_30d_usd_cents: i64,
pub net_spend_60d_usd_cents: i64,
pub net_spend_90d_usd_cents: i64,
pub net_spend_this_year_usd_cents: i64,
pub is_active_subscriber: bool,
pub maybe_reengagement_score: Option<u32>,
}
@@ -0,0 +1,2 @@
pub mod omni_api_image_generate_request;
pub mod omni_api_video_generate_request;
@@ -0,0 +1,58 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_image_model::CommonImageModel;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation::common_resolution::CommonResolution;
use tokens::tokens::media_files::MediaFileToken;
/// Request body for the Omni API image generation endpoint.
///
/// Forked from `OmniGenImageCostAndGenerateRequest` so the Omni API's request
/// shape can evolve independently without affecting the product-critical Omni
/// Gen endpoints.
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
pub struct OmniApiImageGenerateRequest {
/// REQUIRED (even if marked optional)
/// Idempotency token to prevent duplicate requests.
/// If not supplied, we'll generate one for the required providers.
pub idempotency_token: Option<String>,
/// REQUIRED (even if marked optional)
/// Which model to use.
pub model: Option<CommonImageModel>,
/// The prompt for the image generation.
pub prompt: Option<String>,
/// Input images for image editing.
/// If present, we're doing image editing (image-to-image).
/// If absent, we're doing text-to-image.
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// URL alternative to `image_media_tokens`. Mutually exclusive with it.
/// Each URL is downloaded and stored as a media file before generation.
pub image_urls: Option<Vec<String>>,
/// The resolution to use.
pub resolution: Option<CommonResolution>,
/// The aspect ratio to use.
pub aspect_ratio: Option<CommonAspectRatio>,
/// The quality to use.
pub quality: Option<CommonQuality>,
/// How many images to generate.
pub image_batch_count: Option<u16>,
/// Only for angle manipulation models.
pub adjust_horizontal_angle: Option<f64>,
/// Only for angle manipulation models.
pub adjust_vertical_angle: Option<f64>,
/// Only for angle manipulation models.
pub adjust_zoom: Option<f64>,
}
@@ -0,0 +1,96 @@
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;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
/// Request body for the Omni API video generation endpoint.
///
/// Forked from `OmniGenVideoCostAndGenerateRequest` so the Omni API's request
/// shape can evolve independently without affecting the product-critical Omni
/// Gen endpoints.
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
pub struct OmniApiVideoGenerateRequest {
/// REQUIRED (even if marked optional)
/// Idempotency token to prevent duplicate requests.
pub idempotency_token: Option<String>,
/// REQUIRED (even if marked optional)
/// Which model to use.
pub model: Option<CommonVideoModel>,
/// The text prompt for the video generation.
pub prompt: Option<String>,
/// Some models support negative text prompts.
pub negative_prompt: Option<String>,
/// Starting keyframe (optional).
pub start_frame_image_media_token: Option<MediaFileToken>,
/// URL alternative to `start_frame_image_media_token`. Mutually exclusive with it.
/// Downloaded and stored as a media file before generation.
pub start_frame_image_url: Option<String>,
/// Ending keyframe (optional).
pub end_frame_image_media_token: Option<MediaFileToken>,
/// URL alternative to `end_frame_image_media_token`. Mutually exclusive with it.
/// Downloaded and stored as a media file before generation.
pub end_frame_image_url: Option<String>,
/// Reference images (optional).
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
/// URL alternative to `reference_image_media_tokens`. Mutually exclusive with it.
/// Each URL is downloaded and stored as a media file before generation.
pub reference_image_urls: Option<Vec<String>>,
/// Reference videos (optional).
pub reference_video_media_tokens: Option<Vec<MediaFileToken>>,
/// URL alternative to `reference_video_media_tokens`. Mutually exclusive with it.
/// Each URL is downloaded and stored as a media file before generation.
pub reference_video_urls: Option<Vec<String>>,
/// Reference audio (optional).
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
/// URL alternative to `reference_audio_media_tokens`. Mutually exclusive with it.
/// Each URL is downloaded and stored as a media file before generation.
pub reference_audio_urls: Option<Vec<String>>,
/// Optional character tokens to reference in the prompt.
/// Characters are referenced in prompts as @CharacterName.
pub reference_character_tokens: Option<Vec<CharacterToken>>,
/// The resolution to use.
pub resolution: Option<CommonResolution>,
/// 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>,
/// How many seconds to generate.
pub duration_seconds: Option<u16>,
/// How many videos to generate.
pub video_batch_count: Option<u16>,
/// Whether to turn on/off audio.
/// Not all models support audio, not all models have a choice.
/// Some models will default this to true, others will default it to false,
/// so it's best to be explicit.
pub generate_audio: Option<bool>,
}
@@ -0,0 +1,3 @@
pub mod omni_api_batch_get_job_status;
pub mod omni_api_get_job_status;
pub mod omni_api_job_status_payload;
@@ -0,0 +1,25 @@
use std::collections::HashSet;
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use crate::omni_api::job_status::omni_api_job_status_payload::OmniApiJobStatusPayload;
/// Query params for `GET /v1/omni_api/job_status/batch`.
#[derive(Deserialize, ToSchema, IntoParams)]
pub struct OmniApiBatchGetJobStatusQueryParams {
/// A grab bag of job tokens. Each token family generally has its own prefix,
/// so a flat set is sufficient.
///
/// NB: handlers parse this with `actix_web_lab`'s `Query<T>` because the
/// default actix-web `Query<T>` doesn't decode repeated/sequence params yet.
/// See https://github.com/actix/actix-web/issues/1301
pub tokens: HashSet<String>,
}
/// Success body for `GET /v1/omni_api/job_status/batch`.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct OmniApiBatchGetJobStatusSuccessResponse {
pub success: bool,
pub job_states: Vec<OmniApiJobStatusPayload>,
}
@@ -0,0 +1,19 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::omni_api::job_status::omni_api_job_status_payload::OmniApiJobStatusPayload;
/// Path params for `GET /v1/omni_api/job_status/job/{token}`.
#[derive(Deserialize, ToSchema)]
pub struct OmniApiGetJobStatusPathInfo {
pub token: InferenceJobToken,
}
/// Success body for `GET /v1/omni_api/job_status/job/{token}`.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct OmniApiGetJobStatusSuccessResponse {
pub success: bool,
pub state: OmniApiJobStatusPayload,
}
@@ -0,0 +1,69 @@
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::common::job_status_plus::JobStatusPlus;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::prompts::PromptToken;
use crate::common::responses::media_links::MediaLinks;
/// Full status payload for a single inference job.
///
/// Shared by both the single (`/v1/omni_api/job_status/job/{token}`) and batch
/// (`/v1/omni_api/job_status/batch`) Omni API status endpoints. Forked from the
/// cookie-authenticated `/v1/jobs/*` response types so the Omni API surface can
/// evolve independently.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct OmniApiJobStatusPayload {
pub job_token: InferenceJobToken,
pub request: OmniApiJobRequestDetails,
pub status: OmniApiJobStatusDetails,
pub maybe_result: Option<OmniApiJobResultDetails>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Details about what the user requested for generation.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct OmniApiJobRequestDetails {
pub inference_category: InferenceCategory,
pub maybe_prompt_token: Option<PromptToken>,
pub maybe_model_type: Option<String>,
pub maybe_model_token: Option<String>,
}
/// Details about the ongoing job status.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct OmniApiJobStatusDetails {
/// Primary status from the database (a state machine).
pub status: JobStatusPlus,
pub maybe_first_started_at: Option<DateTime<Utc>>,
/// An enum the frontend can use to display localized/I18N error
/// messages. These pertain to both transient and permanent failures.
pub maybe_failure_category: Option<FrontendFailureCategory>,
/// This is an integer number between 0 and 100 (both inclusive) that
/// reports the completeness.
pub progress_percentage: u8,
}
/// Details about the completed result (if any).
#[derive(Serialize, Deserialize, ToSchema)]
pub struct OmniApiJobResultDetails {
pub entity_type: String,
pub entity_token: String,
/// Rich CDN links to the media, including thumbnails, previews, and more.
pub media_links: MediaLinks,
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
}
@@ -1,3 +1,5 @@
pub mod generate_requests;
pub mod job_status;
pub mod omni_upload_audio;
pub mod omni_upload_image;
pub mod omni_upload_video;
@@ -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,131 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonAspectRatio` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientAspectRatio {
Auto,
Square,
WideThreeByTwo,
WideFourByThree,
WideFiveByFour,
WideSixteenByNine,
WideTwentyOneByNine,
TallTwoByThree,
TallThreeByFour,
TallFourByFive,
TallNineBySixteen,
TallNineByTwentyOne,
Wide,
Tall,
Auto2k,
Auto3k,
Auto4k,
SquareHd,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientAspectRatio {
fn from(value: String) -> Self {
match value.as_str() {
"auto" => Self::Auto,
"square" => Self::Square,
"wide_three_by_two" => Self::WideThreeByTwo,
"wide_four_by_three" => Self::WideFourByThree,
"wide_five_by_four" => Self::WideFiveByFour,
"wide_sixteen_by_nine" => Self::WideSixteenByNine,
"wide_twenty_one_by_nine" => Self::WideTwentyOneByNine,
"tall_two_by_three" => Self::TallTwoByThree,
"tall_three_by_four" => Self::TallThreeByFour,
"tall_four_by_five" => Self::TallFourByFive,
"tall_nine_by_sixteen" => Self::TallNineBySixteen,
"tall_nine_by_twenty_one" => Self::TallNineByTwentyOne,
"wide" => Self::Wide,
"tall" => Self::Tall,
"auto_2k" => Self::Auto2k,
"auto_3k" => Self::Auto3k,
"auto_4k" => Self::Auto4k,
"square_hd" => Self::SquareHd,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientAspectRatio> for String {
fn from(value: ApiClientAspectRatio) -> Self {
match value {
ApiClientAspectRatio::Auto => "auto".to_string(),
ApiClientAspectRatio::Square => "square".to_string(),
ApiClientAspectRatio::WideThreeByTwo => "wide_three_by_two".to_string(),
ApiClientAspectRatio::WideFourByThree => "wide_four_by_three".to_string(),
ApiClientAspectRatio::WideFiveByFour => "wide_five_by_four".to_string(),
ApiClientAspectRatio::WideSixteenByNine => "wide_sixteen_by_nine".to_string(),
ApiClientAspectRatio::WideTwentyOneByNine => "wide_twenty_one_by_nine".to_string(),
ApiClientAspectRatio::TallTwoByThree => "tall_two_by_three".to_string(),
ApiClientAspectRatio::TallThreeByFour => "tall_three_by_four".to_string(),
ApiClientAspectRatio::TallFourByFive => "tall_four_by_five".to_string(),
ApiClientAspectRatio::TallNineBySixteen => "tall_nine_by_sixteen".to_string(),
ApiClientAspectRatio::TallNineByTwentyOne => "tall_nine_by_twenty_one".to_string(),
ApiClientAspectRatio::Wide => "wide".to_string(),
ApiClientAspectRatio::Tall => "tall".to_string(),
ApiClientAspectRatio::Auto2k => "auto_2k".to_string(),
ApiClientAspectRatio::Auto3k => "auto_3k".to_string(),
ApiClientAspectRatio::Auto4k => "auto_4k".to_string(),
ApiClientAspectRatio::SquareHd => "square_hd".to_string(),
ApiClientAspectRatio::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientAspectRatio = serde_json::from_str("\"auto\"").unwrap();
assert_eq!(parsed, ApiClientAspectRatio::Auto);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"auto\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientAspectRatio = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientAspectRatio::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientAspectRatio::Auto, "auto"),
(ApiClientAspectRatio::Square, "square"),
(ApiClientAspectRatio::WideThreeByTwo, "wide_three_by_two"),
(ApiClientAspectRatio::WideFourByThree, "wide_four_by_three"),
(ApiClientAspectRatio::WideFiveByFour, "wide_five_by_four"),
(ApiClientAspectRatio::WideSixteenByNine, "wide_sixteen_by_nine"),
(ApiClientAspectRatio::WideTwentyOneByNine, "wide_twenty_one_by_nine"),
(ApiClientAspectRatio::TallTwoByThree, "tall_two_by_three"),
(ApiClientAspectRatio::TallThreeByFour, "tall_three_by_four"),
(ApiClientAspectRatio::TallFourByFive, "tall_four_by_five"),
(ApiClientAspectRatio::TallNineBySixteen, "tall_nine_by_sixteen"),
(ApiClientAspectRatio::TallNineByTwentyOne, "tall_nine_by_twenty_one"),
(ApiClientAspectRatio::Wide, "wide"),
(ApiClientAspectRatio::Tall, "tall"),
(ApiClientAspectRatio::Auto2k, "auto_2k"),
(ApiClientAspectRatio::Auto3k, "auto_3k"),
(ApiClientAspectRatio::Auto4k, "auto_4k"),
(ApiClientAspectRatio::SquareHd, "square_hd"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientAspectRatio = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -0,0 +1,67 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonBitrate` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientBitrate {
Normal,
High,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientBitrate {
fn from(value: String) -> Self {
match value.as_str() {
"normal" => Self::Normal,
"high" => Self::High,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientBitrate> for String {
fn from(value: ApiClientBitrate) -> Self {
match value {
ApiClientBitrate::Normal => "normal".to_string(),
ApiClientBitrate::High => "high".to_string(),
ApiClientBitrate::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientBitrate = serde_json::from_str("\"normal\"").unwrap();
assert_eq!(parsed, ApiClientBitrate::Normal);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"normal\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientBitrate = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientBitrate::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientBitrate::Normal, "normal"),
(ApiClientBitrate::High, "high"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientBitrate = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -0,0 +1,83 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `GenerationProvider` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientGenerationProvider {
Artcraft,
Fal,
Grok,
Midjourney,
Sora,
WorldLabs,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientGenerationProvider {
fn from(value: String) -> Self {
match value.as_str() {
"artcraft" => Self::Artcraft,
"fal" => Self::Fal,
"grok" => Self::Grok,
"midjourney" => Self::Midjourney,
"sora" => Self::Sora,
"world_labs" => Self::WorldLabs,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientGenerationProvider> for String {
fn from(value: ApiClientGenerationProvider) -> Self {
match value {
ApiClientGenerationProvider::Artcraft => "artcraft".to_string(),
ApiClientGenerationProvider::Fal => "fal".to_string(),
ApiClientGenerationProvider::Grok => "grok".to_string(),
ApiClientGenerationProvider::Midjourney => "midjourney".to_string(),
ApiClientGenerationProvider::Sora => "sora".to_string(),
ApiClientGenerationProvider::WorldLabs => "world_labs".to_string(),
ApiClientGenerationProvider::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientGenerationProvider = serde_json::from_str("\"artcraft\"").unwrap();
assert_eq!(parsed, ApiClientGenerationProvider::Artcraft);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"artcraft\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientGenerationProvider = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientGenerationProvider::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientGenerationProvider::Artcraft, "artcraft"),
(ApiClientGenerationProvider::Fal, "fal"),
(ApiClientGenerationProvider::Grok, "grok"),
(ApiClientGenerationProvider::Midjourney, "midjourney"),
(ApiClientGenerationProvider::Sora, "sora"),
(ApiClientGenerationProvider::WorldLabs, "world_labs"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientGenerationProvider = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -0,0 +1,139 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonImageModel` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientImageModel {
Flux1Dev,
Flux1Schnell,
FluxPro11,
FluxPro11Ultra,
GptImage1,
GptImage1p5,
GptImage2,
GrokImagineImage,
GrokImagineImageQuality,
Midjourney7,
Midjourney7Niji,
Midjourney8,
NanoBanana,
NanoBanana2,
NanoBananaPro,
Seedream4,
Seedream4p5,
Seedream5Lite,
QwenEdit2511Angles,
Flux2LoraAngles,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientImageModel {
fn from(value: String) -> Self {
match value.as_str() {
"flux_1_dev" => Self::Flux1Dev,
"flux_1_schnell" => Self::Flux1Schnell,
"flux_pro_1p1" => Self::FluxPro11,
"flux_pro_1p1_ultra" => Self::FluxPro11Ultra,
"gpt_image_1" => Self::GptImage1,
"gpt_image_1p5" => Self::GptImage1p5,
"gpt_image_2" => Self::GptImage2,
"grok_imagine_image" => Self::GrokImagineImage,
"grok_imagine_image_q" => Self::GrokImagineImageQuality,
"midjourney_7" => Self::Midjourney7,
"midjourney_7_niji" => Self::Midjourney7Niji,
"midjourney_8" => Self::Midjourney8,
"nano_banana" => Self::NanoBanana,
"nano_banana_2" => Self::NanoBanana2,
"nano_banana_pro" => Self::NanoBananaPro,
"seedream_4" => Self::Seedream4,
"seedream_4p5" => Self::Seedream4p5,
"seedream_5_lite" => Self::Seedream5Lite,
"qwen_edit_2511_angles" => Self::QwenEdit2511Angles,
"flux_2_lora_angles" => Self::Flux2LoraAngles,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientImageModel> for String {
fn from(value: ApiClientImageModel) -> Self {
match value {
ApiClientImageModel::Flux1Dev => "flux_1_dev".to_string(),
ApiClientImageModel::Flux1Schnell => "flux_1_schnell".to_string(),
ApiClientImageModel::FluxPro11 => "flux_pro_1p1".to_string(),
ApiClientImageModel::FluxPro11Ultra => "flux_pro_1p1_ultra".to_string(),
ApiClientImageModel::GptImage1 => "gpt_image_1".to_string(),
ApiClientImageModel::GptImage1p5 => "gpt_image_1p5".to_string(),
ApiClientImageModel::GptImage2 => "gpt_image_2".to_string(),
ApiClientImageModel::GrokImagineImage => "grok_imagine_image".to_string(),
ApiClientImageModel::GrokImagineImageQuality => "grok_imagine_image_q".to_string(),
ApiClientImageModel::Midjourney7 => "midjourney_7".to_string(),
ApiClientImageModel::Midjourney7Niji => "midjourney_7_niji".to_string(),
ApiClientImageModel::Midjourney8 => "midjourney_8".to_string(),
ApiClientImageModel::NanoBanana => "nano_banana".to_string(),
ApiClientImageModel::NanoBanana2 => "nano_banana_2".to_string(),
ApiClientImageModel::NanoBananaPro => "nano_banana_pro".to_string(),
ApiClientImageModel::Seedream4 => "seedream_4".to_string(),
ApiClientImageModel::Seedream4p5 => "seedream_4p5".to_string(),
ApiClientImageModel::Seedream5Lite => "seedream_5_lite".to_string(),
ApiClientImageModel::QwenEdit2511Angles => "qwen_edit_2511_angles".to_string(),
ApiClientImageModel::Flux2LoraAngles => "flux_2_lora_angles".to_string(),
ApiClientImageModel::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientImageModel = serde_json::from_str("\"flux_1_dev\"").unwrap();
assert_eq!(parsed, ApiClientImageModel::Flux1Dev);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"flux_1_dev\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientImageModel = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientImageModel::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientImageModel::Flux1Dev, "flux_1_dev"),
(ApiClientImageModel::Flux1Schnell, "flux_1_schnell"),
(ApiClientImageModel::FluxPro11, "flux_pro_1p1"),
(ApiClientImageModel::FluxPro11Ultra, "flux_pro_1p1_ultra"),
(ApiClientImageModel::GptImage1, "gpt_image_1"),
(ApiClientImageModel::GptImage1p5, "gpt_image_1p5"),
(ApiClientImageModel::GptImage2, "gpt_image_2"),
(ApiClientImageModel::GrokImagineImage, "grok_imagine_image"),
(ApiClientImageModel::GrokImagineImageQuality, "grok_imagine_image_q"),
(ApiClientImageModel::Midjourney7, "midjourney_7"),
(ApiClientImageModel::Midjourney7Niji, "midjourney_7_niji"),
(ApiClientImageModel::Midjourney8, "midjourney_8"),
(ApiClientImageModel::NanoBanana, "nano_banana"),
(ApiClientImageModel::NanoBanana2, "nano_banana_2"),
(ApiClientImageModel::NanoBananaPro, "nano_banana_pro"),
(ApiClientImageModel::Seedream4, "seedream_4"),
(ApiClientImageModel::Seedream4p5, "seedream_4p5"),
(ApiClientImageModel::Seedream5Lite, "seedream_5_lite"),
(ApiClientImageModel::QwenEdit2511Angles, "qwen_edit_2511_angles"),
(ApiClientImageModel::Flux2LoraAngles, "flux_2_lora_angles"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientImageModel = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -0,0 +1,147 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `ModelCreator` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientModelCreator {
Alibaba,
ArtCraft,
BlackForestLabs,
Bytedance,
Fal,
Google,
Grok,
Hailuo,
Higgsfield,
Kling,
Krea,
Midjourney,
OpenAi,
OpenArt,
Recraft,
Replicate,
Runway,
Stability,
Tencent,
TensorArt,
Vidu,
WorldLabs,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientModelCreator {
fn from(value: String) -> Self {
match value.as_str() {
"alibaba" => Self::Alibaba,
"artcraft" => Self::ArtCraft,
"black_forest_labs" => Self::BlackForestLabs,
"bytedance" => Self::Bytedance,
"fal" => Self::Fal,
"google" => Self::Google,
"grok" => Self::Grok,
"hailuo" => Self::Hailuo,
"higgsfield" => Self::Higgsfield,
"kling" => Self::Kling,
"krea" => Self::Krea,
"midjourney" => Self::Midjourney,
"open_ai" => Self::OpenAi,
"open_art" => Self::OpenArt,
"recraft" => Self::Recraft,
"replicate" => Self::Replicate,
"runway" => Self::Runway,
"stability" => Self::Stability,
"tencent" => Self::Tencent,
"tensor_art" => Self::TensorArt,
"vidu" => Self::Vidu,
"world_labs" => Self::WorldLabs,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientModelCreator> for String {
fn from(value: ApiClientModelCreator) -> Self {
match value {
ApiClientModelCreator::Alibaba => "alibaba".to_string(),
ApiClientModelCreator::ArtCraft => "artcraft".to_string(),
ApiClientModelCreator::BlackForestLabs => "black_forest_labs".to_string(),
ApiClientModelCreator::Bytedance => "bytedance".to_string(),
ApiClientModelCreator::Fal => "fal".to_string(),
ApiClientModelCreator::Google => "google".to_string(),
ApiClientModelCreator::Grok => "grok".to_string(),
ApiClientModelCreator::Hailuo => "hailuo".to_string(),
ApiClientModelCreator::Higgsfield => "higgsfield".to_string(),
ApiClientModelCreator::Kling => "kling".to_string(),
ApiClientModelCreator::Krea => "krea".to_string(),
ApiClientModelCreator::Midjourney => "midjourney".to_string(),
ApiClientModelCreator::OpenAi => "open_ai".to_string(),
ApiClientModelCreator::OpenArt => "open_art".to_string(),
ApiClientModelCreator::Recraft => "recraft".to_string(),
ApiClientModelCreator::Replicate => "replicate".to_string(),
ApiClientModelCreator::Runway => "runway".to_string(),
ApiClientModelCreator::Stability => "stability".to_string(),
ApiClientModelCreator::Tencent => "tencent".to_string(),
ApiClientModelCreator::TensorArt => "tensor_art".to_string(),
ApiClientModelCreator::Vidu => "vidu".to_string(),
ApiClientModelCreator::WorldLabs => "world_labs".to_string(),
ApiClientModelCreator::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientModelCreator = serde_json::from_str("\"alibaba\"").unwrap();
assert_eq!(parsed, ApiClientModelCreator::Alibaba);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"alibaba\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientModelCreator = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientModelCreator::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientModelCreator::Alibaba, "alibaba"),
(ApiClientModelCreator::ArtCraft, "artcraft"),
(ApiClientModelCreator::BlackForestLabs, "black_forest_labs"),
(ApiClientModelCreator::Bytedance, "bytedance"),
(ApiClientModelCreator::Fal, "fal"),
(ApiClientModelCreator::Google, "google"),
(ApiClientModelCreator::Grok, "grok"),
(ApiClientModelCreator::Hailuo, "hailuo"),
(ApiClientModelCreator::Higgsfield, "higgsfield"),
(ApiClientModelCreator::Kling, "kling"),
(ApiClientModelCreator::Krea, "krea"),
(ApiClientModelCreator::Midjourney, "midjourney"),
(ApiClientModelCreator::OpenAi, "open_ai"),
(ApiClientModelCreator::OpenArt, "open_art"),
(ApiClientModelCreator::Recraft, "recraft"),
(ApiClientModelCreator::Replicate, "replicate"),
(ApiClientModelCreator::Runway, "runway"),
(ApiClientModelCreator::Stability, "stability"),
(ApiClientModelCreator::Tencent, "tencent"),
(ApiClientModelCreator::TensorArt, "tensor_art"),
(ApiClientModelCreator::Vidu, "vidu"),
(ApiClientModelCreator::WorldLabs, "world_labs"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientModelCreator = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -0,0 +1,71 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonQuality` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientQuality {
High,
Medium,
Low,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientQuality {
fn from(value: String) -> Self {
match value.as_str() {
"high" => Self::High,
"medium" => Self::Medium,
"low" => Self::Low,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientQuality> for String {
fn from(value: ApiClientQuality) -> Self {
match value {
ApiClientQuality::High => "high".to_string(),
ApiClientQuality::Medium => "medium".to_string(),
ApiClientQuality::Low => "low".to_string(),
ApiClientQuality::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientQuality = serde_json::from_str("\"high\"").unwrap();
assert_eq!(parsed, ApiClientQuality::High);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"high\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientQuality = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientQuality::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientQuality::High, "high"),
(ApiClientQuality::Medium, "medium"),
(ApiClientQuality::Low, "low"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientQuality = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -0,0 +1,91 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonResolution` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientResolution {
OneK,
TwoK,
ThreeK,
FourK,
HalfK,
FourEightyP,
SevenTwentyP,
TenEightyP,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientResolution {
fn from(value: String) -> Self {
match value.as_str() {
"one_k" => Self::OneK,
"two_k" => Self::TwoK,
"three_k" => Self::ThreeK,
"four_k" => Self::FourK,
"half_k" => Self::HalfK,
"four_eighty_p" => Self::FourEightyP,
"seven_twenty_p" => Self::SevenTwentyP,
"ten_eighty_p" => Self::TenEightyP,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientResolution> for String {
fn from(value: ApiClientResolution) -> Self {
match value {
ApiClientResolution::OneK => "one_k".to_string(),
ApiClientResolution::TwoK => "two_k".to_string(),
ApiClientResolution::ThreeK => "three_k".to_string(),
ApiClientResolution::FourK => "four_k".to_string(),
ApiClientResolution::HalfK => "half_k".to_string(),
ApiClientResolution::FourEightyP => "four_eighty_p".to_string(),
ApiClientResolution::SevenTwentyP => "seven_twenty_p".to_string(),
ApiClientResolution::TenEightyP => "ten_eighty_p".to_string(),
ApiClientResolution::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientResolution = serde_json::from_str("\"one_k\"").unwrap();
assert_eq!(parsed, ApiClientResolution::OneK);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"one_k\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientResolution = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientResolution::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientResolution::OneK, "one_k"),
(ApiClientResolution::TwoK, "two_k"),
(ApiClientResolution::ThreeK, "three_k"),
(ApiClientResolution::FourK, "four_k"),
(ApiClientResolution::HalfK, "half_k"),
(ApiClientResolution::FourEightyP, "four_eighty_p"),
(ApiClientResolution::SevenTwentyP, "seven_twenty_p"),
(ApiClientResolution::TenEightyP, "ten_eighty_p"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientResolution = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -0,0 +1,191 @@
use serde_derive::{Deserialize, Serialize};
/// Forward-compatible, self-contained API-client copy of the server's `CommonVideoModel` enum.
///
/// Serialized as a string. Any value this client build does not recognize is preserved
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
/// still round-trip back to the original string.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientVideoModel {
GrokVideo,
GrokImagineVideo,
GrokImagineVideo1p5,
Kling16Pro,
Kling21Pro,
Kling21Master,
Kling2p5TurboPro,
Kling2p6Pro,
Kling3p0Standard,
Kling3p0Pro,
HappyHorse1p0,
Seedance10Lite,
Seedance1p5Pro,
Seedance2p0,
Seedance2p0Fast,
Seedance2p0BytePlus,
Seedance2p0BytePlusFast,
Seedance2p0Ultra,
Seedance2p0UltraFast,
Seedance2p0BytePlusUltra,
Seedance2p0BytePlusUltraFast,
Seedance2p0Mini,
Seedance2p0BytePlusMini,
Seedance2p0BytePlusUltraMini,
Sora2,
Sora2Pro,
Veo2,
Veo3,
Veo3Fast,
Veo3p1,
Veo3p1Fast,
PreviewModel,
PreviewModelFast,
/// A value not known to this client build (preserved as-is).
Unknown(String),
}
impl From<String> for ApiClientVideoModel {
fn from(value: String) -> Self {
match value.as_str() {
"grok_video" => Self::GrokVideo,
"grok_imagine_video" => Self::GrokImagineVideo,
"grok_imagine_video_1p5" => Self::GrokImagineVideo1p5,
"kling_1p6_pro" => Self::Kling16Pro,
"kling_2p1_pro" => Self::Kling21Pro,
"kling_2p1_master" => Self::Kling21Master,
"kling_2p5_turbo_pro" => Self::Kling2p5TurboPro,
"kling_2p6_pro" => Self::Kling2p6Pro,
"kling_3p0_standard" => Self::Kling3p0Standard,
"kling_3p0_pro" => Self::Kling3p0Pro,
"happy_horse_1p0" => Self::HappyHorse1p0,
"seedance_1p0_lite" => Self::Seedance10Lite,
"seedance_1p5_pro" => Self::Seedance1p5Pro,
"seedance_2p0" => Self::Seedance2p0,
"seedance_2p0_fast" => Self::Seedance2p0Fast,
"seedance_2p0_bp" => Self::Seedance2p0BytePlus,
"seedance_2p0_bp_fast" => Self::Seedance2p0BytePlusFast,
"seedance_2p0_u" => Self::Seedance2p0Ultra,
"seedance_2p0_u_fast" => Self::Seedance2p0UltraFast,
"seedance_2p0_bpu" => Self::Seedance2p0BytePlusUltra,
"seedance_2p0_bpu_fast" => Self::Seedance2p0BytePlusUltraFast,
"seedance_2p0_mini" => Self::Seedance2p0Mini,
"seedance_2p0_bp_mini" => Self::Seedance2p0BytePlusMini,
"seedance_2p0_bpu_mini" => Self::Seedance2p0BytePlusUltraMini,
"sora_2" => Self::Sora2,
"sora_2_pro" => Self::Sora2Pro,
"veo_2" => Self::Veo2,
"veo_3" => Self::Veo3,
"veo_3_fast" => Self::Veo3Fast,
"veo_3p1" => Self::Veo3p1,
"veo_3p1_fast" => Self::Veo3p1Fast,
"preview_model" => Self::PreviewModel,
"preview_model_fast" => Self::PreviewModelFast,
_ => Self::Unknown(value),
}
}
}
impl From<ApiClientVideoModel> for String {
fn from(value: ApiClientVideoModel) -> Self {
match value {
ApiClientVideoModel::GrokVideo => "grok_video".to_string(),
ApiClientVideoModel::GrokImagineVideo => "grok_imagine_video".to_string(),
ApiClientVideoModel::GrokImagineVideo1p5 => "grok_imagine_video_1p5".to_string(),
ApiClientVideoModel::Kling16Pro => "kling_1p6_pro".to_string(),
ApiClientVideoModel::Kling21Pro => "kling_2p1_pro".to_string(),
ApiClientVideoModel::Kling21Master => "kling_2p1_master".to_string(),
ApiClientVideoModel::Kling2p5TurboPro => "kling_2p5_turbo_pro".to_string(),
ApiClientVideoModel::Kling2p6Pro => "kling_2p6_pro".to_string(),
ApiClientVideoModel::Kling3p0Standard => "kling_3p0_standard".to_string(),
ApiClientVideoModel::Kling3p0Pro => "kling_3p0_pro".to_string(),
ApiClientVideoModel::HappyHorse1p0 => "happy_horse_1p0".to_string(),
ApiClientVideoModel::Seedance10Lite => "seedance_1p0_lite".to_string(),
ApiClientVideoModel::Seedance1p5Pro => "seedance_1p5_pro".to_string(),
ApiClientVideoModel::Seedance2p0 => "seedance_2p0".to_string(),
ApiClientVideoModel::Seedance2p0Fast => "seedance_2p0_fast".to_string(),
ApiClientVideoModel::Seedance2p0BytePlus => "seedance_2p0_bp".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusFast => "seedance_2p0_bp_fast".to_string(),
ApiClientVideoModel::Seedance2p0Ultra => "seedance_2p0_u".to_string(),
ApiClientVideoModel::Seedance2p0UltraFast => "seedance_2p0_u_fast".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusUltra => "seedance_2p0_bpu".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusUltraFast => "seedance_2p0_bpu_fast".to_string(),
ApiClientVideoModel::Seedance2p0Mini => "seedance_2p0_mini".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusMini => "seedance_2p0_bp_mini".to_string(),
ApiClientVideoModel::Seedance2p0BytePlusUltraMini => "seedance_2p0_bpu_mini".to_string(),
ApiClientVideoModel::Sora2 => "sora_2".to_string(),
ApiClientVideoModel::Sora2Pro => "sora_2_pro".to_string(),
ApiClientVideoModel::Veo2 => "veo_2".to_string(),
ApiClientVideoModel::Veo3 => "veo_3".to_string(),
ApiClientVideoModel::Veo3Fast => "veo_3_fast".to_string(),
ApiClientVideoModel::Veo3p1 => "veo_3p1".to_string(),
ApiClientVideoModel::Veo3p1Fast => "veo_3p1_fast".to_string(),
ApiClientVideoModel::PreviewModel => "preview_model".to_string(),
ApiClientVideoModel::PreviewModelFast => "preview_model_fast".to_string(),
ApiClientVideoModel::Unknown(other) => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_variant_round_trips() {
let parsed: ApiClientVideoModel = serde_json::from_str("\"grok_video\"").unwrap();
assert_eq!(parsed, ApiClientVideoModel::GrokVideo);
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"grok_video\"");
}
#[test]
fn unknown_value_is_preserved_and_round_trips() {
let parsed: ApiClientVideoModel = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
assert_eq!(parsed, ApiClientVideoModel::Unknown("zzz_not_a_real_value".to_string()));
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
}
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientVideoModel::GrokVideo, "grok_video"),
(ApiClientVideoModel::GrokImagineVideo, "grok_imagine_video"),
(ApiClientVideoModel::GrokImagineVideo1p5, "grok_imagine_video_1p5"),
(ApiClientVideoModel::Kling16Pro, "kling_1p6_pro"),
(ApiClientVideoModel::Kling21Pro, "kling_2p1_pro"),
(ApiClientVideoModel::Kling21Master, "kling_2p1_master"),
(ApiClientVideoModel::Kling2p5TurboPro, "kling_2p5_turbo_pro"),
(ApiClientVideoModel::Kling2p6Pro, "kling_2p6_pro"),
(ApiClientVideoModel::Kling3p0Standard, "kling_3p0_standard"),
(ApiClientVideoModel::Kling3p0Pro, "kling_3p0_pro"),
(ApiClientVideoModel::HappyHorse1p0, "happy_horse_1p0"),
(ApiClientVideoModel::Seedance10Lite, "seedance_1p0_lite"),
(ApiClientVideoModel::Seedance1p5Pro, "seedance_1p5_pro"),
(ApiClientVideoModel::Seedance2p0, "seedance_2p0"),
(ApiClientVideoModel::Seedance2p0Fast, "seedance_2p0_fast"),
(ApiClientVideoModel::Seedance2p0BytePlus, "seedance_2p0_bp"),
(ApiClientVideoModel::Seedance2p0BytePlusFast, "seedance_2p0_bp_fast"),
(ApiClientVideoModel::Seedance2p0Ultra, "seedance_2p0_u"),
(ApiClientVideoModel::Seedance2p0UltraFast, "seedance_2p0_u_fast"),
(ApiClientVideoModel::Seedance2p0BytePlusUltra, "seedance_2p0_bpu"),
(ApiClientVideoModel::Seedance2p0BytePlusUltraFast, "seedance_2p0_bpu_fast"),
(ApiClientVideoModel::Seedance2p0Mini, "seedance_2p0_mini"),
(ApiClientVideoModel::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini"),
(ApiClientVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini"),
(ApiClientVideoModel::Sora2, "sora_2"),
(ApiClientVideoModel::Sora2Pro, "sora_2_pro"),
(ApiClientVideoModel::Veo2, "veo_2"),
(ApiClientVideoModel::Veo3, "veo_3"),
(ApiClientVideoModel::Veo3Fast, "veo_3_fast"),
(ApiClientVideoModel::Veo3p1, "veo_3p1"),
(ApiClientVideoModel::Veo3p1Fast, "veo_3p1_fast"),
(ApiClientVideoModel::PreviewModel, "preview_model"),
(ApiClientVideoModel::PreviewModelFast, "preview_model_fast"),
];
for (variant, s) in all {
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
let parsed: ApiClientVideoModel = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
assert_eq!(parsed, variant);
}
}
}
@@ -0,0 +1,8 @@
pub mod api_client_aspect_ratio;
pub mod api_client_bitrate;
pub mod api_client_generation_provider;
pub mod api_client_image_model;
pub mod api_client_model_creator;
pub mod api_client_quality;
pub mod api_client_resolution;
pub mod api_client_video_model;
@@ -0,0 +1 @@
pub mod generation;
@@ -0,0 +1 @@
pub mod common_enums;
@@ -1 +1,2 @@
pub mod generate;
pub mod models;
@@ -0,0 +1 @@
pub mod omni_gen_list_image_models;
@@ -0,0 +1,237 @@
//! Self-contained, forward-compatible binding for `GET /v1/omni_gen/models/image`.
//!
//! IMPORTANT: This module deliberately does NOT reuse any types from
//! `artcraft_api_defs`. Client builds may be deployed in the wild long after the
//! server's models, enums, and response shapes have changed, so every request /
//! response type is copied here and made permissive:
//! - Unknown JSON fields are ignored (serde default behavior).
//! - Missing collection/flag fields default rather than fail (`serde(default)`).
//! - Every string-valued enum has an `Unknown(String)` catch-all so new server
//! variants deserialize instead of erroring.
use serde_derive::{Deserialize, Serialize};
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::datatypes::common_enums::generation::api_client_aspect_ratio::ApiClientAspectRatio;
use crate::datatypes::common_enums::generation::api_client_generation_provider::ApiClientGenerationProvider;
use crate::datatypes::common_enums::generation::api_client_image_model::ApiClientImageModel;
use crate::datatypes::common_enums::generation::api_client_model_creator::ApiClientModelCreator;
use crate::datatypes::common_enums::generation::api_client_quality::ApiClientQuality;
use crate::datatypes::common_enums::generation::api_client_resolution::ApiClientResolution;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_get_request::basic_json_get_request;
pub const OMNI_GEN_IMAGE_MODELS_PATH: &str = "/v1/omni_gen/models/image";
/// Arguments for [`omni_gen_list_image_models`].
pub struct OmniGenListImageModelsArgs<'a> {
pub api_host: &'a ApiHost,
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
/// Which provider's models to list. `None` lets the server default (artcraft).
pub provider: Option<OmniGenImageModelsProvider>,
}
/// List available image models.
pub async fn omni_gen_list_image_models(
args: OmniGenListImageModelsArgs<'_>,
) -> Result<OmniGenImageModelsResponse, StorytellerError> {
let path = match args.provider {
Some(provider) => format!("{}?provider={}", OMNI_GEN_IMAGE_MODELS_PATH, provider.as_query_value()),
None => OMNI_GEN_IMAGE_MODELS_PATH.to_string(),
};
Ok(basic_json_get_request(args.api_host, &path, args.maybe_creds).await?)
}
/// The provider filter for the models endpoint (a client-supplied request value).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OmniGenImageModelsProvider {
/// Only models available through ArtCraft.
Artcraft,
/// All known models across all providers.
All,
}
impl OmniGenImageModelsProvider {
pub fn as_query_value(self) -> &'static str {
match self {
Self::Artcraft => "artcraft",
Self::All => "all",
}
}
}
impl Default for OmniGenImageModelsProvider {
fn default() -> Self {
Self::Artcraft
}
}
// ============================ Response types ============================
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenImageModelsResponse {
#[serde(default)]
pub success: bool,
#[serde(default)]
pub models: Vec<OmniGenImageModelDetails>,
#[serde(default)]
pub providers: Vec<OmniGenImageModelProviderDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenImageModelProviderDetails {
pub provider: ApiClientGenerationProvider,
#[serde(default)]
pub models: Vec<OmniGenImageProviderModelDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenImageProviderModelDetails {
pub model: ApiClientImageModel,
#[serde(default)]
pub overrides: Option<OmniGenImageModelDetails>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OmniGenImageModelDetails {
pub model: ApiClientImageModel,
#[serde(default)]
pub model_creator: Option<ApiClientModelCreator>,
#[serde(default)]
pub full_name: Option<String>,
#[serde(default)]
pub text_prompt_supported: Option<bool>,
#[serde(default)]
pub text_prompt_max_length: Option<u16>,
#[serde(default)]
pub negative_text_prompt_supported: Option<bool>,
#[serde(default)]
pub negative_text_prompt_max_length: Option<u16>,
#[serde(default)]
pub image_refs_supported: Option<bool>,
#[serde(default)]
pub image_refs_max: Option<u16>,
#[serde(default)]
pub has_fixed_editing_aspect_ratio: Option<bool>,
#[serde(default)]
pub aspect_ratio_options: Option<Vec<ApiClientAspectRatio>>,
#[serde(default)]
pub aspect_ratio_default: Option<ApiClientAspectRatio>,
#[serde(default)]
pub aspect_ratio_default_when_editing: Option<ApiClientAspectRatio>,
#[serde(default)]
pub resolution_options: Option<Vec<ApiClientResolution>>,
#[serde(default)]
pub resolution_default: Option<ApiClientResolution>,
#[serde(default)]
pub quality_options: Option<Vec<ApiClientQuality>>,
#[serde(default)]
pub default_quality: Option<ApiClientQuality>,
#[serde(default)]
pub batch_size_min: Option<u16>,
#[serde(default)]
pub batch_size_max: Option<u16>,
#[serde(default)]
pub batch_size_options: Option<Vec<u16>>,
#[serde(default)]
pub batch_size_default: Option<u16>,
#[serde(default)]
pub is_disabled: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn response_tolerates_unknown_fields_and_variants() {
let json = r#"{
"success": true,
"models": [
{
"model": "brand_new_image_model",
"model_creator": "some_new_studio",
"aspect_ratio_options": ["square", "some_new_ratio"],
"surprise_new_field": 123
}
],
"providers": [
{ "provider": "new_provider", "models": [ { "model": "nano_banana_pro" } ] }
],
"another_unexpected_top_level_field": "ignored"
}"#;
let resp: OmniGenImageModelsResponse = serde_json::from_str(json).unwrap();
assert!(resp.success);
assert_eq!(resp.models.len(), 1);
assert_eq!(resp.models[0].model, ApiClientImageModel::Unknown("brand_new_image_model".to_string()));
assert_eq!(resp.models[0].model_creator, Some(ApiClientModelCreator::Unknown("some_new_studio".to_string())));
let ratios = resp.models[0].aspect_ratio_options.as_ref().unwrap();
assert_eq!(ratios[0], ApiClientAspectRatio::Square);
assert_eq!(ratios[1], ApiClientAspectRatio::Unknown("some_new_ratio".to_string()));
assert_eq!(resp.models[0].is_disabled, None);
assert_eq!(resp.providers[0].provider, ApiClientGenerationProvider::Unknown("new_provider".to_string()));
assert_eq!(resp.providers[0].models[0].model, ApiClientImageModel::NanoBananaPro);
}
#[test]
fn empty_object_uses_defaults() {
let resp: OmniGenImageModelsResponse = serde_json::from_str("{}").unwrap();
assert!(!resp.success);
assert!(resp.models.is_empty());
assert!(resp.providers.is_empty());
}
}
/// Live tests that hit real servers. All `#[ignore]` so they never run by default.
/// Run explicitly, e.g.:
/// cargo test -p artcraft_client omni_gen_list_image_models::live_tests -- --ignored --nocapture
#[cfg(test)]
mod live_tests {
mod localhost {
use super::super::{omni_gen_list_image_models, OmniGenListImageModelsArgs};
use crate::utils::api_host::ApiHost;
#[tokio::test]
#[ignore] // live: hits http://localhost:12345
async fn list_image_models() {
let api_host = ApiHost::Localhost { port: 12345 };
let response = omni_gen_list_image_models(OmniGenListImageModelsArgs {
api_host: &api_host,
maybe_creds: None,
provider: None,
})
.await
.expect("request should succeed");
println!("[localhost] image models response: {:#?}", response);
assert!(response.success);
assert!(!response.models.is_empty(), "expected at least one image model");
}
}
mod production {
use super::super::{omni_gen_list_image_models, OmniGenListImageModelsArgs};
use crate::utils::api_host::ApiHost;
#[tokio::test]
#[ignore] // live: hits https://api.storyteller.ai
async fn list_image_models() {
let api_host = ApiHost::Storyteller;
let response = omni_gen_list_image_models(OmniGenListImageModelsArgs {
api_host: &api_host,
maybe_creds: None,
provider: None,
})
.await
.expect("request should succeed");
println!("[production] image models response: {:#?}", response);
assert!(response.success);
assert!(!response.models.is_empty(), "expected at least one image model");
}
}
}
@@ -0,0 +1,2 @@
pub mod image;
pub mod video;

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