Compare commits

...
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 ba3b299715 Cap MySQL statement cache and connection lifetime in storyteller-web
Production hit MySQL error 1461 (max_prepared_stmt_count = 16382), causing
fast 500s on /v1/prompt/batch and other endpoints. sqlx caches up to 100
prepared statements per connection by default, so 7 replicas x 50
connections could hold ~35k statements, more than twice the server cap.

- statement_cache_capacity defaults to 25 (MYSQL_STATEMENT_CACHE_CAPACITY),
  bringing the ceiling to ~8.75k at current scale.
- max_lifetime defaults to 10 minutes (MYSQL_MAX_LIFETIME_SECONDS), down from
  sqlx's 30, so statements orphaned by cancelled queries are released sooner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:12:52 -04:00
Brandon Thomas 17373314ae remove old website (#63) 2026-10-09 11:31:28 -04:00
931a8b925c Bump Crafting App downloads to the new releases; add ARM64, Flatpak and FreeBSD (#61)
## What

Moves every `/apps/<app>` page on getartcraft.com (Next.js site) to the
app's latest GitHub release, and adds the builds every release now
ships.

| App | Site | Latest release |
|---|---|---|
| PhotoCraft | 0.3.0 → **0.5.0** | v0.5.0 |
| VectorCraft | 0.5.0 → **0.6.0** | v0.6.0 |
| EffectCraft | 0.4.0 → **0.6.0** | v0.6.0 |
| FilmCraft, LightCraft, PdfCraft, DesignCraft | 0.2.1 → **0.4.0** |
v0.4.0 |

New files in every app's list (all seven releases ship them now):
- **Windows:** Installer (ARM64), Portable (ARM64), previously only
PhotoCraft, VectorCraft and EffectCraft
- **Linux:** Flatpak (x86_64), Flatpak (ARM64)
- **Other:** FreeBSD (x86_64)

`WITH_WINDOWS_ARM64` and `CRAFTING_APPS_LATEST_VERSION` are gone: every
app ships the same file set, and versions differ per app. PdfCraft's
`fileSlug: "printcraft"` override is dropped, since 0.4.0 files are
named `pdfcraft-…`. Only the seven classic apps are on the site; the
newer apps aren't touched.

## Verification
- Every one of the 21 links per app matches a file in that app's
published (non-draft, non-prerelease) release. The only release files
not linked are the two `.AppImage.zsync` update files.
- `npm run typecheck` passes.
- `next dev`: `/apps/photocraft`, `/apps/pdfcraft` and
`/apps/effectcraft` render the new links (Windows ARM64, both Flatpaks,
FreeBSD); no stale version or `printcraft` URLs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:00:29 -04:00
BFlatandClaude Opus 5.5 0e1991de6d Bump VectorCraft downloads to 0.5.0 (#58)
## What

Moves VectorCraft's `/apps/vectorcraft` downloads from 0.4.0 to
[v0.5.0](https://github.com/storytold/vectorcraft/releases/tag/v0.5.0).
The other Crafting Apps are already on their latest releases:

| App | Site | Latest release |
|---|---|---|
| VectorCraft | 0.4.0 → **0.5.0** | v0.5.0 |
| PhotoCraft | 0.3.0 | v0.3.0 |
| EffectCraft | 0.4.0 | v0.4.0 |
| FilmCraft, LightCraft, PdfCraft, DesignCraft | 0.2.1 | v0.2.1 |

## Verification
- All 19 files the site lists, including the Windows on ARM builds, are
in v0.5.0, and each download URL resolves. The release page returns 200.
- The release also ships a FreeBSD tarball, which the site doesn't list
for any app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:18:42 +07:00
BFlatandClaude Opus 5.5 d950e57352 Rename PrintCraft to PdfCraft on the Crafting Apps pages (#57)
## What

The `storytold/printcraft` repo is now
[`storytold/pdfcraft`](https://github.com/storytold/pdfcraft), and every
Crafting Apps README already links `getartcraft.com/apps/pdfcraft`,
which didn't exist on the site until now. This renames the app on the
site.

- **Slug and name:** `/apps/pdfcraft`, shown as "PdfCraft" in the nav,
footer, `/apps` hub, sitemap, press kit and JSON-LD. Copy, category and
theme colour are unchanged; only the name changes.
- **Legacy links redirect permanently (308):**
  - `/apps/printcraft` → `/apps/pdfcraft`
- `/images/apps/printcraft/<file>` → `/images/apps/pdfcraft/<file>`, so
share images already cached by social sites keep loading.
- **Images:** moved to `public/images/apps/pdfcraft/`. The four
screenshots and the share image were rebuilt from the README's renamed
`pdfcraft-*.png` renders, which are slightly newer than the site's (they
show the in-app Discord button). The "Full size" and press-kit links had
been pointing at the old `printcraft-*.png` names, which no longer
exist; they now point at the new files.
- **Downloads:** v0.2.1 was published before the rename, so its files
are still called `printcraft-0.2.1-*`. `CraftRelease` gains an optional
`fileSlug` for this, and PdfCraft sets it to `printcraft`. Once a
release ships with `pdfcraft-*` names, removing that one line switches
it over.

## Verification
Rendered locally:
- `/apps/printcraft` and the old image URLs return 308 to their PdfCraft
equivalents. Following the redirect lands on the PdfCraft page with its
theme.
- `/apps/pdfcraft`: the title, canonical URL and `og:image` use
pdfcraft. All 17 download links resolve directly on
`storytold/pdfcraft`, without going through GitHub's redirect.
- The sitemap lists `/apps/pdfcraft`, and `/`, `/apps` and `/press-kit`
show no "PrintCraft". All press-kit links for PdfCraft return 200.
- `tsc` shows no errors in the changed files. The errors that remain are
already on `main`.

## Note
The screenshots still show "PrintCraft" inside the app (the window tab
and the sample PDF), because the PdfCraft repo renamed the image files
without re-rendering them. They'll update once the README screenshots
are re-captured.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 03:56:47 +07:00
BFlatandClaude Opus 5.5 652146ad96 Refresh VectorCraft screenshots and bump Crafting App downloads (#56)
## What

### VectorCraft screenshots
The images were regenerated from the latest renders in VectorCraft's
README
([storytold/vectorcraft@1c3b7b1](https://github.com/storytold/vectorcraft/commit/1c3b7b16)).
The site's previous hero still showed the old DrawCraft UI.

- `hero`, `ribbons`, `bezier` and `sheet` replaced, and a new
`perspective` shot added for the Perspective Grid. They follow the
README's order.
- Captions and alt text updated to match what each new shot shows.
- `og.jpg` share image rebuilt from the new hero, composed the same way
as before: a centred crop with a 144px icon in the bottom-left corner.
- Encoding matches the existing files (1600×1000 WebP q82, JPEG q85), so
the files are about the same size, 50–94 KB each.

### Gallery layout
`AppGallery` used to show the first shot after the hero at full width
and pair the rest. With an even number of shots, that left an empty cell
at the end. Now the first shot only goes full width when the count is
odd. VectorCraft's four shots form a 2×2 grid, like the README's "A look
around" table. All other apps have three shots, so their layout is
unchanged.

### Download versions
| App | Before | After |
|---|---|---|
| PhotoCraft | 0.2.0 | **0.3.0** |
| VectorCraft | 0.3.1 | **0.4.0** |
| EffectCraft | 0.3.1 | **0.4.0** |
| FilmCraft, LightCraft, PrintCraft, DesignCraft | 0.2.1 | 0.2.1 (still
latest) |

These three releases add **Windows on ARM** builds (`windows-arm64.msi`,
`windows-arm64-portable.zip`). They're now listed in the Windows card
for those apps through a `WITH_WINDOWS_ARM64` asset list. The main
Windows button stays on x64, because browsers on Windows on ARM
generally report themselves as x64.

## Verification
- Every expected asset exists in each release.
- Rendered all seven `/apps/<slug>` pages locally and requested every
GitHub download and release link. All of them resolve, and the "Version"
label shows the new versions.
- Checked the VectorCraft gallery in the browser: a 2×2 grid with all
images loading.
- `tsc` shows no errors in the changed files. The errors that remain are
already on `main` (missing local Playwright types and
`media-viewer-3d.tsx`).

## Not in this PR
- **`storytold/printcraft` has been renamed to `storytold/pdfcraft`.**
PrintCraft links still work through GitHub's redirect. Renaming the app
on the site is a separate change.
- Releases also ship FreeBSD tarballs, and PhotoCraft also ships Flatpak
and zsync files. The site doesn't list these.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 03:51:45 +07:00
dafe769cda PageScene: dispose GPU assets on webapp (#55)
Targets PageScene and other 3D viewers, handles uniforms and textures,
amongst other general editor mount improvements

---------

Co-authored-by: heojunfo <kleague3@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 17:58:27 -07:00
BFlatandClaude Opus 5.5 ad9b10ba94 Bump Crafting App downloads to their latest releases (#54)
## What

Updates the `/apps/<slug>` download links to each app's latest GitHub
release:

| App | Before | After |
|---|---|---|
| VectorCraft | 0.2.0 | **0.3.1** |
| EffectCraft | 0.2.0 | **0.3.1** |
| FilmCraft | 0.2.0 | **0.2.1** |
| LightCraft | 0.2.0 | **0.2.1** |
| PrintCraft | 0.2.0 | **0.2.1** |
| DesignCraft | 0.2.0 | **0.2.1** |
| PhotoCraft | 0.2.0 | 0.2.0 (still its latest) |

`CRAFTING_APPS_LATEST_VERSION` becomes `0.2.1`, the most common version.
PhotoCraft, VectorCraft and EffectCraft are pinned with
`craftRelease(...)`, following the file's documented convention.

## Verification

- Compared each release's asset list against `CRAFT_RELEASE_ASSETS`:
every expected file exists.
- Rendered all seven app pages locally and requested every GitHub
download/release link (34 per page): all return 200/302.
- The "Version" label and JSON-LD `softwareVersion` show the new
versions.

## Note

PrintCraft 0.2.1 also ships `printcraft-0.2.1-freebsd-x86_64.tar.gz`,
which the site doesn't list. That's left for a follow-up if we want to
offer it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:38:45 +07:00
BFlatandClaude Opus 5.5 3880127f68 Add Google Analytics to the marketing site (#50)
## What

Adds GA4 to `artcraft-website-next`, covering the whole marketing site.

- `src/components/analytics.tsx` loads gtag.js (`afterInteractive`) and
installs one delegated click listener. It's mounted once in the root
layout.
- `src/lib/analytics.ts` holds the typed event catalogue,
`trackEvent()`, `trackAttrs()` (data attributes that work in server
components) and the link classifier.
- Analytics is **off unless `NEXT_PUBLIC_GA_MEASUREMENT_ID` is set**, so
local dev and deploy previews stay out of the reports.

## Why

The new site had no analytics. This gives us download, signup and
checkout funnels, plus CTA performance by page section.

## Events

GA4 Enhanced Measurement handles page views (App Router history
changes), scrolls, outbound clicks and common file downloads. On top of
that:

| Event | Trigger |
|---|---|
| `app_download` | GitHub release asset clicks (ArtCraft + Crafting
Apps): app, version, platform, file |
| `webapp_click` | Links to app.getartcraft.com: destination path |
| `cta_click` | Other `Button` links (tagged `data-cta`) |
| `app_select` | App cards and icons on `/apps` and sibling cards on app
pages: app name |
| `social_click`, `contact_click` | `SOCIAL_LINKS` hosts, mailto / copy
email |
| `begin_checkout` | Plan selection (`signup`/`subscribe`/`switch`) and
credit packs, with GA4 ecommerce `items` |
| `billing_cadence_change`, `view_credit_packs`, `manage_subscription` |
Pricing actions |
| `prompt_submit`, `video_generation` | H3 promptbox (`logged_in`;
complete/failed/timeout) |
| `sign_up`, `login` | Auth gate success |
| `generate_lead` | Beta form submission (honeypot hits excluded) |
| `video_start`, `share`, `copy_command`, `copy_prompt`,
`media_download`, `press_kit_download` | Video facades, share bars, copy
buttons, media/press downloads |

Every delegated click event also carries `link_location`: nav, footer,
modal, or the enclosing `section[id]`.

## Reviewer notes

- `Button` now renders `data-cta=""` on its anchor variants. There is no
visual change.
- `PricingTable.choosePlan` now branches on the computed `checkoutType`
rather than repeating the `user`/`hasActivePlan` conditions. The
behaviour is unchanged.
- `AuthGateModal` forms now return early on error instead of using
if/else, so the success event goes on the success path.

## Deploy

Set `NEXT_PUBLIC_GA_MEASUREMENT_ID` in Netlify, **Production context
only**, then redeploy. The ID is inlined at build time. This is
documented in `netlify.toml`.

After deploying:
- Mark key events in GA: `app_download`, `begin_checkout`, `sign_up`,
`generate_lead`, `webapp_click`.
- Register custom dimensions (e.g. `link_location`, `app_name`,
`platform`, `destination`, `checkout_type`) so the params show in
reports.

## Testing

Ran the dev server with a dummy measurement ID and checked `dataLayer`
on these pages:
- `/`: nav, footer, CTA, social, mailto and YouTube events.
- `/download`: Windows and macOS installers.
- `/apps/photocraft`: Linux downloads, share, copy command, GitHub link,
and `app_select` on the sibling cards (also checked on the `/apps` icon
row and lineup cards).
- `/pricing`: cadence toggle.
- `/minimax-h3`: logged-out `prompt_submit` and example `video_start`.

Not exercised in the browser: the beta form (submitting would post a
real Google Form entry), signup/login, checkout, the credits modal, the
media page, the press kit, email copy and manage plan.

`tsc` shows no new errors. Existing errors in `media-viewer-3d.tsx` and
the Playwright test files are unchanged. The production build was not
run.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:00:50 +07:00
Brandon Thomas 03dea1c660 app downloads 2 (#49) 2026-10-05 06:38:56 -04:00
Brandon Thomas b5272188f7 App downloads (#48) 2026-10-05 06:17:51 -04:00
bflatastic b02f710c76 page scroll fix 2026-10-04 21:15:58 +07:00
BFlat 708de238c5 Add VectorCraft, EffectCraft, DesignCraft and app icons to Craft Apps pages (#47)
## Summary

Updates the Craft Apps pages to cover the org's seven Craft repos and
adds each app's icon.

- **VectorCraft replaces DrawCraft.** The repo was renamed, so the page
moves to `/apps/vectorcraft`. `/apps/drawcraft` permanently redirects
there, because every Craft README's app table still links the old URL.
- **New pages:** `/apps/effectcraft` (motion graphics & VFX) and
`/apps/designcraft` (page layout & publishing), each with copy,
screenshots and a share image.
- **App icons:**
  - A row in the `/apps` header linking to every app.
- On each lineup card and sibling-app card, overlapping the bottom of
the screenshot.
  - Above the wordmark in each app's hero.
- **PhotoCraft installers (`v0.1.1-rc.4`):**
  - A download button in the hero.
- macOS, Windows and Linux buttons, plus an "All builds and checksums"
link.
  - An "Installers ready" badge on its card.
  - `softwareVersion` and `downloadUrl` in its JSON-LD.
- **Share images:** each app's image has its icon in the corner, and the
`/apps` share image is the seven icons in a row.
- **Press kit:** a new "Crafting Apps icons" section, linking to each
repo's 1024px PNG.

## Details
- **Data file:** everything is still driven by
`src/lib/crafting-apps.ts`. An app's `release` field (version plus a
filename suffix per platform) produces its download buttons, so other
apps need one data entry when they ship.
- **Icons:** each repo's 512px render, converted to a 256px WebP with
transparency, about 25 KB each.
- **DesignCraft gallery:** its magazine pages are portrait, so they show
four across instead of in the 16:10 layout.
- **Hub copy:** the app count is worked out from the data ("Seven
apps"). The lineup is four columns wide on large screens, so seven cards
plus the Discord cell fill two even rows.
- **Colors:** EffectCraft and DesignCraft get their icon tile colors as
accents, with AA-contrast text colors in light and dark.
- **Adobe names:** the copy still never names Adobe products.

## Testing
- A clean `npm ci && npm run build` (what Netlify runs) passes with a
clean TypeScript check, and all seven app pages are generated as static
HTML.
- `next start` serves every page and asset with a 200, `/apps/drawcraft`
returns a 308 to `/apps/vectorcraft`, and an unknown slug returns 404.
- Checked visually at 1440px and at a 390px mobile viewport: no broken
images or horizontal overflow, and the mobile icon row wraps four over
three.
- The PhotoCraft download links and every 1024px icon URL in the press
kit resolve.
2026-10-02 23:06:56 +07:00
Brandon Thomas fc78787f30 better website hero text (#46) 2026-10-01 20:22:51 -04:00
BFlat e92253c5b4 Add Craft Apps marketing pages to the website (#45)
## Summary

Marketing pages on getartcraft.com for the five Craft Apps: PhotoCraft,
DrawCraft, FilmCraft, LightCraft and PrintCraft. Everything is driven by
one data file, `src/lib/crafting-apps.ts`.

- **`/apps` hub:** lineup cards for all five apps, shared principles,
and Discord calls to action.
- **`/apps/<slug>`:**
  - Hero screenshot with Discord, GitHub and share links.
  - Six highlights and a screenshot gallery.
- "Get it": a Discord waitlist for installers, next to build-from-source
steps with a copy button.
  - Cards linking to the sibling apps.
- **Nav:** a "Craft Apps" link after a divider following Support, and a
Discord button on desktop.
- **Footer:** a "Craft Apps" column.
- **Sitemap:** entries for the hub and all five app pages.
- **Press kit:** all 20 app screenshots, with full-resolution downloads.

## Branding
- **Colors:** each app has its own accent (`.app-*` in `globals.css`),
AA-contrast in light and dark.
- **Wordmarks:** text only, with "Craft" set in the app's color. These
pages don't use the serif accent font.
- **Share previews:** a 1200×630 image per app plus one for the hub, and
`SoftwareApplication` JSON-LD on each app page.
- **Adobe:** the copy never names Adobe products.

## Notes
- No installers exist yet in any of the five repos, so each page sends
visitors to Discord and to building from source. When an app ships a
release, adding `downloads` to its entry in `crafting-apps.ts` replaces
that block with download buttons.
- Screenshots come from each repo's `docs/images`, show only
public-domain artwork, and are converted to WebP at 1600px.
- **Small refactors:**
- New shared `DiscordButton`, now also used by the community strip and
the press kit.
  - New `CopyButton` in the UI kit.
  - `SITE_URL`/`siteUrl` moved into `lib/links.ts`.
  - New `kicker`/`mediaTag` props on `CampaignHero`.

## Testing
- A clean `npm ci && npm run build`, the same build Netlify runs,
passes: TypeScript is clean, `/apps` is static, and all five app pages
prerender as static HTML.
- `next start` serves all six routes plus their images, and an unknown
slug returns 404.
- Each page's Open Graph and Twitter tags render with the per-app image.
- Checked visually at 1440px and 1024px (the nav fits) and at a 390px
mobile viewport.
2026-10-01 22:56:48 +07:00
Brandon Thomas 847edc81e5 Merge pull request #44 from storytold/bt-no-more-gif-thumbnails
webp instead of gif thumbnails
2026-09-28 01:15:13 -04:00
Brandon Thomas 2e596cc4b3 ffmpeg fallback on mac 2026-09-28 01:07:45 -04:00
Brandon Thomas c3d72fb321 Generate WebP thumbnails for videos instead of Gif (except: MacOS 10.15)
MacOS Catalina 10.15 released October 7, 2019 can't do gifs in
safari/webkit.
2026-09-28 00:52:07 -04:00
Brandon Thomas cd27599c0a Merge pull request #43 from storytold/bt-additional-cors-tests
additional cors tests to prevent regressions
2026-09-27 23:43:50 -04:00
Brandon Thomas 46fd907182 additional cors tests to prevent regressions 2026-09-27 23:43:06 -04:00
Brandon Thomas 5bdc19f573 Merge pull request #42 from storytold/bt-domain-config-2
tests
2026-09-27 22:57:15 -04:00
Brandon Thomas 6b34f23b43 tests 2026-09-27 22:55:36 -04:00
Brandon Thomas ddf151794e Merge pull request #41 from storytold/bt-domain-config
domain config
2026-09-27 22:46:20 -04:00
Brandon Thomas 7a88118349 fold cdn configs in 2026-09-27 22:41:01 -04:00
Brandon Thomas e07bf35cd0 static config for first party domains 2026-09-27 22:40:14 -04:00
Brandon Thomas a1147a98d2 Merge pull request #40 from storytold/bt-desktop-site
add desktop marketing website
2026-09-27 22:12:11 -04:00
Brandon Thomas f67fb7db31 add desktop marketing website 2026-09-27 22:10:27 -04:00
Brandon Thomas 7d4e89ced9 Merge pull request #39 from storytold/bt-query-perf-2
fixes to new endpoint
2026-09-27 22:01:25 -04:00
Brandon Thomas b9ee1bd225 fixes to new endpoint 2026-09-27 22:01:01 -04:00
Brandon Thomas 68886b62fe Merge pull request #37 from storytold/bt-query-perf
more performant list user files endpoint
2026-09-27 20:22:51 -04:00
Brandon Thomas 293a41c7d4 let's actually not call the new endpoint prematurely 2026-09-27 17:35:00 -04:00
Brandon Thomas 35a859529f Merge pull request #38 from storytold/bt-tauri-cors
cors for tauri localhost + tauri desktop
2026-09-27 17:28:25 -04:00
Brandon Thomas 3a93e358cd tests 2026-09-27 17:27:40 -04:00
Brandon Thomas 1cb0b32898 move desktop configs 2026-09-27 17:25:20 -04:00
Brandon Thomas c50bb78c32 tauri cors for new domain 2026-09-27 17:12:27 -04:00
Brandon Thomas 55ab800542 more performant list user files endpoint 2026-09-27 15:02:19 -04:00
Brandon Thomas 2c93b4cf42 Merge pull request #36 from storytold/bt-opengraph-metadata
add opengraph metadata (social media tags)
2026-09-26 16:32:39 -07:00
Brandon Thomas 9169e4d662 add opengraph metadata (social media tags) 2026-09-26 16:30:57 -07:00
Brandon Thomas 4c7af51851 Merge pull request #35 from storytold/bt-media-previews
fix media previews in new design
2026-09-26 10:30:44 -07:00
Brandon Thomas eadfb4f821 Merge origin/main into bt-media-previews
Combine the media share implementations, preserving interactive players, social cards, model/provider labels, reference thumbnails, and legacy redirects. Verify with 17 isolated Chrome integration tests, TypeScript checks, and the production build.
2026-09-26 09:58:16 -07:00
Brandon Thomas 0c163d6c19 routes 2026-09-26 09:33:04 -07:00
Brandon Thomas 26484685ee media previews on website 2026-09-26 09:25:40 -07:00
BFlat 5bceb33801 Website: model/provider names and icons, video reference thumbs on share page (#34)
Ports the model-list display names and brand marks, shows the Created
row as date + time like the webapp lightbox, and uses the still-frame
preview for video references.
2026-09-26 21:49:03 +07:00
BFlat c6e2c4296a Website: port media share page (/media/[token]) to artcraft-website-next (#33)
* Website: port the media share page to artcraft-website-next

Adds the /media/[token] route with server-side OG metadata, the media
viewer with prompt/reference/info details, and legacy ?media= redirects.

* no ruler on media share page
2026-09-26 21:36:04 +07:00
Brandon Thomas 8551d58def fix download version again 2026-09-26 04:32:59 -07:00
Brandon Thomas 807180bf22 Merge pull request #32 from storytold/bt-new-website-downloads
new website downloads
2026-09-26 04:24:45 -07:00
Brandon Thomas f48d2beea7 new website downloads 2026-09-26 04:24:15 -07:00
Brandon Thomas dc72bbd5e6 Merge pull request #31 from storytold/bt-sso-otp
desktop app login bridge
2026-09-26 02:43:00 -07:00
Brandon Thomas b6a184bcb0 enum variants 2026-09-26 02:36:24 -07:00
Brandon Thomas d0e1d113fe break out queries 2026-09-26 02:13:23 -07:00
Brandon Thomas 1facb312dc tests 2026-09-26 01:57:16 -07:00
Brandon Thomas f2c8a0390f final fixes 2026-09-26 00:46:19 -07:00
Brandon Thomas 2e83d39d2b login challenge on website works 2026-09-26 00:22:45 -07:00
Brandon Thomas 4f1c9134ee WIP SSO OTP bridge 2026-09-25 23:23:00 -07:00
Brandon Thomas f0a877c988 Merge pull request #30 from storytold/bt-fix-thumbs
thumbnail generation speedup / concurrency
2026-09-20 10:45:32 -04:00
Brandon Thomas 1b2d2f49e5 improve concurrency 2026-09-20 10:44:54 -04:00
Brandon Thomas 3e6d759fe2 fix thumbnail job latency 2026-09-20 10:12:38 -04:00
BFlat f77f09018a Landing: wordmark thickness update (#29) 2026-09-20 20:31:18 +07:00
Brandon Thomas fc9d727ffc Merge pull request #28 from storytold/bt-routing
re-route request
2026-09-20 08:21:44 -04:00
Brandon Thomas e2da58b3e5 re-route request 2026-09-20 08:21:08 -04:00
BFlat 681f32b230 Add folder moving and nested sidebar navigation (#27) 2026-09-19 13:11:02 +07:00
Brandon Thomas edfa80dde3 Merge pull request #26 from storytold/bt-debug
debug tool
2026-09-18 18:55:00 -04:00
Brandon Thomas cfa8264642 debug tool 2026-09-18 18:54:10 -04:00
Brandon Thomas fcb567ec3c Merge pull request #25 from storytold/gpt-image-qualities
gpt image 2.5: new qualities
2026-09-18 14:13:57 -04:00
Brandon Thomas 734b757ef9 gpt image 2.5: new qualities 2026-09-18 14:12:34 -04:00
Brandon Thomas daf401c635 Merge pull request #24 from storytold/bt-regroup
group Wan video differently
2026-09-18 14:00:04 -04:00
Brandon Thomas c0207c61ba group Wan video differently 2026-09-18 13:59:01 -04:00
Brandon Thomas 0a7b7ae2ac Merge pull request #23 from storytold/bt-wan-pt2
wan 3.0 + prime
2026-09-18 13:45:58 -04:00
Brandon Thomas 92bc1e24c4 wan 3.0 + prime 2026-09-18 13:40:50 -04:00
Brandon Thomas 6dd7a88a12 Merge pull request #22 from storytold/bt-wan
wan 3.0
2026-09-18 12:41:18 -04:00
Brandon Thomas d5a2b36525 promo pricing 2026-09-18 12:09:19 -04:00
Brandon Thomas e3c553ff2c wan 3.0 client bindings 2026-09-18 12:00:58 -04:00
Brandon Thomas 094d33e405 Merge pull request #21 from storytold/bt-batch-counts
standardize batch counts
2026-09-18 09:46:32 -04:00
Brandon Thomas 6a35fccba3 standardize / cleanup video batch sizes 2026-09-18 09:45:53 -04:00
Brandon Thomas db925450c5 video batching 2026-09-18 09:36:01 -04:00
Brandon Thomas 90f1fcb25f prices 2026-09-18 09:17:00 -04:00
Brandon Thomas 0245058e05 tests 2026-09-18 09:12:50 -04:00
Brandon Thomas 5d311b7a20 increase batch counts 2026-09-18 09:07:06 -04:00
Brandon Thomas 4836854263 Merge pull request #20 from storytold/bt-rate-increase
seedance rates
2026-09-17 12:12:07 -04:00
Brandon Thomas 2a43d929ec update margins 2026-09-17 12:10:31 -04:00
Brandon Thomas 437bbadd0f seedance 1080p price is going up 2026-09-17 11:35:24 -04:00
Brandon Thomas d181ce5089 Merge pull request #19 from storytold/bt-fix-sound
don't turn off audio for seedance
2026-09-16 17:29:27 -04:00
Brandon Thomas dab29e1cc0 don't turn off audio for seedance 2026-09-16 17:27:24 -04:00
BFlat 3e6a02c29c Fix toggle sizing, soften hover states, and simplify selection marks (#18) 2026-09-16 23:49:13 +07:00
BFlat 16b87f3e07 Align editor, auth, create, and pricing UI with ArtCraft styling (#17) 2026-09-16 23:12:20 +07:00
Brandon Thomas fa56bb1c9c Merge pull request #16 from storytold/bt-fix-frontend
fix frontend for production
2026-09-16 10:37:41 -04:00
Brandon Thomas c7751251ac fix frontend for production 2026-09-16 10:36:52 -04:00
BFlat d2fc91f1f0 Pricing page fix webapp 2026-09-16 20:38:08 +07:00
BFlat 0379aa95dd Align webapp pricing page with the ArtCraft website design (#15)
* Align webapp pricing page with website design

* Clarify pricing headline to avoid implying unlimited usage

* Simplify pricing headline to Pick a plan
2026-09-16 20:30:29 +07:00
BFlat 6eb4f7707a Fix generation credit cost visibility on mobile (#14)
* Fix generation credit cost visibility on mobile

* Polish webapp styling, navigation, and media dialogs
2026-09-16 20:05:07 +07:00
BFlat 3ee521b2a5 Fix generation credit cost visibility on mobile (#13) 2026-09-16 18:00:37 +07:00
bflatastic 86367b6e33 hide demo section temporarily 2026-09-16 10:17:46 +07:00
Tanish Manku 730cf247f8 Merge pull request #12 from storytold/design/premium-website
Site redesign: Content tweaks and mobile improvements
2026-09-15 20:08:12 -07:00
Tanish Manku bbd9595352 Merge remote-tracking branch 'services/design/premium-website' into design/idea-hero-spiral 2026-09-15 19:46:00 -07:00
Tanish Manku 0386e2fd9f sections: seat half ticks on the hairline, lift above the frost
-top-[5px] centers the crossbar on the section's top rule (was 1px
high), and z-[31] lifts the marks over the frosted eyebrow band
(z-30), which was swallowing the right tick's downward stem.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
2026-09-15 19:44:20 -07:00
Tanish Manku 2a8a61697f sections: draw-up goes fully one-shot; top-edge ticks lose their upper arm
The whole frame (rule, eyebrow, ticks) now assembles once on first
entry and stays built — the scrub-linked reversal read as
inconsistent against the one-shot content. And the section-top
crosses become half ticks (.tick-half: crossbar + downward stem):
the full plus poked its upper arm above the boundary into the
frosted sticky eyebrow band.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
2026-09-15 19:42:48 -07:00
Tanish Manku 8c732ab38e sections: eyebrow reveal is one-shot — reversibility stays with the silent geometry
The eyebrow carries readable text and rides sticky with the reader,
so it must never fade back out under them. It now reveals once (same
tween as the cells) while the rule and ticks keep the scrubbed,
reversible draw. Verified: scroll back un-draws the rule to zero with
the eyebrow holding at full opacity.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
2026-09-15 19:39:35 -07:00
Tanish Manku 2912c8b608 sections: eyebrow top hairline restored; content reveals go per-cell
The sticky eyebrow band now owns BOTH hairlines (border-y): its top
edge was the section's drawn rule, which the frosted background
painted over at rest and which vanished entirely once the band stuck
mid-section. The band also fades as one unit with the draw
choreography — frost, rules, and contents together — coinciding with
the drawn rule at rest so they read as a single line.

Group content reveals move from one group-level trigger to per-CHILD
ScrollTriggers: the old single trigger fired when a tall grid's top
entered, revealing every cell below the fold unseen — so scrolled-to
content sat static while only the frames animated. Each cell now
assembles as it arrives, cells sharing a row cascading left to right
(delay derived from horizontal position), consistent with the drawn
frames at every scroll depth.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
2026-09-15 19:35:26 -07:00
Tanish Manku 5ec34e89ca sections: scrub-drawn frames, frosted sticky eyebrows, studio demo leads Features
Premium transition-in for the scrolled page, in the instrument voice:

- Structural draw-up, scrub-linked and reversible: as a section enters
  the viewport its top rule DRAWS across, the eyebrow's mono contents
  settle in, and the corner ticks pop — all tied to scroll position
  (scrub 0.35), so pulling back down un-draws the frame. Content
  reveals stay one-shot: text never wiggles under the reader. Rules
  render complete in server HTML; only motion-capable visitors get
  the choreography.

- Section eyebrows ("01 / Crafting features") are sticky while their
  section scrolls — frosted (backdrop blur over translucent page
  background) like the ruler rail, pinned under the navbar, pushed
  away by the next section rather than stacking.

- The studio demo moves out of the hero into Features as the lead
  cell: the announce strip becomes its header row and the
  blocking↔render comparator its 21:9 body (cursor hint as an
  invert chip over the footage, md+ only). The hero ends at the
  poster. Image to Location returns to the grid, which now splits
  evenly: seven feature cells plus a full-sized eighth cell — the
  launch CTA, a clickable door to the webapp with an arrow chip
  and the same header/body/copy anatomy as its siblings.

Verified in Firefox: rule at 52% mid-viewport and back to 0 on
scroll-back (reversible), eyebrow pinned at 48px deep into the
section, zero page errors.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
2026-09-15 19:23:49 -07:00
Tanish Manku 645e971b99 mobile: ruler off below the capability boundary; own spiral dial group
The scroll ruler is now truly absent on small screens — the
capability gate went live (media-query listeners instead of a
one-shot mount check) and the component renders nothing when
incapable: no rail element, no data-ruler stamp, native scrollbar,
no hero heading choreography. Resizing across the boundary
mounts/unmounts the instrument in place: a late mount comes up
settled instantly (an intro cascade firing on a window resize reads
as a glitch), and unmounting releases every inline style HeadingFlow
wrote on the hero wordmark letters so the masthead stands naturally.

The spiral's mobile profile graduates to its own tuner group (Galaxy
mobile): below the area threshold its dials replace the Galaxy-layout
values wholesale — arms, turns, card count (fixed, not scaled from
the desktop knob), outer radius, card height cap, birth radius,
density — so small screens get an independently tuned composition.

Verified in Firefox: phone shows no rail/attr and an untouched
wordmark; growing to 1500px mounts the settled instrument (ticks up,
HeadingFlow at identity rest pose); shrinking unmounts and restores.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
2026-09-15 19:06:22 -07:00
Tanish Manku 1764e4b590 galaxy: pulses ride the intro reveal wave; bake tuned defaults
The pulse comets are now choreographed into the intro: their
visibility is gated by the same trailing radial wave that uncovers
the cards (uWaveR/uWaveBand uniforms fed from the rollout clock), so
they appear rolling outward with the field instead of materializing
across the whole spiral at the instrument beat. Post-intro the wave
radius sits past every arm; the tuner replay rewinds it for free.

Baked the user's tuned values as defaults: cardN back to 64 (the
0.76 arm desync scatters it cleanly in slot units), guide lines 1×,
line 2px, pulses 0.72 alpha / 0.19 speed / 0.05 length / 3 per arm /
0.4 hue spread / 0.62 light-mode gray.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
2026-09-15 18:54:07 -07:00
Tanish Manku 75d8004d1b galaxy: light-mode pulses go colorless — dark gray + thickness only
Dark theme keeps the hue-spread accent comets. Light theme now drops
color entirely: pulses render as plain dark gray (ink mixed over the
paper by the Pulse gray knob, default 0.75) and read through the
ribbon thickness alone — colored trails fought the paper background
no matter how dark they sank. The hue spread is zeroed through the
same cross-fade that drives the color, so a theme toggle morphs
between the two treatments instead of snapping.

Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
2026-09-15 18:43:00 -07:00
Brandon Thomas f653987369 Merge pull request #11 from storytold/bt-quicktime-audio
quicktime audio
2026-09-15 19:41:39 -04:00
Brandon Thomas 6d1ede0b75 feature flags for uploads 2026-09-15 19:37:54 -04:00
Brandon Thomas 70eec108fa gate quicktime uploads 2026-09-15 19:11:41 -04:00
Brandon Thomas e977743912 don't change 2026-09-15 18:49:59 -04:00
Brandon Thomas 7dd5331232 hide behind feature flag 2026-09-15 18:48:36 -04:00
Brandon Thomas d368abe867 advertise mov feature flag, frontend UX 2026-09-15 16:22:19 -04:00
Tanish Manku 28762f51c5 galaxy: pulse ribbons — light-mode ink, thickness, comets, denser guides
Pulse feedback round. The arm guides move from GL lines (stuck at
1px) to thin ribbon meshes — each sample extrudes two vertices along
the curve normal and the vertex shader sets the width — so the
colored part can swell above the resting hairline (Pulse thick px,
default 3.5; base Line px 1.2).

Light theme sinks the pulse color toward ink (Pulse dark, default
0.65): bright accents washed out on the paper background; near-black
trails carry the needed contrast. Dark theme keeps the luminous
accent. The mix tracks background luminance, so theme cross-fades
morph it rather than snapping.

Trails are now comets — asymmetric falloff (sharp head, longer tail
behind the direction of travel), shorter (Pulse length 0.05 → 0.035)
and faster than the conveyor (Pulse speed 0.12 → 0.22).

Underlay density: Guide lines × (default 2) interleaves phantom guide
curves between the card arms — pulses ride those too. The mobile
profile pins 1× (its whole point is fewer, cheaper elements).

Claude-Session: https://claude.ai/code/session_013vwLpYJbWEqTJg9PGV6B6H
2026-09-15 12:23:56 -07:00
Tanish Manku 32d4a741f9 galaxy: mobile spiral profile, proximity clip dedup, arm color pulses
Three queued improvements, all tunable:

Mobile profile (Galaxy layout: Mobile < Mpx / arms / turns / density ×).
Below the area threshold the spiral switches to fewer, straighter arms
— at the tuned desktop angle, consecutive cards along an arm sit
radially close, so the neighbor-gap collision bound forced them small
on phones. Straighter arms space the ladder out and a density
multiplier lets cards claim the larger gaps.

Proximity dedup (Galaxy layout: Dedup px). Rebirth's least-recently-
shown pick spreads clip repeats over time, but cross-arm neighbors at
similar radii keep a constant angular offset — the same clip could
ride side by side indefinitely. A sweep on the cull cadence catches
visible pairs sharing a clip within the radius and reassigns the
blurrier card (the swap hides inside its birth blur) to the clip whose
current users sit furthest away, recency as tiebreak.

Arm color pulses (Galaxy look: Pulse alpha/speed/width/count/hue).
The arm guide lines move to a ShaderMaterial carrying arc-length and
arm-index attributes: a gaussian band of the theme accent travels
outward along each arm (with the cards' direction), hue-rotated per
arm for the colorful spread and dephased by the golden conjugate so
arms never flash in lockstep. Base hairline color/alpha and the
intro draw-up match the old line material; pulses fade out over the
blurred birth zone and lerp with theme cross-fades.

Also ported onto this branch: the pre-flight WebGL probe (cherry-pick)
and the 35-clip showcase pool.

Claude-Session: https://claude.ai/code/session_013vwLpYJbWEqTJg9PGV6B6H
2026-09-15 12:08:12 -07:00
Tanish Manku 2f4221da56 hero: pre-flight WebGL probe — context failure degrades silently
When WebGL context creation fails (exhausted drivers, blocklisted
GPU, webgl.disabled), THREE.WebGLRenderer throws inside r3f's async
init — an unhandled REJECTION that the render-phase CanvasBoundary
never catches, surfacing as a page error instead of the designed
static fallback.

Both r3f canvas gates (galaxy, comparator blocking scene) now consult
a shared cached probe (src/lib/webgl-support.ts) before mounting the
<Canvas>: no context available → the canvas never mounts, the page's
static layers stand, and the slate chrome already hides itself via
the dead-instrument rule. The probe releases its test context through
WEBGL_lose_context so it never eats a context slot.

Verified with Playwright Firefox: webgl.disabled=true → zero
canvases, zero page errors, copy/ruler fully functional; normal run →
both canvases mount, zero errors.

Claude-Session: https://claude.ai/code/session_013vwLpYJbWEqTJg9PGV6B6H
2026-09-15 12:03:05 -07:00
BFlat 60d8c0fd5f Merge pull request #10 from storytold/design/other-pages
Port marketing + campaign pages, pricing API flows
2026-09-15 09:20:18 +07:00
bflatastic 637f777135 website-next: port the campaign landing pages
Ports /seedance-2, /seedance2-5, /minimax-h3, and /creators/jboogxcreative
from the Vite site into the instrument design language, reusing the
landing's own Features / Ownership / Made-with sections (now taking an
`index` prop so eyebrow numbers stay sequential) plus a small set of
campaign building blocks (components/campaign): centered hero with reel
figure, section heading block, prose + flagged note, numbered tip grid,
native <details> FAQ, five-reasons cell grid with the "every model"
marquee, Discord/GitHub strip, closing CTA, and a shared CTA pair that
routes subscribers into the webapp and everyone else to pricing, with the
OS-detected desktop download and post-download signup modal.

- Seedance 2.5: overview with rumor disclaimer, prompt tips, FAQ (FAQPage
  JSON-LD), scroll-scrubbed manifesto word reveal (the Vite page's Three.js
  walking character and scrubbed knight video were not carried over), then
  the reused feature/ownership/reasons/community sections.
- MiniMax H3: real promptbox in the hero. Enqueues an H3 generation via
  /v1/omni_gen/generate/video, polls /v1/jobs/job/{token}, and plays the
  result inline; logged-out visitors get an inline email/password signup
  or login gate and the typed prompt runs on success. Aspect ratio,
  resolution, duration, and sound controls; keyframe uploads are left to
  the full app. Google SSO is not offered (no OAuth provider in this app).
- Creator spotlight: reel triptych, credits marquee, feed grid, craft
  cards, ProfilePage JSON-LD.

Shared: lib/use-platform.ts (OS detection + download-initiated flag) and
components/download/download-modal.tsx extracted from the download page;
api.ts gains login, signup, generateVideo, getVideoJob; ruler roster
reordered so campaign sections precede the reused landing ids; sitemap
lists the four routes.
2026-09-15 09:14:44 +07:00
bflatastic a1b4c4f362 website-next: port the pricing API flows (session, Stripe checkout, credits)
Restores the Vite pricing table's live behaviour on the Next site without
pulling the Nx @storyteller/api lib into this standalone app:

- lib/api.ts: minimal browser client for the endpoints pricing needs
  (session, active subscriptions, signup checkout, subscription checkout,
  switch plan, manage-plan portal, credits pack checkout, web referral
  log). Same paths, bodies, and envelopes as the shared lib; cookies plus
  the signed-session header fallback. Host via NEXT_PUBLIC_API_HOST.
- lib/referral.ts: landing-context capture (?u=, ?r=, landing URL,
  referrer) to apex-domain cookies + localStorage, mounted site-wide by
  components/landing-context.tsx, which also logs the referral once per
  browser session (the old main.tsx bootstrap).
- lib/use-account.ts: one shared session + ArtCraft plan fetch per page
  load for every consumer.
- Plan grid: logged out -> Stripe signup checkout with attribution; logged
  in without a plan -> subscription checkout; with a plan -> portal plan
  switch. "Your plan" tab and disabled CTA on the current tier, "Switch
  to X" labels, Manage plan portal link, inline error line.
- Credits cell: logged-in visitors get the one-time credit pack modal
  (Stripe checkout); logged-out visitors are sent to the app to sign in.

Server HTML still renders the logged-out state, so static output, SEO,
and every other page are unchanged.
2026-09-15 09:00:55 +07:00
bflatastic b6b24eef6c website-next: restore original pricing lede with a new headline 2026-09-15 08:50:06 +07:00
bflatastic 37dcab4db5 website-next: show the tagline on every pricing plan 2026-09-15 08:49:25 +07:00
bflatastic b9a769fdc4 website-next: sharper pricing hero headline and lede 2026-09-15 08:48:39 +07:00
bflatastic 2b127b6230 website-next: ruler treats interior page titles as current from load
The page-top section on interior pages (Support, Tutorials, News, …) was
running the ordinary anchor-space flip, whose progress at scroll 0 is
(12% of vh + 200px − ~110px) / 200px — so on shorter viewports the 01
heading sat partially flipped, or hidden, instead of leading the stack.
The first non-hero section is now pinned like the hero: flipP = detachP =
1 from the first frame, no queue home, no rail ride. Every page now opens
with its 01 section stacked at the top and 02+ queued below.
2026-09-15 08:48:18 +07:00
bflatastic 73ed977811 website-next: ruler sections from page top, desktop-only ruler, pricing tweaks
- Ruler: every interior page header now carries a section id (pricing,
  download, tutorials, news, faq, support, press-kit, beta) registered in
  RULER_SECTIONS ahead of the interior ids, so the heading stack starts
  with the page's 01 section at the top instead of first appearing
  mid-page. The pricing section wraps the promo strip so it begins at the
  very first pixel. Roster order is documented as load-bearing (anchors
  must increase with index).
- Ruler: capability gate tightened to hover-capable fine pointers at
  1024px+ and never on a mobile/tablet user agent.
- Pricing: dropped the "See plans" jump button; the "20% off · ends soon"
  badge moved into the plan toolbar next to the cadence toggle.
2026-09-15 08:45:52 +07:00
bflatastic e81f0bb94b website-next: color-coded, conversion-focused pricing page
Reworks /pricing on top of the ported instrument layout so the plan table
pops the way the original did:

- Per-plan color identity restored (green Basic, purple Pro, orange Max,
  blue Enterprise) via `.plan-*` tokens with light/dark ink variants: solid
  color index tab, color wash pooling under it, plan-colored checks and
  CTAs, and a 2px frame on the Most popular / Best value tiers.
- Conversion elements: full-width accent promo strip above the header,
  benefit-led headline ("Every model. One studio. Yours forever.") with a
  "See plans" jump CTA, 20% off pill and crossed-out anchor on every card,
  per-plan yearly savings in dollars, taglines, "2 months free" nudge on
  the cadence toggle, arrow CTAs, and a trust strip (cancel anytime, Stripe
  checkout, credit refunds, open source) under the grid.
2026-09-15 08:39:17 +07:00
bflatastic c3050a33f2 website-next: port marketing pages from the Vite site
Ports the interior marketing pages into artcraft-website-next, rebuilt in
the landing's instrument language (SectionShell rails + ticks, mono HUD
eyebrows, hairline gap-px cell grids, Archivo display headings with one
serif-italic contrast word, theme tokens for light/dark) instead of the old
dark-only Truchet/glow treatment.

Pages
- /pricing: plan grid (Basic/Pro/Max + Enterprise) with yearly/monthly
  TabSelector, 20% promo anchor pricing, highlighted plans invert their
  index row, credit-pack and footnote cells. ?ref=sd2fakeyou keeps the
  Seedance early-access header + reel. Checkout hands off to the webapp.
- /faq and /faq/[slug]: guide grid + article, FAQPage/Article JSON-LD.
- /news and /news/[slug]: post list + article, blog images resolved
  against the media host.
- /tutorials and /tutorials/[slug]: category filter grid, click-to-load
  YouTube via LiteYouTube, frontmatter aliases 308 to the canonical slug.
- /support, /download (client-side OS detection, requirements, quick
  start, post-download signup modal), /press-kit (asset cells + video
  modal), /beta (Google Form signup with honeypot).

Platform (Vite -> Next)
- Markdown content copied into src/content and read from disk in server
  code (lib/content.ts) with generateStaticParams, replacing
  import.meta.glob; markdown renderer ported to lib/markdown.ts and styled
  by token-based .article-content rules in globals.css.
- react-router Links/useParams/useSearchParams -> next/link, awaited
  params/searchParams, generateMetadata; react-device-detect -> a
  post-hydration user-agent hook; @storyteller/* lib imports replaced by
  local data modules (pricing-data, download-links, press-kit-data).
- Removed the legacy-route redirects to the Vite site from next.config
  (only the old Stripe return paths still redirect, now to the webapp);
  sitemap lists every page and article; ruler roster gains the interior
  section ids; Discord/Windows brand icons added.
2026-09-15 08:36:01 +07:00
Brandon Thomas ef2710fcc8 wip 2026-09-14 15:40:33 -04:00
Brandon Thomas ad08682610 support seedance 2.5 quicktime output 2026-09-14 15:40:32 -04:00
1017 changed files with 33428 additions and 32983 deletions

No files matched your search

@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO media_files\nSET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n maybe_project_type = ?,\n\n origin_category = ?,\n origin_product_category = ?,\n\n maybe_engine_category = ?,\n maybe_animation_type = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n\n checksum_sha2 = ?,\n\n maybe_prompt_token = ?,\n maybe_batch_token = ?,\n\n maybe_title = ?,\n maybe_duration_millis = ?,\n maybe_frame_width = ?,\n maybe_frame_height = ?,\n\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n\n creator_set_visibility = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n is_intermediate_system_file = ?,\n is_user_upload = ?,\n\n maybe_generation_provider = ?,\n\n maybe_creator_file_synthetic_id = ?,\n maybe_creator_category_synthetic_id = ?,\n\n maybe_origin_model_type = NULL,\n maybe_origin_model_token = NULL\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 30
},
"nullable": []
},
"hash": "026a3ab39a1ba09a891cebd840ab1c956e1bcedd07006ee34473bd4f1d0ff67a"
}
@@ -0,0 +1,102 @@
{
"db_name": "MySQL",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 20,
"type": "LongLong"
}
},
{
"name": "confirmation_code",
"ordinal": 1,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"max_size": 32,
"type": "String"
}
},
{
"name": "status: UserLoginChallengeStatus",
"ordinal": 2,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 64,
"type": "VarString"
}
},
{
"name": "maybe_deciding_user_token",
"ordinal": 3,
"type_info": {
"char_set": 224,
"flags": "MULTIPLE_KEY | BINARY",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "maybe_redeemed_user_session_id",
"ordinal": 4,
"type_info": {
"char_set": 63,
"flags": "UNIQUE_KEY",
"max_size": 20,
"type": "LongLong"
}
},
{
"name": "maybe_failure_type: UserLoginChallengeFailureType",
"ordinal": 5,
"type_info": {
"char_set": 224,
"flags": "BINARY",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "ip_address_creation",
"ordinal": 6,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 160,
"type": "VarString"
}
},
{
"name": "expires_at",
"ordinal": 7,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"max_size": 19,
"type": "Timestamp"
}
}
],
"nullable": [
false,
false,
false,
true,
true,
true,
false,
false
],
"parameters": {
"Right": 1
}
},
"hash": "06dc6f19f013fe24ffd32a313b66dd10a87498bf17c9a63b1ece87e338b0dc74",
"query": "\nSELECT id, confirmation_code, status AS `status: UserLoginChallengeStatus`, maybe_deciding_user_token, maybe_redeemed_user_session_id,\n maybe_failure_type AS `maybe_failure_type: UserLoginChallengeFailureType`, ip_address_creation, expires_at\nFROM user_login_challenges WHERE device_token_sha256 = ? FOR UPDATE\n"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 5
}
},
"hash": "1af20e0d9a5630302f987aea4847b65a34d6c412245f7fbf9c6fb7b513d7324b",
"query": "\nUPDATE user_login_challenges SET status = ?, maybe_failure_type = ?, maybe_failed_at = expires_at\nWHERE id = ? AND status IN (?, ?) AND expires_at <= NOW()\n"
}
@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"describe": {
"columns": [
{
"name": "expired",
"ordinal": 0,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL | BINARY",
"max_size": 1,
"type": "LongLong"
}
}
],
"nullable": [
false
],
"parameters": {
"Right": 1
}
},
"hash": "1c618e9816d6f3f7f217770f988a132d9521693da6474e945628d1b3efbc4b80",
"query": "\nSELECT expires_at <= NOW() AS expired FROM user_login_challenges WHERE id = ?\n"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -75,6 +75,16 @@
},
{
"ordinal": 7,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 8,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -84,7 +94,7 @@
}
},
{
"ordinal": 8,
"ordinal": 9,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -212,6 +222,7 @@
true,
true,
true,
true,
false,
true,
true,
@@ -219,5 +230,5 @@
false
]
},
"hash": "a52cc2d6e86607c430173553230b3794a4c1ada07274a975c04128e47d3b7f83"
"hash": "251082fa59374723719147af19e3411300291f1c7de0acffd078dff73f7fcf40"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n ",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"describe": {
"columns": [
{
@@ -255,6 +255,16 @@
},
{
"ordinal": 25,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 26,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -264,7 +274,7 @@
}
},
{
"ordinal": 26,
"ordinal": 27,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -303,9 +313,10 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "971471f349bcd22364b16b5d6c68e807ed897101eec1e8a62a7b8fab3359fa0c"
"hash": "29f205860e3a790cc0b4b4b8bbd28bbc92a9df27ba62cc2688be1aa195b1fcf2"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 6
}
},
"hash": "2ddd5589138c822385b39dde58fc0d44ffa003b09018f1799e243d8f09530981",
"query": "\nINSERT INTO user_sessions (\n token,\n user_token,\n maybe_impersonation_user_token,\n ip_address_creation,\n maybe_creation_type,\n expires_at\n)\nVALUES (?, ?, ?, ?, ?, ?)\n "
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,\n pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension\n\nFROM prompt_context_items pci\nJOIN media_files m\nON\n pci.media_token = m.token\nWHERE\n pci.prompt_token = ?\nORDER BY pci.id ASC\n ",
"query": "\nSELECT\n pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,\n pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n m.created_at as `created_at: DateTime<Utc>`\n\nFROM prompt_context_items pci\nJOIN media_files m\nON\n pci.media_token = m.token\nWHERE\n pci.prompt_token = ?\nORDER BY pci.id ASC\n ",
"describe": {
"columns": [
{
@@ -52,6 +52,26 @@
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 6,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
@@ -62,8 +82,10 @@
false,
false,
true,
true
true,
true,
false
]
},
"hash": "094eb099cde85835db0662c2700c6b82644b51bf9b3609391ed4d2945fe5e98f"
"hash": "2e53d51429bf380b141a8a680d1fc17990ab02f822cae4d8def2d1126e513549"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -95,6 +95,16 @@
},
{
"ordinal": 9,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 10,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "is_intermediate_system_file: bool",
"type_info": {
"type": "Tiny",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -214,7 +224,7 @@
}
},
{
"ordinal": 21,
"ordinal": 22,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -224,7 +234,7 @@
}
},
{
"ordinal": 22,
"ordinal": 23,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -250,6 +260,7 @@
true,
true,
true,
true,
false,
false,
false,
@@ -263,5 +274,5 @@
false
]
},
"hash": "c0ba9f9aa3184b1cb3d714173b31e530e201a6c24afd29ad534cf7f3954c7316"
"hash": "41b47b3e0874676651bd7de2c6c3bd2c25db56c51ce6e16810f4a7f56dfb48ab"
}
@@ -0,0 +1,36 @@
{
"db_name": "MySQL",
"describe": {
"columns": [
{
"name": "session_token",
"ordinal": 0,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "user_token",
"ordinal": 1,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 128,
"type": "VarString"
}
}
],
"nullable": [
false,
false
],
"parameters": {
"Right": 1
}
},
"hash": "459898b1b42a51097dcbc0f182bd0badce642b11bd33a7e1973a6cc25538ffbf",
"query": "\nSELECT\n token as session_token,\n user_token\nFROM user_sessions\nWHERE token = ?\nAND deleted_at IS NULL\nAND expires_at > NOW()\n "
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\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\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\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\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n media_files.maybe_thumbnail_version as `maybe_media_file_thumbnail_version?: u8`,\n media_files.created_at as `maybe_media_file_created_at?: DateTime<Utc>`,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
"describe": {
"columns": [
{
@@ -265,6 +265,26 @@
},
{
"ordinal": 26,
"name": "maybe_media_file_thumbnail_version?: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 27,
"name": "maybe_media_file_created_at?: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 28,
"name": "maybe_assigned_worker",
"type_info": {
"type": "VarString",
@@ -274,7 +294,7 @@
}
},
{
"ordinal": 27,
"ordinal": 29,
"name": "maybe_assigned_cluster",
"type_info": {
"type": "VarString",
@@ -284,7 +304,7 @@
}
},
{
"ordinal": 28,
"ordinal": 30,
"name": "is_keepalive_required",
"type_info": {
"type": "Tiny",
@@ -294,7 +314,7 @@
}
},
{
"ordinal": 29,
"ordinal": 31,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -304,7 +324,7 @@
}
},
{
"ordinal": 30,
"ordinal": 32,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -314,7 +334,7 @@
}
},
{
"ordinal": 31,
"ordinal": 33,
"name": "maybe_first_started_at",
"type_info": {
"type": "Timestamp",
@@ -324,7 +344,7 @@
}
},
{
"ordinal": 32,
"ordinal": 34,
"name": "maybe_successfully_completed_at",
"type_info": {
"type": "Timestamp",
@@ -334,7 +354,7 @@
}
},
{
"ordinal": 33,
"ordinal": 35,
"name": "database_clock",
"type_info": {
"type": "Datetime",
@@ -376,6 +396,8 @@
true,
true,
true,
true,
true,
false,
false,
false,
@@ -384,5 +406,5 @@
false
]
},
"hash": "6948edca3f06643605695665df51cc94f897e2cb9a2d6906910d58d4d56ee73e"
"hash": "4b3fa0dcfb916ba7af04ae35c69a2b75d1f4a4858e8f94f87d74c69fccc32079"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"hash": "4d3666433b8100c9b5f351c2be0faa982127c5bb4431ef14e84ff11c5a2fd9ff",
"query": "\nINSERT INTO user_sessions (\n token,\n user_token,\n ip_address_creation,\n maybe_creation_type,\n expires_at\n)\nVALUES ( ?, ?, ?, ?, NOW() + interval 1 year )\n "
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -213,11 +223,12 @@
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "d8f4ce4e1026c71a7be8fd87f00f6f8858c401701da1ca3316eed1ed971d4bb2"
"hash": "503c8faa461462b3e2f8cae3cb702922ad54a8ddbdb01d1e7fc785ca1b7dd4c4"
}
@@ -0,0 +1,58 @@
{
"db_name": "MySQL",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
"max_size": 20,
"type": "LongLong"
}
},
{
"name": "token",
"ordinal": 1,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "user_token",
"ordinal": 2,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "username",
"ordinal": 3,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 80,
"type": "VarString"
}
}
],
"nullable": [
false,
false,
false,
false
],
"parameters": {
"Right": 3
}
},
"hash": "705c952102e32df11ba98ec39a3c7943bdf633947c32d4e8f7d77a861560b1bd",
"query": "\nSELECT s.id, s.token, s.user_token, u.username FROM user_sessions s JOIN users u ON u.token = s.user_token\nWHERE s.id = ? AND s.user_token = ? AND s.maybe_creation_type = ?\n AND s.deleted_at IS NULL AND s.expires_at > NOW() AND s.maybe_impersonation_user_token IS NULL\n AND u.is_banned = FALSE AND u.user_deleted_at IS NULL AND u.mod_deleted_at IS NULL\n"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -213,11 +223,12 @@
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "11cc7b91730784e9e8c8f073aeaf51a1f1660584e379b9a5be3aea370d84f692"
"hash": "770f5c0672653e82c40deade6ba7b1e769558d8ebcf40d736f798f30c7a75c68"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.maybe_origin_filename,\n m.maybe_duration_millis as `maybe_duration_millis: u64`,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_source_job_token = ?\n AND m.maybe_creator_user_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\nORDER BY m.id ASC\nLIMIT ?\n ",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.maybe_origin_filename,\n m.maybe_duration_millis as `maybe_duration_millis: u64`,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_source_job_token = ?\n AND m.maybe_creator_user_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_duration_millis: u64",
"type_info": {
"type": "Long",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -171,9 +181,10 @@
true,
true,
true,
true,
false,
false
]
},
"hash": "cd04f4f7b48ae9da53b6a210111f8e3edff94dee30263e22b45d5e29dbe9245a"
"hash": "79f08e53fdacb526524810f481d22ca169b9f35e8874ed898b63cd2ba4b7453d"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_engine_category as `maybe_engine_category: enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory`,\n m.maybe_animation_type as `maybe_animation_type: enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_style_transfer_source_media_file_token as `maybe_style_transfer_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n m.maybe_scene_source_media_file_token as `maybe_scene_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n model_weights.token as `maybe_model_weights_token: tokens::tokens::model_weights::ModelWeightToken`,\n model_weights.title as maybe_model_weights_title,\n model_weights.weights_type as `maybe_model_weights_type: enums::by_table::model_weights::weights_types::WeightsType`,\n model_weights.weights_category as `maybe_model_weights_category: enums::by_table::model_weights::weights_category::WeightsCategory`,\n\n model_weight_cover_image.public_bucket_directory_hash as maybe_model_cover_image_public_bucket_hash,\n model_weight_cover_image.maybe_public_bucket_prefix as maybe_model_cover_image_public_bucket_prefix,\n model_weight_cover_image.maybe_public_bucket_extension as maybe_model_cover_image_public_bucket_extension,\n\n model_weight_creator.token as `maybe_model_weight_creator_user_token: tokens::tokens::users::UserToken`,\n model_weight_creator.username as maybe_model_weight_creator_username,\n model_weight_creator.display_name as maybe_model_weight_creator_display_name,\n model_weight_creator.email_gravatar_hash as maybe_model_weight_creator_gravatar_hash,\n\n m.extra_file_modification_info,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n entity_stats.ratings_positive_count as maybe_ratings_positive_count,\n entity_stats.ratings_negative_count as maybe_ratings_negative_count,\n entity_stats.bookmark_count as maybe_bookmark_count,\n\n featured_items.entity_token IS NOT NULL AS is_featured,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN model_weights\n ON m.maybe_origin_model_token = model_weights.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN media_files as model_weight_cover_image\n ON model_weight_cover_image.token = model_weights.maybe_cover_image_media_file_token\nLEFT OUTER JOIN users as model_weight_creator\n ON model_weight_creator.token = model_weights.creator_user_token\nLEFT OUTER JOIN entity_stats\n ON entity_stats.entity_type = \"media_file\"\n AND entity_stats.entity_token = m.token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nLEFT OUTER JOIN featured_items\n ON featured_items.entity_type = 'media_file'\n AND featured_items.entity_token = m.token\n AND featured_items.deleted_at IS NULL\nWHERE\n m.token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_engine_category as `maybe_engine_category: enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory`,\n m.maybe_animation_type as `maybe_animation_type: enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_style_transfer_source_media_file_token as `maybe_style_transfer_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n m.maybe_scene_source_media_file_token as `maybe_scene_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n model_weights.token as `maybe_model_weights_token: tokens::tokens::model_weights::ModelWeightToken`,\n model_weights.title as maybe_model_weights_title,\n model_weights.weights_type as `maybe_model_weights_type: enums::by_table::model_weights::weights_types::WeightsType`,\n model_weights.weights_category as `maybe_model_weights_category: enums::by_table::model_weights::weights_category::WeightsCategory`,\n\n model_weight_cover_image.public_bucket_directory_hash as maybe_model_cover_image_public_bucket_hash,\n model_weight_cover_image.maybe_public_bucket_prefix as maybe_model_cover_image_public_bucket_prefix,\n model_weight_cover_image.maybe_public_bucket_extension as maybe_model_cover_image_public_bucket_extension,\n\n model_weight_creator.token as `maybe_model_weight_creator_user_token: tokens::tokens::users::UserToken`,\n model_weight_creator.username as maybe_model_weight_creator_username,\n model_weight_creator.display_name as maybe_model_weight_creator_display_name,\n model_weight_creator.email_gravatar_hash as maybe_model_weight_creator_gravatar_hash,\n\n m.extra_file_modification_info,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n entity_stats.ratings_positive_count as maybe_ratings_positive_count,\n entity_stats.ratings_negative_count as maybe_ratings_negative_count,\n entity_stats.bookmark_count as maybe_bookmark_count,\n\n featured_items.entity_token IS NOT NULL AS is_featured,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN model_weights\n ON m.maybe_origin_model_token = model_weights.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN media_files as model_weight_cover_image\n ON model_weight_cover_image.token = model_weights.maybe_cover_image_media_file_token\nLEFT OUTER JOIN users as model_weight_creator\n ON model_weight_creator.token = model_weights.creator_user_token\nLEFT OUTER JOIN entity_stats\n ON entity_stats.entity_type = \"media_file\"\n AND entity_stats.entity_token = m.token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nLEFT OUTER JOIN featured_items\n ON featured_items.entity_type = 'media_file'\n AND featured_items.entity_token = m.token\n AND featured_items.deleted_at IS NULL\nWHERE\n m.token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"describe": {
"columns": [
{
@@ -415,6 +415,16 @@
},
{
"ordinal": 41,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 42,
"name": "maybe_ratings_positive_count",
"type_info": {
"type": "Long",
@@ -424,7 +434,7 @@
}
},
{
"ordinal": 42,
"ordinal": 43,
"name": "maybe_ratings_negative_count",
"type_info": {
"type": "Long",
@@ -434,7 +444,7 @@
}
},
{
"ordinal": 43,
"ordinal": 44,
"name": "maybe_bookmark_count",
"type_info": {
"type": "Long",
@@ -444,7 +454,7 @@
}
},
{
"ordinal": 44,
"ordinal": 45,
"name": "is_featured",
"type_info": {
"type": "LongLong",
@@ -454,7 +464,7 @@
}
},
{
"ordinal": 45,
"ordinal": 46,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -464,7 +474,7 @@
}
},
{
"ordinal": 46,
"ordinal": 47,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -522,10 +532,11 @@
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "ad217c26087b2ebed6f2c9f79b16ead63727107dfce3366f3c1d12bc9d565c2c"
"hash": "7a499a8fabb4633ef1c26a2df29851a706d9b6d5e99d2dc57a42f3939c7c5ecd"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 5
}
},
"hash": "7e9bc784634f436a8bf898223a5ddeea57886b7c248d82e782dd3289c23cbaec",
"query": "\nUPDATE user_login_challenges SET status = ?, maybe_redeemed_user_session_id = ?,\n maybe_ip_address_redemption = ?, maybe_redeemed_at = NOW()\nWHERE id = ? AND status = ? AND expires_at > NOW() AND maybe_redeemed_user_session_id IS NULL\n"
}
@@ -0,0 +1,58 @@
{
"db_name": "MySQL",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT",
"max_size": 20,
"type": "LongLong"
}
},
{
"name": "token",
"ordinal": 1,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "user_token",
"ordinal": 2,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "username",
"ordinal": 3,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 80,
"type": "VarString"
}
}
],
"nullable": [
false,
false,
false,
false
],
"parameters": {
"Right": 1
}
},
"hash": "8bfbb8bef3bec235f37c5f4f5c2f3a8b0833241a63da122b8abd97cd9b71208f",
"query": "\nSELECT s.id, s.token, s.user_token, u.username FROM user_sessions s JOIN users u ON u.token = s.user_token\nWHERE s.token = ? AND s.deleted_at IS NULL AND s.expires_at > NOW()\n AND s.maybe_impersonation_user_token IS NULL AND u.is_banned = FALSE\n AND u.user_deleted_at IS NULL AND u.mod_deleted_at IS NULL\n"
}
@@ -0,0 +1,454 @@
{
"db_name": "MySQL",
"describe": {
"columns": [
{
"name": "user_token",
"ordinal": 0,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "username",
"ordinal": 1,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 80,
"type": "VarString"
}
},
{
"name": "display_name",
"ordinal": 2,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"max_size": 80,
"type": "VarString"
}
},
{
"name": "email_address",
"ordinal": 3,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 1020,
"type": "VarString"
}
},
{
"name": "email_gravatar_hash",
"ordinal": 4,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"max_size": 128,
"type": "String"
}
},
{
"name": "email_confirmed",
"ordinal": 5,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "email_confirmed_by_google",
"ordinal": 6,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "email_is_synthetic",
"ordinal": 7,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "is_without_password",
"ordinal": 8,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "username_is_not_customized",
"ordinal": 9,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "maybe_stripe_customer_id",
"ordinal": 10,
"type_info": {
"char_set": 224,
"flags": "BINARY",
"max_size": 1020,
"type": "VarString"
}
},
{
"name": "maybe_loyalty_program_key",
"ordinal": 11,
"type_info": {
"char_set": 224,
"flags": "BINARY",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "disable_gravatar",
"ordinal": 12,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "auto_play_audio_preference",
"ordinal": 13,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "auto_play_video_preference",
"ordinal": 14,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "preferred_tts_result_visibility: enums::common::visibility::Visibility",
"ordinal": 15,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | BINARY | ENUM",
"max_size": 28,
"type": "String"
}
},
{
"name": "preferred_w2l_result_visibility: enums::common::visibility::Visibility",
"ordinal": 16,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | BINARY | ENUM",
"max_size": 28,
"type": "String"
}
},
{
"name": "user_role_slug",
"ordinal": 17,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 64,
"type": "VarString"
}
},
{
"name": "is_banned",
"ordinal": 18,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_access_studio",
"ordinal": 19,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL | MULTIPLE_KEY",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "maybe_feature_flags",
"ordinal": 20,
"type_info": {
"char_set": 224,
"flags": "BINARY",
"max_size": 1020,
"type": "VarString"
}
},
{
"name": "can_use_tts",
"ordinal": 21,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_use_w2l",
"ordinal": 22,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_own_tts_results",
"ordinal": 23,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_own_w2l_results",
"ordinal": 24,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_own_account",
"ordinal": 25,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_upload_tts_models",
"ordinal": 26,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_upload_w2l_templates",
"ordinal": 27,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_own_tts_models",
"ordinal": 28,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_own_w2l_templates",
"ordinal": 29,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_approve_w2l_templates",
"ordinal": 30,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_edit_other_users_profiles",
"ordinal": 31,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_edit_other_users_tts_models",
"ordinal": 32,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_edit_other_users_w2l_templates",
"ordinal": 33,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_other_users_tts_models",
"ordinal": 34,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_other_users_tts_results",
"ordinal": 35,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_other_users_w2l_templates",
"ordinal": 36,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_other_users_w2l_results",
"ordinal": 37,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_ban_users",
"ordinal": 38,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
},
{
"name": "can_delete_users",
"ordinal": 39,
"type_info": {
"char_set": 63,
"flags": "",
"max_size": 1,
"type": "Tiny"
}
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
false,
true,
true,
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true
],
"parameters": {
"Right": 1
}
},
"hash": "934aad794dcb0057e39c471e7b25e5e191a395a2bd16553cb58a483b91c68437",
"query": "\nSELECT\n users.token as user_token,\n users.username,\n users.display_name,\n\n users.email_address,\n users.email_gravatar_hash,\n \n users.email_confirmed,\n users.email_confirmed_by_google,\n users.email_is_synthetic,\n users.is_without_password,\n users.username_is_not_customized,\n\n users.maybe_stripe_customer_id,\n users.maybe_loyalty_program_key,\n\n users.disable_gravatar,\n users.auto_play_audio_preference,\n users.auto_play_video_preference,\n users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::common::visibility::Visibility`,\n users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::common::visibility::Visibility`,\n\n users.user_role_slug,\n users.is_banned,\n\n users.can_access_studio,\n users.maybe_feature_flags,\n\n user_roles.can_use_tts,\n user_roles.can_use_w2l,\n user_roles.can_delete_own_tts_results,\n user_roles.can_delete_own_w2l_results,\n user_roles.can_delete_own_account,\n\n user_roles.can_upload_tts_models,\n user_roles.can_upload_w2l_templates,\n user_roles.can_delete_own_tts_models,\n user_roles.can_delete_own_w2l_templates,\n\n user_roles.can_approve_w2l_templates,\n user_roles.can_edit_other_users_profiles,\n user_roles.can_edit_other_users_tts_models,\n user_roles.can_edit_other_users_w2l_templates,\n user_roles.can_delete_other_users_tts_models,\n user_roles.can_delete_other_users_tts_results,\n user_roles.can_delete_other_users_w2l_templates,\n user_roles.can_delete_other_users_w2l_results,\n user_roles.can_ban_users,\n user_roles.can_delete_users\n\nFROM users\nLEFT OUTER JOIN user_sessions\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND user_sessions.expires_at > NOW()\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\n "
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.media_class = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1\n FROM folder_media_files fmf\n JOIN folders f\n ON f.token = fmf.folder_token\n AND f.maybe_deleted_at IS NULL\n WHERE fmf.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.media_class = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1\n FROM folder_media_files fmf\n JOIN folders f\n ON f.token = fmf.folder_token\n AND f.maybe_deleted_at IS NULL\n WHERE fmf.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "3681e931cc87714d71c95da44d6dd92568ce58aea252164ab3d5f8e96491169c"
"hash": "96d03f60d11b2303a8fe8ac16e17cd1ee58fec404049ee3f906ed7b5fcc9c3ed"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -95,6 +95,16 @@
},
{
"ordinal": 9,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 10,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "is_intermediate_system_file: bool",
"type_info": {
"type": "Tiny",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -214,7 +224,7 @@
}
},
{
"ordinal": 21,
"ordinal": 22,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -224,7 +234,7 @@
}
},
{
"ordinal": 22,
"ordinal": 23,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -250,6 +260,7 @@
true,
true,
true,
true,
false,
false,
false,
@@ -263,5 +274,5 @@
false
]
},
"hash": "8aca9bc976b980ea1a3b3052bc8a45dd4d8db55c20fd1aede2a4415cc1395cf3"
"hash": "9bf5ccbfaaa7c1c4e9c4af4b3044b307976825383c9a737a22a877c89cd6db09"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -213,11 +223,12 @@
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "6c7adbc8171bc3fdfb6109193ffb0a55413fa0942746d65037c2b1d39b361bd0"
"hash": "9c8c0ea8510cd66175014e4cf4ffaf403c27256ae1e39f06f1bc430dbcea5e62"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1 FROM media_file_tags mft\n WHERE mft.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1 FROM media_file_tags mft\n WHERE mft.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "aa1e693babe6997e75935ec893e6eed46351ec72ce02a9144fd11d2fcfd37fdf"
"hash": "9f116e00964272b8389a766674177e7882a16ad6459785551bc2093e27404f80"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"hash": "ab9e5c27ce0c460aa6d5d1b202829c2082fa0701773582d22aae05a9d376d4a6",
"query": "\nINSERT INTO user_sessions (\n token,\n user_token,\n ip_address_creation,\n maybe_creation_type,\n expires_at\n)\nVALUES ( ?, ?, ?, ?, NOW() + interval 1 year )\n "
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -75,6 +75,16 @@
},
{
"ordinal": 7,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 8,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -84,7 +94,7 @@
}
},
{
"ordinal": 8,
"ordinal": 9,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -212,6 +222,7 @@
true,
true,
true,
true,
false,
true,
true,
@@ -219,5 +230,5 @@
false
]
},
"hash": "73540435892b7b4dbc11c6c63712abdb18fd8b3db2bcd56b8518c968b8169de7"
"hash": "b0d2f025aa7d137c0a9f50d0af4bf863e1d0741d9a34111bdfaaf18f10ec11f4"
}
@@ -0,0 +1,102 @@
{
"db_name": "MySQL",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL | PRIMARY_KEY | UNSIGNED | AUTO_INCREMENT",
"max_size": 20,
"type": "LongLong"
}
},
{
"name": "confirmation_code",
"ordinal": 1,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"max_size": 32,
"type": "String"
}
},
{
"name": "status: UserLoginChallengeStatus",
"ordinal": 2,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 64,
"type": "VarString"
}
},
{
"name": "maybe_deciding_user_token",
"ordinal": 3,
"type_info": {
"char_set": 224,
"flags": "MULTIPLE_KEY | BINARY",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "maybe_redeemed_user_session_id",
"ordinal": 4,
"type_info": {
"char_set": 63,
"flags": "UNIQUE_KEY",
"max_size": 20,
"type": "LongLong"
}
},
{
"name": "maybe_failure_type: UserLoginChallengeFailureType",
"ordinal": 5,
"type_info": {
"char_set": 224,
"flags": "BINARY",
"max_size": 128,
"type": "VarString"
}
},
{
"name": "ip_address_creation",
"ordinal": 6,
"type_info": {
"char_set": 224,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"max_size": 160,
"type": "VarString"
}
},
{
"name": "expires_at",
"ordinal": 7,
"type_info": {
"char_set": 63,
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"max_size": 19,
"type": "Timestamp"
}
}
],
"nullable": [
false,
false,
false,
true,
true,
true,
false,
false
],
"parameters": {
"Right": 1
}
},
"hash": "c0c85c8c55fb494d44efa6f1f2d37491c740d0ac897dcfb7a7e737bb01b824d6",
"query": "\nSELECT id, confirmation_code, status AS `status: UserLoginChallengeStatus`, maybe_deciding_user_token, maybe_redeemed_user_session_id,\n maybe_failure_type AS `maybe_failure_type: UserLoginChallengeFailureType`, ip_address_creation, expires_at\nFROM user_login_challenges WHERE approval_token_sha256 = ? FOR UPDATE\n"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 6
}
},
"hash": "cc29378fa77e7f09ca2ca0803127d111703a4edca24acfe3ccecf79c22fc1a80",
"query": "\nINSERT INTO user_login_challenges\n (token, approval_token_sha256, device_token_sha256, confirmation_code, ip_address_creation, status, created_at, expires_at)\nVALUES (?, ?, ?, ?, ?, ?, NOW(), NOW() + INTERVAL 20 MINUTE)\n"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"hash": "d0067b5e1b3ef5dec5572eb2c0741d129ab2f706db4bb9f37a32bb89aa44fa4f",
"query": "\nUPDATE user_login_challenges SET status = ?, maybe_failure_type = ?, maybe_failed_at = expires_at\nWHERE status IN (?, ?) AND expires_at <= NOW() ORDER BY expires_at LIMIT 1000\n"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n id,\n token as `token: MediaFileToken`,\n created_at as `created_at: DateTime<Utc>`,\n maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n public_bucket_directory_hash,\n maybe_public_bucket_prefix,\n maybe_public_bucket_extension\nFROM media_files\nWHERE\n id >= (SELECT COALESCE(MIN(id), 0) FROM media_files WHERE created_at >= NOW() - INTERVAL ? HOUR)\n AND id < ?\n AND media_class = ?\n AND maybe_thumbnail_version IS NULL\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nORDER BY id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n media.id,\n media.token as `token: MediaFileToken`,\n media.created_at as `created_at: DateTime<Utc>`,\n NOW(6) as `database_read_at!: DateTime<Utc>`,\n jobs.successfully_completed_at as `maybe_generation_completed_at?: DateTime<Utc>`,\n media.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n media.public_bucket_directory_hash,\n media.maybe_public_bucket_prefix,\n media.maybe_public_bucket_extension\nFROM media_files AS media\nLEFT JOIN generic_inference_jobs AS jobs ON jobs.token = media.maybe_source_job_token\nWHERE\n media.id >= (SELECT COALESCE(MIN(id), 0) FROM media_files WHERE created_at >= NOW() - INTERVAL ? HOUR)\n AND media.id < ?\n AND media.media_class = ?\n AND media.maybe_thumbnail_version IS NULL\n AND media.user_deleted_at IS NULL\n AND media.mod_deleted_at IS NULL\nORDER BY media.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -35,6 +35,26 @@
},
{
"ordinal": 3,
"name": "database_read_at!: DateTime<Utc>",
"type_info": {
"type": "Datetime",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 26
}
},
{
"ordinal": 4,
"name": "maybe_generation_completed_at?: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "BINARY",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 5,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
@@ -44,7 +64,7 @@
}
},
{
"ordinal": 4,
"ordinal": 6,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
@@ -54,7 +74,7 @@
}
},
{
"ordinal": 5,
"ordinal": 7,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -64,7 +84,7 @@
}
},
{
"ordinal": 6,
"ordinal": 8,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -81,11 +101,13 @@
false,
false,
false,
false,
true,
true,
false,
true,
true
]
},
"hash": "daed2dcd03b3c9fbdb9057b743df078f43e2af87a2c31f418234f98da5ff979f"
"hash": "d14ad3a64d261e64fa75f97c93ca74af97fad8591d6ebe9887aeabc43bed0982"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -213,11 +223,12 @@
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "97e4bc3ab037126e3f4e213cdb7535c2ad233809fbeb6607c34fb02ec45cfe47"
"hash": "d96bca438343b0a5a3f202ae48b51670ed3e660cdfaa0100b47893249d5d0c11"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 8
}
},
"hash": "da64968638e564d28ed180270df407157f6c9b3f27c3443417f30c12c158ab89",
"query": "\nUPDATE user_login_challenges SET status = ?, maybe_deciding_user_token = ?,\n maybe_ip_address_decision = ?, maybe_decided_at = NOW(), maybe_failure_type = ?,\n maybe_ip_address_failure = ?, maybe_failed_at = IF(?, NULL, NOW())\nWHERE id = ? AND status = ? AND expires_at > NOW()\n"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND EXISTS (\n SELECT 1 FROM media_file_tags mft\n WHERE mft.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND EXISTS (\n SELECT 1 FROM media_file_tags mft\n WHERE mft.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "76bcf698f40d852558f071a6909b05b1c960c1cf236831ae009f90de46d3aefb"
"hash": "dabd15fbe5fd3cc9f0ce921d9c804ea25956fc36bb7e677d29b869b31f2c6c1d"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_engine_category as `maybe_engine_category: enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory`,\n m.maybe_animation_type as `maybe_animation_type: enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_style_transfer_source_media_file_token as `maybe_style_transfer_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n m.maybe_scene_source_media_file_token as `maybe_scene_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n model_weights.token as `maybe_model_weights_token: tokens::tokens::model_weights::ModelWeightToken`,\n model_weights.title as maybe_model_weights_title,\n model_weights.weights_type as `maybe_model_weights_type: enums::by_table::model_weights::weights_types::WeightsType`,\n model_weights.weights_category as `maybe_model_weights_category: enums::by_table::model_weights::weights_category::WeightsCategory`,\n\n model_weight_cover_image.public_bucket_directory_hash as maybe_model_cover_image_public_bucket_hash,\n model_weight_cover_image.maybe_public_bucket_prefix as maybe_model_cover_image_public_bucket_prefix,\n model_weight_cover_image.maybe_public_bucket_extension as maybe_model_cover_image_public_bucket_extension,\n\n model_weight_creator.token as `maybe_model_weight_creator_user_token: tokens::tokens::users::UserToken`,\n model_weight_creator.username as maybe_model_weight_creator_username,\n model_weight_creator.display_name as maybe_model_weight_creator_display_name,\n model_weight_creator.email_gravatar_hash as maybe_model_weight_creator_gravatar_hash,\n\n m.extra_file_modification_info,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n entity_stats.ratings_positive_count as maybe_ratings_positive_count,\n entity_stats.ratings_negative_count as maybe_ratings_negative_count,\n entity_stats.bookmark_count as maybe_bookmark_count,\n\n featured_items.entity_token IS NOT NULL AS is_featured,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN model_weights\n ON m.maybe_origin_model_token = model_weights.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN media_files as model_weight_cover_image\n ON model_weight_cover_image.token = model_weights.maybe_cover_image_media_file_token\nLEFT OUTER JOIN users as model_weight_creator\n ON model_weight_creator.token = model_weights.creator_user_token\nLEFT OUTER JOIN entity_stats\n ON entity_stats.entity_type = \"media_file\"\n AND entity_stats.entity_token = m.token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nLEFT OUTER JOIN featured_items\n ON featured_items.entity_type = 'media_file'\n AND featured_items.entity_token = m.token\n AND featured_items.deleted_at IS NULL\nWHERE\n m.token = ?\n ",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_engine_category as `maybe_engine_category: enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory`,\n m.maybe_animation_type as `maybe_animation_type: enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_style_transfer_source_media_file_token as `maybe_style_transfer_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n m.maybe_scene_source_media_file_token as `maybe_scene_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n model_weights.token as `maybe_model_weights_token: tokens::tokens::model_weights::ModelWeightToken`,\n model_weights.title as maybe_model_weights_title,\n model_weights.weights_type as `maybe_model_weights_type: enums::by_table::model_weights::weights_types::WeightsType`,\n model_weights.weights_category as `maybe_model_weights_category: enums::by_table::model_weights::weights_category::WeightsCategory`,\n\n model_weight_cover_image.public_bucket_directory_hash as maybe_model_cover_image_public_bucket_hash,\n model_weight_cover_image.maybe_public_bucket_prefix as maybe_model_cover_image_public_bucket_prefix,\n model_weight_cover_image.maybe_public_bucket_extension as maybe_model_cover_image_public_bucket_extension,\n\n model_weight_creator.token as `maybe_model_weight_creator_user_token: tokens::tokens::users::UserToken`,\n model_weight_creator.username as maybe_model_weight_creator_username,\n model_weight_creator.display_name as maybe_model_weight_creator_display_name,\n model_weight_creator.email_gravatar_hash as maybe_model_weight_creator_gravatar_hash,\n\n m.extra_file_modification_info,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n entity_stats.ratings_positive_count as maybe_ratings_positive_count,\n entity_stats.ratings_negative_count as maybe_ratings_negative_count,\n entity_stats.bookmark_count as maybe_bookmark_count,\n\n featured_items.entity_token IS NOT NULL AS is_featured,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN model_weights\n ON m.maybe_origin_model_token = model_weights.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN media_files as model_weight_cover_image\n ON model_weight_cover_image.token = model_weights.maybe_cover_image_media_file_token\nLEFT OUTER JOIN users as model_weight_creator\n ON model_weight_creator.token = model_weights.creator_user_token\nLEFT OUTER JOIN entity_stats\n ON entity_stats.entity_type = \"media_file\"\n AND entity_stats.entity_token = m.token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nLEFT OUTER JOIN featured_items\n ON featured_items.entity_type = 'media_file'\n AND featured_items.entity_token = m.token\n AND featured_items.deleted_at IS NULL\nWHERE\n m.token = ?\n ",
"describe": {
"columns": [
{
@@ -415,6 +415,16 @@
},
{
"ordinal": 41,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 42,
"name": "maybe_ratings_positive_count",
"type_info": {
"type": "Long",
@@ -424,7 +434,7 @@
}
},
{
"ordinal": 42,
"ordinal": 43,
"name": "maybe_ratings_negative_count",
"type_info": {
"type": "Long",
@@ -434,7 +444,7 @@
}
},
{
"ordinal": 43,
"ordinal": 44,
"name": "maybe_bookmark_count",
"type_info": {
"type": "Long",
@@ -444,7 +454,7 @@
}
},
{
"ordinal": 44,
"ordinal": 45,
"name": "is_featured",
"type_info": {
"type": "LongLong",
@@ -454,7 +464,7 @@
}
},
{
"ordinal": 45,
"ordinal": 46,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -464,7 +474,7 @@
}
},
{
"ordinal": 46,
"ordinal": 47,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -522,10 +532,11 @@
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "784ca68c6cb61d481b605bfe6ee3248a5b44499038c71b57fc9ea6c4722d170f"
"hash": "e6c3c16d96339ad84b65302f3610b4b798e7ea16c2ae9d03d6b706cb42989990"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"hash": "eb5fdd0e93978edfbe97c58238ea81f0c21dba047975beb53a39224894e9d41c",
"query": "\nINSERT INTO user_sessions (token, user_token, ip_address_creation, maybe_creation_type, expires_at)\nSELECT ?, token, ?, ?, NOW() + INTERVAL 1 YEAR FROM users\nWHERE token = ? AND is_banned = FALSE AND user_deleted_at IS NULL AND mod_deleted_at IS NULL\n"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n ",
"describe": {
"columns": [
{
@@ -255,6 +255,16 @@
},
{
"ordinal": 25,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 26,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -264,7 +274,7 @@
}
},
{
"ordinal": 26,
"ordinal": 27,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -303,9 +313,10 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "f75fedb86a93ac2542315ddb7ef0d46aa42ecc25a3cf9856219114064183337a"
"hash": "ede1fc9893cb4309a89acc15a567b660faba1bb7eb0bf012605a9dfa15a53229"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1\n FROM folder_media_files fmf\n JOIN folders f\n ON f.token = fmf.folder_token\n AND f.maybe_deleted_at IS NULL\n WHERE fmf.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1\n FROM folder_media_files fmf\n JOIN folders f\n ON f.token = fmf.folder_token\n AND f.maybe_deleted_at IS NULL\n WHERE fmf.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "6f077c5d35b19319aba4e9f4c59610822334fd304ba9133ba8e099056c426f2a"
"hash": "f392e28b5f4287625ccb1ee0668089f9049940368da50b7f0e0b8370f112305c"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_file_tags mft\nJOIN media_files mf\n ON mf.token = mft.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mft.tag_token = ?\n AND mft.user_token = ?\n AND mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_file_tags mft\nJOIN media_files mf\n ON mf.token = mft.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mft.tag_token = ?\n AND mft.user_token = ?\n AND mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "f36c7238231353882b5b2ac39616dcdc6adebf7c7e1198d9c2af4674ba5d982c"
"hash": "fbdbb627018a7664499834149d5f571457ba4313b1666152197cdbea92c8068c"
}
+44 -3
View File
@@ -32,12 +32,41 @@ artcraft-services/
│ ├── plugins/ # Shared billing and service components
│ └── web/storyteller_web/ # Main Actix Web HTTP API
├── frontend/
│ ├── apps/artcraft-webapp/ # Browser application at app.getartcraft.com
│ ├── apps/artcraft-website/ # Product website at getartcraft.com
│ └── libs/ # Shared React components and TypeScript libraries
│ ├── apps/artcraft-webapp/ # Browser application at app.getartcraft.com
│ ├── apps/artcraft-website-next/ # Current public website at getartcraft.com (Next.js)
│ ├── apps/artcraft-desktop-website/ # Static landing page at desktop.getartcraft.com (Vite)
│ └── libs/ # Shared React components and TypeScript libraries
└── Cargo.toml # Rust workspace
```
## Updating Desktop Download Links
Desktop release links are configured independently in **two places**. When
updating the advertised ArtCraft release, update both `WINDOWS_VERSION` and
`MAC_VERSION` in both unless the request explicitly targets one platform or
app:
1. **Current public website (Next.js):**
[frontend/apps/artcraft-website-next/src/lib/download-links.ts](frontend/apps/artcraft-website-next/src/lib/download-links.ts).
This controls the installer links and visible version at
`https://getartcraft.com/download`. Add the release to `DOWNLOAD_HISTORY`
(newest first), then update both version selectors.
2. **Browser webapp:**
[frontend/apps/artcraft-webapp/src/config/github_download_links.ts](frontend/apps/artcraft-webapp/src/config/github_download_links.ts).
Update both version constants; these links are used by the welcome and
checkout-success pages at `app.getartcraft.com`.
Keep previous releases for rollback. The Next.js download page reads its visible
version from `DOWNLOAD_VERSIONS`, which uses the same selectors as the URLs; do
not hardcode a separate version in the page component.
Check that both configs produce the requested release tag and installer
filenames for Windows (`ArtCraft_<version>_x64-setup.exe`) and macOS
(`ArtCraft_<version>_universal.dmg`). Run `npm run typecheck` from
`frontend/apps/artcraft-website-next` after changing its config. Repository edits
require deployment of the affected apps before the public pages change; do not
report a local update as a deployed update.
## Code Style
- Rust with no minimum supported version
@@ -63,6 +92,18 @@ Organize for top-to-bottom reading. Important things first, details later.
- Among helpers: meatier logic above leaf-level formatters
- **In test modules**: constants first, then test cases (grouped into sub-modules when 2+), then helper functions last
## Public API Compatibility
- Public HTTP endpoints have deployed legacy clients. Preserve their request and
response schemas, defaults, limits, and pagination behavior when optimizing them.
- Put a changed contract behind a new endpoint/version and update new clients to
call it explicitly. Do not add a new mode or change response fields on an existing
public endpoint as part of a performance fix.
- For an endpoint-local revision, version the operation path (for example,
`/v1/media_files/list_v2/user/{username}`). Reserve a top-level `/v2` for broader
API changes.
- Add regression tests for the original contract alongside tests for the new route.
## Markdown
- **Tables must be space-padded so columns align in plain text.** Markdown
Generated
+17
View File
@@ -313,6 +313,7 @@ dependencies = [
"actix-http",
"actix-rt",
"actix-web",
"domains",
"log",
"server_environment",
"speculoos",
@@ -1348,6 +1349,7 @@ dependencies = [
"hostname 0.4.0",
"log",
"server_environment",
"tempfile",
"workspace-build-acceleration",
]
@@ -2687,6 +2689,13 @@ dependencies = [
"litrs",
]
[[package]]
name = "domains"
version = "0.0.1"
dependencies = [
"url",
]
[[package]]
name = "dotenv"
version = "0.15.0"
@@ -3239,6 +3248,7 @@ dependencies = [
"ffprobe",
"log",
"tempdir",
"tempfile",
"test_utils",
"workspace-build-acceleration",
]
@@ -7919,10 +7929,12 @@ dependencies = [
"easyenv",
"enums",
"errors",
"ffmpeg_utils",
"hashing",
"jobs_common",
"kinovi_web_client",
"log",
"mimetypes",
"mysql_queries",
"pager",
"rand 0.9.2",
@@ -8711,6 +8723,7 @@ dependencies = [
"datetimes",
"derive_more 0.99.17",
"diesel",
"domains",
"easyenv",
"elasticsearch",
"elasticsearch_schema",
@@ -8760,6 +8773,7 @@ dependencies = [
"redis_common",
"redis_schema",
"regex",
"reqwest 0.12.23",
"resend-rs",
"reusable_types",
"rootly_client",
@@ -8771,6 +8785,7 @@ dependencies = [
"serde_json",
"server_environment",
"sha1_hash",
"sha2 0.11.0-rc.5",
"shared_env_var_config",
"speculoos",
"sqlx",
@@ -10380,11 +10395,13 @@ dependencies = [
"anyhow",
"bootstrap",
"bucket_paths",
"chrono",
"cloud_storage",
"concurrency",
"easyenv",
"errors",
"ffmpeg_utils",
"futures 0.3.32",
"jobs_common",
"log",
"mysql_queries",
+2
View File
@@ -67,6 +67,7 @@ members = [
"crates/lib/files/zip_archives",
"crates/lib/http_headers",
"crates/lib/jobs_common",
"crates/lib/domains",
"crates/lib/jwt_light",
"crates/lib/opaque_cursors",
"crates/lib/jwt_signer",
@@ -252,6 +253,7 @@ grok_api_client = { path = "crates/api_clients/grok_api_client" }
rootly_client = { path = "crates/api_clients/rootly_client" }
rootly_config = { path = "crates/schema/config/rootly_config" }
shared_env_var_config = { path = "crates/schema/config/shared_env_var_config" }
domains = { path = "crates/lib/domains" }
http_headers = { path = "crates/lib/http_headers" }
images = { path = "crates/lib/files/images" }
jobs_common = { path = "crates/lib/jobs_common" }
+5 -8
View File
@@ -104,7 +104,7 @@ and signals for state, Three.js for 3D scenes, and shared UI and generation tool
| Path | Purpose |
|----------------------------------|-----------------------------------------------|
| `frontend/apps/artcraft-webapp` | Browser application at `app.getartcraft.com` |
| `frontend/apps/artcraft-website` | Product website at `getartcraft.com` |
| `frontend/apps/artcraft-website-next` | Product website at `getartcraft.com` (Next.js) |
| `frontend/libs/api` | HTTP clients, API host selection, and models |
| `frontend/libs/omni-gen` | Shared generation logic |
| `frontend/libs/components` | Reusable UI, editors, and generation controls |
@@ -190,14 +190,11 @@ The browser app runs at `http://localhost:4201`. `VITE_USE_LOCAL_API=true` selec
the hosted API. API host selection lives in
[`StorytellerApiHostStore`](./frontend/libs/api/src/lib/config/StorytellerApiHostStore.ts).
From `frontend`, build the web app or run the product website with:
From `frontend`, build the web app with `npx nx build artcraft-webapp`. The
product website is a standalone Next.js app (not part of the Nx workspace); run it
from the repository root with `./script/website/unix_frontend_website_dev.sh`.
```bash
npx nx build artcraft-webapp
npx nx dev artcraft-website
```
The website runs at `http://localhost:4200`. Repository-root launchers are in
The website runs at `http://localhost:3000`. Repository-root launchers are in
[`script/website`](./script/website). For desktop development, use the
[ArtCraft desktop repository](https://github.com/storytold/artcraft).
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_login_challenges;
@@ -0,0 +1,93 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- One approval request for a new cookie-based user session on another device.
-- See _database/sql/user_login_challenges.md for transitions and API requirements.
-- This table does not authorize MCP sessions or API keys.
CREATE TABLE user_login_challenges (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Non-secret identifier for logs/support. Never sufficient to approve or redeem.
token VARCHAR(32) NOT NULL,
-- SHA-256 digests of TWO INDEPENDENT cryptographically random 256-bit tokens.
-- Hash the canonical unpadded base64url token text; never store the raw tokens.
-- The approval token goes in the browser/QR URL and only permits reviewing a
-- request and submitting an authenticated user's explicit approve/decline choice.
approval_token_sha256 BINARY(32) NOT NULL,
-- Only the initiating device receives this token. Required for polling/redeeming.
-- Never include it in the approval URL, QR code, browser page, or application logs.
device_token_sha256 BINARY(32) NOT NULL,
-- Eight random uppercase consonants, displayed as e.g. WDJB-MJHT on BOTH screens.
-- Comparison aid only: not unique, not a lookup key, not an authentication factor.
confirmation_code CHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
-- UserLoginChallengeStatus: pending, approved, redeemed, failed.
-- Only pending may be approved/declined. Only approved may mint a session.
-- Expiry is checked independently of status on EVERY approval/redemption request.
-- Application inserts explicitly bind UserLoginChallengeStatus::Pending.
status VARCHAR(16) NOT NULL,
-- The authenticated web user who approved OR declined. NULL until a decision.
-- On approval this becomes the immutable owner of the session to be minted.
maybe_deciding_user_token VARCHAR(32) DEFAULT NULL,
-- Set atomically with status=redeemed and session insertion, under a row lock.
-- Numeric reference avoids copying a session credential into this audit table.
-- Intentionally signed to match the existing user_sessions.id column.
-- At most one session per challenge; retries may only return that SAME live session.
maybe_redeemed_user_session_id BIGINT(20) DEFAULT NULL,
-- UserLoginChallengeFailureType: user_declined, expired.
-- Only failed rows have a failure type and maybe_failed_at.
-- Transient errors / invalid credentials must not overwrite a valid challenge.
maybe_failure_type VARCHAR(32) DEFAULT NULL,
-- ========== IP ADDRESSES ==========
-- Server-observed requesting device IP, displayed on the approval screen.
-- Canonical IPv4/IPv6 strings; never accept an IP supplied in a request body.
ip_address_creation VARCHAR(40) NOT NULL,
-- Browser/mobile IP for approval or denial; may differ from the device IP.
maybe_ip_address_decision VARCHAR(40) DEFAULT NULL,
-- Device IP at FIRST successful redemption. Retries do not overwrite it.
maybe_ip_address_redemption VARCHAR(40) DEFAULT NULL,
-- For user_declined this is the browser IP. For expiry there is no actor/IP.
-- ip_address_creation is retained on ALL failures, including timeouts.
maybe_ip_address_failure VARCHAR(40) DEFAULT NULL,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Immutable deadline: insert with created_at=NOW(), expires_at=NOW()+INTERVAL 20 MINUTE.
-- The default deliberately fails closed if a writer forgets to set the deadline.
-- Approval, polling, and retrying must NEVER extend it. NOW() >= expires_at is expired.
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
maybe_decided_at TIMESTAMP NULL DEFAULT NULL,
maybe_redeemed_at TIMESTAMP NULL DEFAULT NULL,
-- Decision time for user_declined; expires_at for expired (even if recorded later).
maybe_failed_at TIMESTAMP NULL DEFAULT NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (approval_token_sha256),
UNIQUE KEY (device_token_sha256),
UNIQUE KEY (maybe_redeemed_user_session_id),
KEY index_status_expires_at (status, expires_at),
KEY index_expires_at (expires_at),
KEY index_deciding_user_created_at (maybe_deciding_user_token, created_at),
KEY index_creation_ip_created_at (ip_address_creation, created_at),
KEY index_failure_ip_failed_at (maybe_ip_address_failure, maybe_failed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE user_sessions
DROP COLUMN maybe_creation_type;
@@ -0,0 +1,10 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- UserWebSessionCreationType: direct_login, device_approval, impersonation.
-- NULL means unknown / legacy writer, not direct_login. Do not backfill guesses.
-- Additive metadata only: does not change validation, expiry, or impersonation rules.
-- MCP sessions and API keys are separate tables and do not use this field.
ALTER TABLE user_sessions
ADD COLUMN maybe_creation_type VARCHAR(32) DEFAULT NULL;
+6
View File
@@ -24,6 +24,11 @@ These are created with the `diesel` tool, and in particular:
- Primary key `id` columns must be `BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT`. Do not use signed IDs.
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
- Application-owned state/status/type columns must not have schema defaults. Use
`NOT NULL` without `DEFAULT` when required, and have application inserts explicitly
supply the value. Document the corresponding Rust enum; application queries bind its
variants instead of hardcoding enum strings in SQL. See
`crates/schema/database/mysql_queries/AGENTS.md` for the query rules.
- All SQL files should start with the standard IntelliJ noinspection comments:
```sql
-- noinspection SqlDialectInspectionForFile
@@ -33,6 +38,7 @@ These are created with the `diesel` tool, and in particular:
## Rules
- Keep documentation in this directory as regular files, not symlinks. Diesel can treat a documentation symlink as a migration directory and reject the entire migration list.
- Every `up.sql` must have a corresponding `down.sql` that cleanly reverses the migration.
- For `CREATE TABLE` migrations, the `down.sql` is typically `DROP TABLE IF EXISTS table_name;`.
- For `ALTER TABLE` migrations, the `down.sql` reverses the alteration (e.g. `DROP COLUMN`).
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+6
View File
@@ -0,0 +1,6 @@
# SQL Migrations
Read and follow [AGENTS.md](AGENTS.md) in this directory.
This must remain a regular file. Diesel treats a symlink here as a migration
directory and fails with `Invalid migration directory`.
@@ -13,6 +13,9 @@ Update the corresponding squashed file whenever a migration changes a table's sc
- Primary key `id` columns must be `BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT`. Do not use signed IDs.
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
- Application-owned state/status/type columns must not have schema defaults. Required
values stay `NOT NULL` and are supplied explicitly by application code using the
corresponding Rust enum. Keep these definitions consistent with the applied migrations.
- All SQL files should start with the standard IntelliJ noinspection comments:
```sql
-- noinspection SqlDialectInspectionForFile
@@ -0,0 +1,93 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- One approval request for a new cookie-based user session on another device.
-- See _database/sql/user_login_challenges.md for transitions and API requirements.
-- This table does not authorize MCP sessions or API keys.
CREATE TABLE user_login_challenges (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- Non-secret identifier for logs/support. Never sufficient to approve or redeem.
token VARCHAR(32) NOT NULL,
-- SHA-256 digests of TWO INDEPENDENT cryptographically random 256-bit tokens.
-- Hash the canonical unpadded base64url token text; never store the raw tokens.
-- The approval token goes in the browser/QR URL and only permits reviewing a
-- request and submitting an authenticated user's explicit approve/decline choice.
approval_token_sha256 BINARY(32) NOT NULL,
-- Only the initiating device receives this token. Required for polling/redeeming.
-- Never include it in the approval URL, QR code, browser page, or application logs.
device_token_sha256 BINARY(32) NOT NULL,
-- Eight random uppercase consonants, displayed as e.g. WDJB-MJHT on BOTH screens.
-- Comparison aid only: not unique, not a lookup key, not an authentication factor.
confirmation_code CHAR(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
-- UserLoginChallengeStatus: pending, approved, redeemed, failed.
-- Only pending may be approved/declined. Only approved may mint a session.
-- Expiry is checked independently of status on EVERY approval/redemption request.
-- Application inserts explicitly bind UserLoginChallengeStatus::Pending.
status VARCHAR(16) NOT NULL,
-- The authenticated web user who approved OR declined. NULL until a decision.
-- On approval this becomes the immutable owner of the session to be minted.
maybe_deciding_user_token VARCHAR(32) DEFAULT NULL,
-- Set atomically with status=redeemed and session insertion, under a row lock.
-- Numeric reference avoids copying a session credential into this audit table.
-- Intentionally signed to match the existing user_sessions.id column.
-- At most one session per challenge; retries may only return that SAME live session.
maybe_redeemed_user_session_id BIGINT(20) DEFAULT NULL,
-- UserLoginChallengeFailureType: user_declined, expired.
-- Only failed rows have a failure type and maybe_failed_at.
-- Transient errors / invalid credentials must not overwrite a valid challenge.
maybe_failure_type VARCHAR(32) DEFAULT NULL,
-- ========== IP ADDRESSES ==========
-- Server-observed requesting device IP, displayed on the approval screen.
-- Canonical IPv4/IPv6 strings; never accept an IP supplied in a request body.
ip_address_creation VARCHAR(40) NOT NULL,
-- Browser/mobile IP for approval or denial; may differ from the device IP.
maybe_ip_address_decision VARCHAR(40) DEFAULT NULL,
-- Device IP at FIRST successful redemption. Retries do not overwrite it.
maybe_ip_address_redemption VARCHAR(40) DEFAULT NULL,
-- For user_declined this is the browser IP. For expiry there is no actor/IP.
-- ip_address_creation is retained on ALL failures, including timeouts.
maybe_ip_address_failure VARCHAR(40) DEFAULT NULL,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Immutable deadline: insert with created_at=NOW(), expires_at=NOW()+INTERVAL 20 MINUTE.
-- The default deliberately fails closed if a writer forgets to set the deadline.
-- Approval, polling, and retrying must NEVER extend it. NOW() >= expires_at is expired.
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
maybe_decided_at TIMESTAMP NULL DEFAULT NULL,
maybe_redeemed_at TIMESTAMP NULL DEFAULT NULL,
-- Decision time for user_declined; expires_at for expired (even if recorded later).
maybe_failed_at TIMESTAMP NULL DEFAULT NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (approval_token_sha256),
UNIQUE KEY (device_token_sha256),
UNIQUE KEY (maybe_redeemed_user_session_id),
KEY index_status_expires_at (status, expires_at),
KEY index_expires_at (expires_at),
KEY index_deciding_user_created_at (maybe_deciding_user_token, created_at),
KEY index_creation_ip_created_at (ip_address_creation, created_at),
KEY index_failure_ip_failed_at (maybe_ip_address_failure, maybe_failed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -41,6 +41,12 @@ CREATE TABLE user_sessions (
-- designed to be recoverable.
deleted_at TIMESTAMP NULL,
-- UserWebSessionCreationType: direct_login, device_approval, impersonation.
-- NULL means unknown / legacy writer, not direct_login. Do not backfill guesses.
-- Additive metadata only: does not change validation, expiry, or impersonation rules.
-- MCP sessions and API keys are separate tables and do not use this field.
maybe_creation_type VARCHAR(32) DEFAULT NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
+322
View File
@@ -0,0 +1,322 @@
# Website-approved device login
Implemented across storyteller-web, the browser approval page, and the Tauri
login dialog. Apply the two migrations before deploying the API; deploy the web
approval route before releasing the desktop buttons. No production migrations or
deployments were performed while developing this change.
Call the flow **device approval**, and each attempt a **login challenge**. It works
for a desktop browser link or a phone scanning a QR code, regardless of how the
approving account signed in. It creates an ordinary cookie-based web session.
The design borrows separate device/browser credentials, polling, explicit consent,
and code comparison from [OAuth device authorization (RFC 8628)](https://www.rfc-editor.org/rfc/rfc8628.html#section-3.3.1).
It is an ArtCraft session bridge, not an OAuth token endpoint implementation.
## Two migrations
1. `2026-09-25-120000-0000_create_user_login_challenges` creates the challenge and
its terminal outcome audit record together. A separate failure table is not
needed for one immutable outcome per attempt. Status is `NOT NULL` without a
default; inserts explicitly bind `UserLoginChallengeStatus::Pending`.
2. `2026-09-25-120000-0001_alter_user_sessions_add_maybe_creation_type` adds nullable
provenance to cookie sessions. Existing rows and older writers retain NULL.
Both have reverse migrations and matching materialized schemas under
`migrations_squashed/user_tables/`. Deploy the schema before new writers. Roll
back feature code before dropping the column/table; rollback removes bridge audit
records but does not revoke sessions already issued.
`UserWebSessionCreationType` values:
| Rust variant | Stored value | Meaning |
|------------------|-------------------|-------------------------------------------------------------------|
| DirectLogin | direct_login | Password or SSO login/signup, including desktop password login. |
| DeviceApproval | device_approval | A new session minted from an approved login challenge. |
| Impersonation | impersonation | Existing consented staff impersonation flow. |
| None | NULL | Unknown provenance, including existing rows and older writers. |
`DirectLogin` describes the creation mechanism more accurately than `Web`, since
ordinary password login is also available in the desktop app. This field does not
replace `maybe_impersonation_user_token` or influence authentication/authorization.
MCP and API credentials continue using `mcp_sessions` and `api_keys` respectively.
Existing password/SSO and impersonation session writers now record provenance.
There is no historical backfill or default that would invent it.
## Credentials and confirmation
Generate two independent 32-byte CSPRNG values per challenge. Encode each as
unpadded base64url (43 ASCII characters). Store only the SHA-256 digest of that
canonical ASCII representation in the two uniquely indexed `BINARY(32)` columns.
Reject noncanonical input instead of accepting multiple representations. These
are random tokens, not passwords, so a fast cryptographic hash is appropriate.
| Value | Who receives it | What it permits |
|---------------------|-------------------------------|--------------------------------------------------------|
| token | Internal logs/support | Identifying a row; no authentication. |
| approval token | Desktop, browser link, QR | Review and authenticated approval/decline only. |
| device token | Initiating desktop only | Polling and collecting the newly authorized session. |
| confirmation code | Desktop and approval screen | Visual comparison only; no lookup or authentication. |
Use eight uniformly random characters from `BCDFGHJKLMNPQRSTVWXZ` for the code.
Store `WDJBMJHT`, display `WDJB-MJHT`, and ask the user to verify the match. It need
not be unique because it is never used to locate or authorize a challenge. Manual
short-code entry is outside this initial flow; both entry points carry the full
approval token. The private device token must never be in the URL or QR code.
The website displays the requesting IP, the account being authorized, the matching
code, and explicit **Approve desktop login** / **Decline** actions. An existing
website session skips only the login screen, never consent. If login is needed,
keep the pending approval context in first-party browser state and resume the
confirmation page after password or Google SSO login. Recheck expiry after login.
Do not send the bridge tokens to Google or another identity provider. Allow only
the fixed first-party return route, not an arbitrary caller-supplied redirect URL.
The approval page is a read-only GET; link scanners and previews cannot decide
the challenge. Decisions require a valid, non-impersonated user web session plus
CSRF/origin protection. Never accept an API key or MCP credential as consent.
Require the user to have initiated the request on a device in their possession;
the visible code and IP help detect phishing but are not authentication factors.
The approval URL uses HTTPS and carries the browser credential in its fragment.
The page captures and removes that fragment before referral collection or React
providers initialize, skips analytics/referral tracking on direct entry, and sets
a no-referrer policy. API responses send `Cache-Control: no-store` and
`Referrer-Policy: no-referrer`; API credentials travel in JSON bodies, never query
strings. Shared fonts and the Google sign-in provider still load. Keep request and
response bodies out of gateway logs. Invalid credentials reveal no account or
challenge details. The IP follows the existing trusted-gateway forwarding policy;
the gateway must replace untrusted incoming forwarding headers.
## State and the 20-minute deadline
```mermaid
stateDiagram-v2
[*] --> pending
pending --> approved: authenticated approval
pending --> failed: user_declined
pending --> failed: expired
approved --> failed: expired before redemption
approved --> redeemed: atomically mint one session
redeemed --> [*]
failed --> [*]
```
Set `created_at = NOW()` and `expires_at = NOW() + INTERVAL 20 MINUTE` in the same
INSERT using the database's clock. The default expiry is immediate so a writer
that omits it fails closed. Neither approval nor polling extends this deadline.
All eligibility checks require `expires_at > NOW()`; equality means expired.
The create query enforces this exact lifetime; these migrations do not add
triggers or database CHECK constraints. Keep database connections in UTC.
The persisted status alone is insufficient to decide validity. A pending or
approved row whose deadline passed is already expired even before a sweeper
marks it `failed`. Approvals and first redemptions must check the deadline again
under the row lock. Polls are normally read-only and do not touch `updated_at`.
| Status | Required accompanying data |
|------------|-------------------------------------------------------------------------------------|
| pending | No decision, failure, redemption, or session reference. |
| approved | Deciding user, decision IP/time; no failure, redemption, or session reference. |
| redeemed | Approval data plus session ID, first redemption IP/time; no failure. |
| failed | Failure type/time; no redemption or session reference; retain any prior approval. |
`UserLoginChallengeFailureType` initially has two values:
- `user_declined`: record the deciding user, browser IP in both decision/failure
IP fields, and the decision time in both decision/failure timestamps. Keep the
original desktop IP. Polling with the valid device token reports the decline.
- `expired`: record `maybe_failed_at = expires_at`. Retain original desktop IP and
any prior approval, but leave the failure IP NULL: a timeout has no actor. A
scheduled sweep records abandoned requests even when nobody polls again.
Failed challenges are unusable for approval or redemption. Their digests and
audit metadata remain for diagnosing failures; do not physically delete the row
when a user declines. A new attempt creates a new row and fresh tokens. Closing
the desktop dialog stops polling; unused requests expire normally.
Invalid tokens, failed website sign-ins, throttling, and transient infrastructure
errors do not terminally fail a valid challenge. Record those requests in the
existing operational/abuse logging with their IP and failure category, without
credentials. A second decision cannot replace the first actor or outcome.
## Atomic redemption and response loss
The website's approval records consent; it never creates or receives the desktop
cookie. The desktop's authenticated poll performs redemption after approval:
1. Begin an InnoDB transaction on the primary and find the challenge by the device
token digest with `SELECT ... FOR UPDATE`.
2. Validate the immutable deadline and state after taking the lock. Handle
failed/pending states without creating a session. Revalidate that the approved
user exists and is permitted to log in under the ordinary session rules.
3. For `approved`, insert a fresh `user_sessions` row owned by the deciding user,
with `maybe_creation_type = 'device_approval'`, no impersonation, the current
desktop redemption IP, and the ordinary web-session lifetime.
4. Store its numeric ID, redemption IP/time, and `status = 'redeemed'` on the
challenge in that same transaction. Generate the signed cookie using
`HttpUserSessionManager::create_cookie` before commit, so signing failure rolls
back too. Commit successfully before returning any credential to the caller.
Both approval/decline and redemption must serialize on the same challenge row;
conditional updates include the expected state and the unexpired deadline, and
must check rows affected. SQL for this flow belongs in `mysql_queries`, with
executor/transaction support, not in the web handler. All three Rust enums in this
change are database enums; separate client-facing status/failure enums include
an unknown-value fallback. Desktop callers fail closed on an unknown state.
One-time means **at most one new session per challenge**. If the first successful
HTTP response is lost, a retry with the same device token may return the same
session until the original 20-minute deadline. For `redeemed`, read the linked
session and verify its owner, creation type, absence of impersonation, expiry,
deletion state, and the user's current eligibility before reconstructing its
cookie. Never mint a replacement or resurrect a deleted session. After the
deadline, return expiry without a cookie; the already-issued session keeps its
normal lifetime, and the audit outcome remains `redeemed`.
The unique nullable session ID index prevents two challenges from referencing one
session and supports retries without duplicating bearer credentials in this
table. The reference is intentionally signed because the legacy `user_sessions.id`
is signed. Like neighboring schemas, references are indexed logical links rather
than cascading foreign keys; removal of a session must not erase the audit row.
If the referenced session is missing, redemption fails closed.
## Implemented endpoints and clients
All endpoints are JSON POSTs under `/v1/login_challenges`:
| Endpoint | Authentication | Effect |
|------------|---------------------------------------------|--------------------------------------------------------------|
| create | None; limited by requesting IP | Returns device token, verification URL, code, expiry. |
| review | Approval token + web session + Origin | Returns code, requesting IP, account, state, expiry. |
| decide | Approval token + web session + Origin | Records the first Yes/No; never creates a desktop session. |
| poll | Private device token | Reports state or atomically redeems approved consent. |
The API advertises five-second polling. Per-process IP limits are ten creates and
120 other requests per minute, shared across Actix workers, with bounded memory.
Production gateways can additionally enforce aggregate limits across replicas.
A worker expires up to 1,000 abandoned challenges every 30 seconds. Audit retention
is intentionally not an automatic purge policy in this change.
Both browser applications implement `/login/desktop`: `https://getartcraft.com`
(including `www`) and `https://app.getartcraft.com`. They share the approval UI in
`frontend/libs/login`. The API currently advertises the webapp URL. Local API
requests advertise `http://localhost:4201/login/desktop`; the website also handles
the route on port 4200. Run either frontend with `USE_LOCAL_API=1` so approval
reaches the same local API; local origins cannot approve production challenges.
The website signs in inline to preserve its origin-scoped pending token and
mobile session header. The webapp preserves the return destination through login,
signup, and password reset, bypassing billing redirects for desktop consent.
Tokens stay in tab-scoped session storage, are removed from URL fragments before
tracking, and are cleared on terminal results. Returning from authentication
always requires explicit consent. The code stays visible for comparison; a single
Approve or Decline click records the choice, without an extra checkbox.
The desktop displays the QR code, code comparison, countdown, and status. Opening
the website invokes the native system browser. Network errors back off up to 30
seconds; closing the dialog stops polling and cancels the native handle. A response
arriving after cancellation cannot install credentials.
Successful polling returns the signed value and the normal `Set-Cookie` header.
Tauri commands use the Rust `artcraft_client` bindings for creation, polling, and
ordinary session verification, all against `AppEnvConfigs.storyteller_host`. Rust
keeps the private device token and signed session out of IPC/JavaScript; the UI
holds only a local handle and public display data. After verifying the downstream
session, Rust installs and persists the cookie in the native HTTP jar and updates
the credential manager. No browser session is copied into the desktop.
The desktop login/signup modal also uses Rust commands for its initial session
check and password forms. Website/QR buttons appear only in login mode. Unknown
optional feature flags in the session response are ignored by older desktops;
malformed authentication data still fails verification. A stale initial session
check cannot reset a pending or completed login.
## Integration tests
The backend harness uses the fixed local Unix socket `/tmp/mysql.sock` and local
root account. It never reads `DATABASE_URL`, any environment database URL, DNS,
or a TCP endpoint (including localhost SSH forwarding). Each test creates a new
randomly named `artcraft_test_bridge_*` database, replays all migrations, and drops
only its own database on success. Existing `storyteller` and `artcraft_test` data
are not used. A failed test leaves its isolated database for diagnosis. The local
MySQL instance must be running; there is no fallback to a remote database.
The HTTP endpoint suite is
`crates/service/web/storyteller_web/src/http_server/endpoints/login_challenges/endpoint_integration_tests.rs`.
It starts the real challenge handlers and `/v1/session` handler on an ephemeral
`127.0.0.1` port. Separate browser and desktop HTTP clients maintain their own
cookie jars; requests cannot follow redirects or use system proxies. Fixtures in
`mysql_testing::fixtures::login_challenges` create passwordless users, Google email
confirmation, feature flags (including `use_qt`), and signed browser sessions.
The desktop cookie comes only from the real redemption response and is then sent
to the real session endpoint. HTTP responses and SQL queries are not mocked.
From the services repository:
```sh
SQLX_OFFLINE=true cargo test --offline -p storyteller-web --bin storyteller-web login_challenges
cd frontend
npm exec vitest -- run --config apps/artcraft-webapp/vite.config.ts src/pages/login/desktop-login.spec.tsx src/pages/login/login-bridge-continuation.spec.tsx
```
`SQLX_OFFLINE` controls compile-time query metadata only. These tests still execute
real SQL against local MySQL. Do not enable `skip_database_tests` for this run.
To run only the four HTTP endpoint tests with database fixtures:
```sh
SQLX_OFFLINE=true cargo test --offline -p storyteller-web --bin storyteller-web login_challenges::endpoint_integration_tests
```
From the desktop repository:
```sh
SQLX_OFFLINE=true cargo test --offline -p artcraft --lib login_bridge_tests
cd frontend
npm exec vitest -- run --config libs/components/login-modal/vite.config.ts src/lib/DesktopLoginBridge.spec.tsx src/lib/login-modal.spec.tsx
```
Coverage:
- Four HTTP endpoint integration tests with database fixtures: the complete
create/review/approve/redeem/session flow, independent browser/desktop cookies,
passwordless onboarding and `use_qt` serialization, concurrent redemption,
lost-response recovery with a fresh client, no sessions before redemption,
audit ownership/IPs/timestamps, decline, pending/approved expiry, downstream
session revocation/expiry/bans, and immutable approval ownership across accounts.
- Seven additional backend handler integration tests: both production browser origins, passwordless account approval, zero sessions
before redemption, rejection/audit IPs, token separation, CSRF, MCP rejection,
invalid approving sessions, concurrent decisions/redemptions, response-loss
retries, abandoned expiry, expiry during lock contention, revocation, bans,
signed-cookie authentication through the production session guard, and an issued
session remaining valid after the challenge expires.
- Twenty-five browser tests: API/consent interaction, code/IP display, decline,
expiry, terminal/unknown states, lost-response retries, safe return URLs,
website inline Google/password sign-in, and webapp login/signup/password-reset
continuation without subscription gating or bridge credentials in SSO requests.
- Thirteen desktop UI tests: IPC-only transport (JavaScript HTTP is forbidden),
native browser invocation, QR/code values, polling, verified user results,
decline, timeout, network backoff, cancellation, host/status diagnostics,
login-only button placement, native password forms, and stale session checks.
- Thirteen native tests: real API bindings against ephemeral loopback HTTP servers,
downstream verification before cookie installation, persistence, retries,
rejection, timeout, cancellation during HTTP, unknown states, no redirects,
redacted errors, local/production origin isolation, visitor-only telemetry,
forward-compatible feature flags, malformed session data, and native password
login/signup/session recheck. Desktop tests do not connect to MySQL.
Frontend tests replace external Google/native boundaries; they do not
perform a live Google OAuth exchange or drive a packaged Tauri GUI. Backend tests
use real MySQL and the production cookie signer/session guard with Redis disabled;
the HTTP suite also exercises the real session endpoint and its serialized body.
These endpoint tests do not drive a live Google OAuth exchange or the Tauri GUI.
Existing session-cache TTL behavior is unchanged. SQL lookups now reject expired
sessions; bridge consent/redemption always checks MySQL directly.
Validation also covered both migration directions, nullable compatibility with
legacy session writers, uniqueness constraints, fail-closed expiry defaults, and
matching materialized schemas. The focused web TypeScript check and the production
builds of both browser apps (including 53 dependency tasks) passed. Broad workspace TypeScript
checks have existing errors. The desktop Nx build is blocked by the existing
`api -> tauri-api -> api` cycle; its direct Vite login bundle builds but declaration
checks report unavailable dependency outputs. A source-only desktop check reports
four existing `model-list` imports of `src/index.js`, with no bridge-file errors.
The full native Rust test target compiled successfully.
+2 -2
View File
@@ -35,6 +35,6 @@ VITE_USE_LOCAL_API=true npx nx dev artcraft-webapp
The web app runs at `http://localhost:4201`, using the local backend. To develop
against the hosted API, omit `VITE_USE_LOCAL_API=true`.
For the product website, run `npx nx dev artcraft-website` from `frontend`; it uses
`http://localhost:4200`. Repository-root launchers live under [`script/website`](../script/website).
For the product website, run `./script/website/unix_frontend_website_dev.sh` from the
repository root; it is a standalone Next.js app on `http://localhost:3000`. Repository-root launchers live under [`script/website`](../script/website).
See the [frontend README](../frontend/README.md) for builds and troubleshooting.
@@ -0,0 +1,30 @@
use serde_derive::{Deserialize, Serialize};
use url::Url;
use utoipa::ToSchema;
use crate::common::responses::cover_image_links::CoverImageLinks;
use crate::common::responses::media_file_cover_image_details::MediaFileDefaultCover;
// User-media list endpoints retain the deprecated bucket fields for compatibility.
/// Everything we need to create a cover image.
/// Cover images are small descriptive images that can be set for any media file.
/// If a cover image is set, this is the path to the asset.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct LegacyMediaFileCoverImageDetails {
/// (DEPRECATED) URL path to the media file
#[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
pub maybe_cover_image_public_bucket_path: Option<String>,
/// (DEPRECATED) Full URL to the media file
#[deprecated(note="This points to the bucket. Use media_links instead to leverage the CDN.")]
pub maybe_cover_image_public_bucket_url: Option<Url>,
/// Links to the cover image (CDN direct link, thumbnail template)
/// If a cover image is set, this is the path to the asset.
/// If a cover image is not set, use the information in `default_cover` instead.
/// Rich CDN links to the media, including thumbnails, previews, and more.
pub maybe_links: Option<CoverImageLinks>,
/// For items without a cover image, we can use one of our own.
pub default_cover: MediaFileDefaultCover,
}
@@ -0,0 +1,44 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use tokens::tokens::model_weights::ModelWeightToken;
/// Fields useful for enriching media file listings
#[derive(Clone, Serialize, Deserialize, ToSchema)]
pub struct MediaFileOriginDetails {
/// Where the file came from (broadly)
pub origin_category: MediaFileOriginCategory,
/// Where the file came from (specifically, a product area, eg. "face_animator")
pub product_category: MediaFileOriginProductCategory,
/// If the file was produced by a model, the details.
pub maybe_model: Option<MediaFileModelDetails>,
}
/// Information about the model weights
/// https://serde.rs/enum-representations.html#untagged
#[derive(Clone, Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
pub enum MediaFileModelDetails {
// Try the specific shape first so deserialization retains token and title.
/// A model weight is typically a user-submitted model.
/// We store lots of these.
ModelWeight {
/// The type of model weight
model_type: MediaFileOriginModelType,
/// The model token
token: ModelWeightToken,
/// The model title (typically only populated for `model_weights` models, not legacy tables such as `tts_models`.)
title: String,
},
/// A system model is one that we maintain and update,
/// eg. SadTalker, Wav2Lip, MocapNet, etc.
SystemModel {
/// The type of model weight
model_type: MediaFileOriginModelType,
},
}
@@ -15,7 +15,8 @@ pub struct MediaLinks {
/// video previews, which, in turn, have their own thumbnail templates.)
pub maybe_thumbnail_template: Option<String>,
/// Video preview images (still and animated gif) for mp4 video files.
/// Video preview images (still, animated webp or gif, and a Catalina gif
/// fallback) for mp4 video files.
/// These are only set for video media files.
/// These are not set for "cover images" that are gif or webms, but rather video files.
pub maybe_video_previews: Option<VideoPreviews>,
@@ -26,7 +27,9 @@ pub struct VideoPreviews {
/// A static single frame preview image of the video.
pub still: Url,
/// An animated gif preview of the video.
/// An animated preview of the video: an animated webp for videos with
/// thumbnail version 2 (2026-09+), otherwise a gif. Clients that can't
/// render animated webp should use `maybe_catalina_fallback_animated_gif`.
pub animated: Url,
/// A template used to construct the still thumbnail URL.
@@ -36,4 +39,21 @@ pub struct VideoPreviews {
/// A template used to construct the animated thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
pub animated_thumbnail_template: String,
/// An animated gif preview of the video, for clients that can't render the
/// webp in `animated`: ArtCraft desktop on macOS 10.15 (Catalina) runs on a
/// WKWebView without animated webp support. Since thumbnail version 2
/// (2026-09) the thumbnail job writes this gif beside the webp; for older
/// videos it's the same file as `animated`. See `video_thumbnail_suffixes`
/// in `bucket_paths` for the version history.
///
/// Always set by the server. Optional (and defaulted) only so newer clients
/// can still read responses from older servers.
#[serde(default)]
pub maybe_catalina_fallback_animated_gif: Option<Url>,
/// A template used to construct the Catalina fallback gif thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
#[serde(default)]
pub maybe_catalina_fallback_animated_gif_thumbnail_template: Option<String>,
}
@@ -1,8 +1,10 @@
pub mod cover_image_links;
pub mod job_details;
pub mod legacy_media_file_cover_image_details;
pub mod media_file_cover_image_details;
pub mod media_file_origin_details;
pub mod media_links;
pub mod pagination_cursors;
pub mod simple_entity_stats;
pub mod simple_generic_json_success;
pub mod user_details_light;
pub mod user_details_light;
@@ -1,9 +1,10 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
// TODO(bt,2025-08-20): Replace the storyteller-web version of this.
/// Simple stats that can be attached to any entity
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct SimpleEntityStats {
/// Number of positive ratings (or "likes") for this item
pub positive_rating_count: u32,
@@ -68,7 +68,8 @@ pub struct FolderThumbnail {
/// which, in turn, have their own thumbnail templates.)
pub maybe_thumbnail_template: Option<String>,
/// Video preview images (still and animated gif) for mp4 video files.
/// Video preview images (still, animated webp or gif, and a Catalina gif
/// fallback) for mp4 video files.
/// These are only set for video media files.
pub maybe_video_previews: Option<FolderThumbnailVideoPreviews>,
}
@@ -81,7 +82,9 @@ pub struct FolderThumbnailVideoPreviews {
/// A static single frame preview image of the video.
pub still: Url,
/// An animated gif preview of the video.
/// An animated preview of the video: an animated webp for videos with
/// thumbnail version 2 (2026-09+), otherwise a gif. Clients that can't
/// render animated webp should use `maybe_catalina_fallback_animated_gif`.
pub animated: Url,
/// A template used to construct the still thumbnail URL.
@@ -91,4 +94,21 @@ pub struct FolderThumbnailVideoPreviews {
/// A template used to construct the animated thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
pub animated_thumbnail_template: String,
/// An animated gif preview of the video, for clients that can't render the
/// webp in `animated`: ArtCraft desktop on macOS 10.15 (Catalina) runs on a
/// WKWebView without animated webp support. Since thumbnail version 2
/// (2026-09) the thumbnail job writes this gif beside the webp; for older
/// videos it's the same file as `animated`. See `video_thumbnail_suffixes`
/// in `bucket_paths` for the version history.
///
/// Always set by the server. Optional (and defaulted) only so newer clients
/// can still read responses from older servers.
#[serde(default)]
pub maybe_catalina_fallback_animated_gif: Option<Url>,
/// A template used to construct the Catalina fallback gif thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
#[serde(default)]
pub maybe_catalina_fallback_animated_gif_thumbnail_template: Option<String>,
}
@@ -0,0 +1,67 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use crate::media_file::list::user_media_file_list_item::MediaFileForUserListItem;
pub const LIST_MEDIA_FILES_FOR_USER_V2_URL_PATH: &str = "/v1/media_files/list_v2/user/{username}";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ListMediaFilesForUserV2PathInfo {
pub username: String,
}
#[derive(Serialize, Deserialize, ToSchema, IntoParams)]
pub struct ListMediaFilesForUserV2QueryParams {
pub sort_ascending: Option<bool>,
pub page_size: Option<usize>,
pub page_index: Option<usize>,
/// NB: This can be one (or more comma-separated values) from `MediaFileClass`,
/// which are the broad category of media files: image, video, etc.
///
/// Usage:
/// - `?filter_media_classes=audio`
/// - `?filter_media_classes=image,video`
/// - `?filter_media_classes=dimensional`
/// - etc.
pub filter_media_classes: Option<String>,
/// NB: This can be one (or more comma-separated values) from `MediaFileType`,
/// which are mimetype-like / format-like categories of media files: glb, gltf,
/// scene_json, jpg, png, mp4, wav, etc.
///
/// Usage:
/// - `?filter_media_type=scene_json`
/// - `?filter_media_type=glb,gltf,fbx`
/// - `?filter_media_type=pmd,vmd,pmx`
/// - `?filter_media_type=jpg,png,gif`
/// - `?filter_media_type=wav,mp3`
/// - `?filter_media_type=mp4`
/// - etc.
pub filter_media_type: Option<String>,
/// NB: This can be one (or more comma-separated values) from `MediaFileEngineCategory`.
///
/// Usage:
/// - `?filter_engine_categories=scene`
/// - `?filter_engine_categories=animation,character,object`
/// - etc.
pub filter_engine_categories: Option<String>,
/// Include user uploaded files in the results.
/// By default, we do not return them unless this flag is set to true.
pub include_user_uploads: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ListMediaFilesForUserV2SuccessResponse {
pub success: bool,
pub results: Vec<MediaFileForUserListItem>,
pub pagination: UserMediaFileV2Pagination,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct UserMediaFileV2Pagination {
pub current: usize,
pub has_more: bool,
}
@@ -1 +1,3 @@
pub mod by_type;
pub mod list_media_files_for_user_v2;
pub mod user_media_file_list_item;
@@ -0,0 +1,106 @@
use chrono::{DateTime, Utc};
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use utoipa::ToSchema;
use crate::common::responses::legacy_media_file_cover_image_details::LegacyMediaFileCoverImageDetails;
use crate::common::responses::media_file_origin_details::MediaFileOriginDetails;
use crate::common::responses::media_links::MediaLinks;
use crate::common::responses::simple_entity_stats::SimpleEntityStats;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct MediaFileForUserListItem {
pub token: MediaFileToken,
/// The coarse-grained class of media file: image, video, etc.
pub media_class: MediaFileClass,
/// Type of media will dictate which fields are populated and what
/// the frontend should display (eg. video player vs audio player).
/// This is closer in meaning to a "mime type".
pub media_type: MediaFileType,
/// If this is an engine/3D asset, this is the broad category (scene,
/// animation, etc.) of that object.
/// This can also be used for filtering in list/batch endpoints.
pub maybe_engine_category: Option<MediaFileEngineCategory>,
/// If this is an engine/3D asset for an animation or a rig that can
/// be animated with either (or both) skeletal or blend shape animations,
/// this describes the animation regime used or supported.
pub maybe_animation_type: Option<MediaFileAnimationType>,
/// Details where the media file came from.
pub origin: MediaFileOriginDetails,
#[deprecated(note="Use MediaFileOriginDetails instead")]
pub origin_category: MediaFileOriginCategory,
#[deprecated(note="Use MediaFileOriginDetails instead")]
pub origin_product_category: MediaFileOriginProductCategory,
#[deprecated(note="Use MediaFileOriginDetails instead")]
pub maybe_origin_model_type: Option<MediaFileOriginModelType>,
#[deprecated(note="Use MediaFileOriginDetails instead")]
pub maybe_origin_model_token: Option<String>,
/// (DEPRECATED) URL path to the media file
#[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
pub public_bucket_path: String,
/// (DEPRECATED) Full URL to the media file
#[deprecated(note="This points to the bucket. Use media_links instead to leverage the CDN.")]
pub public_bucket_url: String,
/// Rich CDN links to the media, including thumbnails, previews, and more.
pub media_links: MediaLinks,
/// The foreign key to the prompt used to generate the media, if applicable.
pub maybe_prompt_token: Option<PromptToken>,
/// Information about the cover image. Many media files do not require a cover image,
/// e.g. image files, video files with thumbnails, audio files, etc.
/// 3D files require them.
pub cover_image: LegacyMediaFileCoverImageDetails,
pub creator_set_visibility: Visibility,
/// The file was uploaded by the user.
/// This does not include files generated on the client side, like studio renders.
pub is_user_upload: bool,
/// The file was created by the system.
/// This includes files generated on the client side, like studio renders.
pub is_intermediate_system_file: bool,
/// The name or title of the media file (optional)
pub maybe_title: Option<String>,
/// Text transcripts for TTS, etc.
pub maybe_text_transcript: Option<String>,
/// For Comfy / Video Style Transfer jobs, this might include
/// the name of the selected style.
pub maybe_style_name: Option<StyleTransferName>,
/// Duration for audio and video files, if available.
/// Measured in milliseconds.
pub maybe_duration_millis: Option<u64>,
/// Statistics about the media file
pub stats: SimpleEntityStats,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -6,6 +6,7 @@ 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 enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -79,6 +80,11 @@ pub struct OmniApiVideoGenerateRequest {
/// Not all models support this; models that don't simply ignore it.
pub bitrate: Option<CommonBitrate>,
/// Output container for Seedance 2.5 (including Ultra). Other models ignore it.
/// Use "mp4" or "mov" (QuickTime). Omitted or null defaults to MP4.
#[serde(rename = "output_format", skip_serializing_if = "Option::is_none")]
pub maybe_output_format: Option<CommonVideoOutputFormat>,
/// The quality to use.
pub quality: Option<CommonQuality>,
@@ -6,6 +6,7 @@ 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 enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -55,6 +56,11 @@ pub struct OmniGenVideoCostAndGenerateRequest {
/// Not all models support this; models that don't simply ignore it.
pub bitrate: Option<CommonBitrate>,
/// Output container for Seedance 2.5 (including Ultra). Other models ignore it.
/// Use "mp4" or "mov" (QuickTime). Omitted or null defaults to MP4.
#[serde(rename = "output_format", skip_serializing_if = "Option::is_none")]
pub maybe_output_format: Option<CommonVideoOutputFormat>,
/// The quality to use.
pub quality: Option<CommonQuality>,
@@ -2,6 +2,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
use enums::common::generation::model_creator::ModelCreator;
use enums::common::generation_provider::GenerationProvider;
use serde_derive::{Deserialize, Serialize};
@@ -156,6 +157,12 @@ pub struct OmniGenVideoModelDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub bitrate_default: Option<CommonBitrate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format_options: Option<Vec<CommonVideoOutputFormat>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format_default: Option<CommonVideoOutputFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality_options: Option<Vec<CommonQuality>>,
@@ -229,6 +236,8 @@ impl Default for OmniGenVideoModelDetails {
resolution_default: None,
bitrate_options: None,
bitrate_default: None,
output_format_options: None,
output_format_default: None,
quality_options: None,
default_quality: None,
duration_seconds_min: None,
@@ -0,0 +1,73 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Deserialize, Serialize, ToSchema)]
pub struct CreateLoginChallengeRequest {}
#[derive(Deserialize, Serialize, ToSchema)]
pub struct ReviewLoginChallengeRequest {
pub approval_token: String,
}
#[derive(Deserialize, Serialize, ToSchema)]
pub struct DecideLoginChallengeRequest {
pub approval_token: String,
pub approve: bool,
}
#[derive(Deserialize, Serialize, ToSchema)]
pub struct PollLoginChallengeRequest {
pub device_token: String,
}
#[derive(Deserialize, Serialize, ToSchema)]
pub struct CreateLoginChallengeResponse {
pub success: bool,
pub device_token: String,
pub verification_url: String,
pub confirmation_code: String,
pub expires_at: DateTime<Utc>,
pub poll_interval_seconds: u32,
}
#[derive(Deserialize, Serialize, ToSchema)]
pub struct ReviewLoginChallengeResponse {
pub success: bool,
pub status: LoginChallengeState,
pub maybe_failure_type: Option<LoginChallengeFailure>,
pub confirmation_code: String,
pub requesting_ip: String,
pub expires_at: DateTime<Utc>,
pub username: String,
}
#[derive(Deserialize, Serialize, ToSchema)]
pub struct LoginChallengeResponse {
pub success: bool,
pub status: LoginChallengeState,
pub maybe_failure_type: Option<LoginChallengeFailure>,
/// Present only to the device after the redemption transaction commits.
#[serde(skip_serializing_if = "Option::is_none")]
pub maybe_signed_session: Option<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum LoginChallengeState {
Pending,
Approved,
Redeemed,
Failed,
#[serde(other)]
Unknown,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum LoginChallengeFailure {
UserDeclined,
Expired,
#[serde(other)]
Unknown,
}
@@ -2,4 +2,5 @@ pub mod change_password;
pub mod edit_email;
pub mod edit_username;
pub mod login;
pub mod session_info;
pub mod login_challenges;
pub mod session_info;
@@ -8,6 +8,9 @@ use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
pub enum ApiClientQuality {
Auto,
Max,
XHigh,
High,
Medium,
Low,
@@ -18,6 +21,9 @@ pub enum ApiClientQuality {
impl From<String> for ApiClientQuality {
fn from(value: String) -> Self {
match value.as_str() {
"auto" => Self::Auto,
"max" => Self::Max,
"xhigh" => Self::XHigh,
"high" => Self::High,
"medium" => Self::Medium,
"low" => Self::Low,
@@ -29,6 +35,9 @@ impl From<String> for ApiClientQuality {
impl From<ApiClientQuality> for String {
fn from(value: ApiClientQuality) -> Self {
match value {
ApiClientQuality::Auto => "auto".to_string(),
ApiClientQuality::Max => "max".to_string(),
ApiClientQuality::XHigh => "xhigh".to_string(),
ApiClientQuality::High => "high".to_string(),
ApiClientQuality::Medium => "medium".to_string(),
ApiClientQuality::Low => "low".to_string(),
@@ -58,6 +67,9 @@ mod tests {
#[test]
fn every_known_variant_round_trips() {
let all = [
(ApiClientQuality::Auto, "auto"),
(ApiClientQuality::Max, "max"),
(ApiClientQuality::XHigh, "xhigh"),
(ApiClientQuality::High, "high"),
(ApiClientQuality::Medium, "medium"),
(ApiClientQuality::Low, "low"),
@@ -41,7 +41,7 @@ pub async fn upload_media_file_from_file<P: AsRef<Path>>(
Err(err) => Err(StorytellerError::Api(err)),
}
}
Some(FileExtension::Mp4) => {
Some(FileExtension::Mp4 | FileExtension::Mov) => {
match upload_video_media_file_from_file(UploadVideoFromFileArgs {
api_host,
maybe_creds,
@@ -13,5 +13,6 @@ pub mod router_quality;
pub mod router_resolution;
pub mod router_splat_model;
pub mod router_video_model;
pub mod router_video_output_format;
pub mod video_list_ref;
pub mod video_ref;
@@ -1,10 +1,20 @@
use serde_derive::{Deserialize, Serialize};
/// Common quality levels you can specify when enqueuing a generation.
/// Not every model will use this; models that don't simply ignore it.
/// Not every model will use this; models that don't simply ignore it, and models that only
/// offer a subset of tiers clamp to the nearest tier they support.
///
/// Serialized names mirror the server's `CommonQuality` so the two convert via serde.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RouterQuality {
/// Let the model choose. Models without a native "auto" use their default tier.
Auto,
/// Highest tier (GPT Image 2.5 `max`).
Max,
/// Between High and Max (GPT Image 2.5 `xhigh`).
#[serde(rename = "xhigh")]
XHigh,
High,
Medium,
Low,
@@ -97,6 +97,12 @@ pub enum RouterVideoModel {
#[serde(rename = "happy_horse_1p0")]
HappyHorse1p0,
#[serde(rename = "wan_3p0")]
Wan3p0,
#[serde(rename = "wan_3p0_prime")]
Wan3p0Prime,
#[serde(rename = "sora_2")]
Sora2,
@@ -0,0 +1,10 @@
use serde_derive::{Deserialize, Serialize};
/// Output video container. Supported by Seedance 2.5 on Kinovi and Artcraft.
/// Other models/providers ignore this option.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RouterVideoOutputFormat {
Mp4,
Mov,
}
@@ -151,8 +151,11 @@ fn to_resolution_enum(r: RouterResolution) -> CommonResolutionEnum {
fn to_quality_enum(q: RouterQuality) -> CommonQualityEnum {
match q {
RouterQuality::Low => CommonQualityEnum::Low,
RouterQuality::Medium => CommonQualityEnum::Medium,
RouterQuality::Auto => CommonQualityEnum::Auto,
RouterQuality::Max => CommonQualityEnum::Max,
RouterQuality::XHigh => CommonQualityEnum::XHigh,
RouterQuality::High => CommonQualityEnum::High,
RouterQuality::Medium => CommonQualityEnum::Medium,
RouterQuality::Low => CommonQualityEnum::Low,
}
}
@@ -36,7 +36,7 @@ impl ArtcraftGptImage1CostState {
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
let quality = clamp_to_supported_quality(self.quality);
let is_square = is_square_or_none(self.aspect_ratio);
let output_cost_per_image: u64 = match (quality, is_square) {
@@ -45,6 +45,8 @@ impl ArtcraftGptImage1CostState {
(CommonQualityEnum::Medium, false) => 7,
(CommonQualityEnum::High, true) => 17,
(CommonQualityEnum::High, false) => 25,
// Clamped away above.
(CommonQualityEnum::Auto | CommonQualityEnum::XHigh | CommonQualityEnum::Max, _) => unreachable!(),
};
let output_cost = output_cost_per_image * self.num_images as u64;
@@ -63,6 +65,20 @@ impl ArtcraftGptImage1CostState {
}
}
/// GPT Image 1 only offers low / medium / high. `auto` prices as the default (high) and the
/// higher GPT Image 2.5 tiers clamp down to high, matching the Fal request planner.
fn clamp_to_supported_quality(quality: Option<CommonQualityEnum>) -> CommonQualityEnum {
match quality {
Some(CommonQualityEnum::Low) => CommonQualityEnum::Low,
Some(CommonQualityEnum::Medium) => CommonQualityEnum::Medium,
Some(CommonQualityEnum::High)
| Some(CommonQualityEnum::XHigh)
| Some(CommonQualityEnum::Max)
| Some(CommonQualityEnum::Auto)
| None => CommonQualityEnum::High,
}
}
fn is_square_or_none(aspect_ratio: Option<CommonAspectRatioEnum>) -> bool {
use CommonAspectRatioEnum as Ar;
matches!(
@@ -37,7 +37,7 @@ impl ArtcraftGptImage1p5CostState {
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
let quality = clamp_to_supported_quality(self.quality);
let size = size_bucket(self.aspect_ratio);
let cost_per_image: u64 = match (quality, size) {
@@ -50,6 +50,8 @@ impl ArtcraftGptImage1p5CostState {
(CommonQualityEnum::High, SizeBucket::Square) => 14,
(CommonQualityEnum::High, _) => 20,
// Clamped away above.
(CommonQualityEnum::Auto | CommonQualityEnum::XHigh | CommonQualityEnum::Max, _) => unreachable!(),
};
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
@@ -65,6 +67,20 @@ impl ArtcraftGptImage1p5CostState {
}
}
/// GPT Image 1.5 only offers low / medium / high. `auto` prices as the default (high) and the
/// higher GPT Image 2.5 tiers clamp down to high, matching the Fal request planner.
fn clamp_to_supported_quality(quality: Option<CommonQualityEnum>) -> CommonQualityEnum {
match quality {
Some(CommonQualityEnum::Low) => CommonQualityEnum::Low,
Some(CommonQualityEnum::Medium) => CommonQualityEnum::Medium,
Some(CommonQualityEnum::High)
| Some(CommonQualityEnum::XHigh)
| Some(CommonQualityEnum::Max)
| Some(CommonQualityEnum::Auto)
| None => CommonQualityEnum::High,
}
}
fn size_bucket(aspect_ratio: Option<CommonAspectRatioEnum>) -> SizeBucket {
use CommonAspectRatioEnum as Ar;
match aspect_ratio {
@@ -42,7 +42,7 @@ impl ArtcraftGptImage2CostState {
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
let quality = clamp_to_supported_quality(self.quality);
let size = size_bucket(self.aspect_ratio);
let cost_per_image: u64 = match (quality, size) {
@@ -62,6 +62,8 @@ impl ArtcraftGptImage2CostState {
| (CommonQualityEnum::High, SizeBucket::Portrait4x3) => 15,
(CommonQualityEnum::High, SizeBucket::Landscape16x9)
| (CommonQualityEnum::High, SizeBucket::Portrait16x9) => 16,
// Clamped away above.
(CommonQualityEnum::Auto | CommonQualityEnum::XHigh | CommonQualityEnum::Max, _) => unreachable!(),
};
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
@@ -79,6 +81,20 @@ impl ArtcraftGptImage2CostState {
/// Mirrors v1's `plan_image_size` for GPT Image 2 — see the v1 file
/// `plan_generate_image_artcraft_gpt_image_2.rs::plan_image_size`.
/// GPT Image 2 only offers low / medium / high. `auto` prices as the default (high) and the
/// higher GPT Image 2.5 tiers clamp down to high, matching the Fal request planner.
fn clamp_to_supported_quality(quality: Option<CommonQualityEnum>) -> CommonQualityEnum {
match quality {
Some(CommonQualityEnum::Low) => CommonQualityEnum::Low,
Some(CommonQualityEnum::Medium) => CommonQualityEnum::Medium,
Some(CommonQualityEnum::High)
| Some(CommonQualityEnum::XHigh)
| Some(CommonQualityEnum::Max)
| Some(CommonQualityEnum::Auto)
| None => CommonQualityEnum::High,
}
}
fn size_bucket(aspect_ratio: Option<CommonAspectRatioEnum>) -> SizeBucket {
use CommonAspectRatioEnum as Ar;
match aspect_ratio {
@@ -8,25 +8,27 @@ use fal_client::requests::api::image::common::gpt_image_2p5::gpt_image_2p5_image
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
/// Artcraft price per output image, in hundredths of a US cent, by output dimensions and
/// quality (`[low, medium, high]`). Rows are stored landscape-first; portrait orientations of
/// the same dimensions price identically. Dimensions come from the aspect ratio preset and
/// resolution tier the same way the Fal request builder derives them.
const OUTPUT_IMAGE_PRICE_TABLE: &[(u32, u32, [u64; 3])] = &[
/// quality (`[low, medium, high, xhigh, max]`). Each cell is the `fal_client` GPT Image 2.5
/// estimate for that size and tier with a 10% markup, rounded up. Rows are stored
/// landscape-first; portrait orientations of the same dimensions price identically.
/// Dimensions come from the aspect ratio preset and resolution tier the same way the Fal
/// request builder derives them.
const OUTPUT_IMAGE_PRICE_TABLE: &[(u32, u32, [u64; 5])] = &[
// 1:1
(1024, 1024, [ 65, 152, 582]),
(2048, 2048, [103, 239, 919]),
(2880, 2880, [151, 352, 1357]),
(1024, 1024, [ 65, 152, 582, 1034, 2327]),
(2048, 2048, [103, 239, 919, 1633, 3672]),
(2880, 2880, [151, 352, 1357, 2412, 5426]),
// 4:3
(1024, 768, [ 46, 105, 400]),
(2048, 1536, [ 73, 170, 652]),
(3072, 2304, [120, 279, 1073]),
(3312, 2480, [134, 311, 1194]),
(1024, 768, [ 46, 105, 400, 710, 1597]),
(2048, 1536, [ 73, 170, 652, 1159, 2605]),
(3072, 2304, [120, 279, 1073, 1906, 4287]),
(3312, 2480, [134, 311, 1194, 2122, 4773]),
// 16:9
(1088, 608, [ 32, 75, 285]),
(1920, 1080, [ 49, 114, 436]),
(2048, 1152, [ 53, 121, 467]),
(3072, 1728, [ 87, 204, 783]),
(3840, 2160, [124, 286, 1102]),
(1088, 608, [ 32, 75, 285, 506, 1139]),
(1920, 1080, [ 49, 114, 436, 775, 1743]),
(2048, 1152, [ 53, 121, 467, 830, 1865]),
(3072, 1728, [ 87, 204, 783, 1390, 3127]),
(3840, 2160, [124, 286, 1102, 1957, 4404]),
];
/// Artcraft price per reference image on an edit request, in hundredths of a US cent.
@@ -55,7 +57,8 @@ impl ArtcraftGptImage2p5CostInputs {
}
/// Per-image price rounded up to whole cents, times the batch size. Quality defaults to
/// high. Without an aspect ratio, text-to-image prices as landscape 4:3 and edit as auto,
/// high, and `auto` is priced as high (the documented default the model falls back to).
/// Without an aspect ratio, text-to-image prices as landscape 4:3 and edit as auto,
/// matching the Fal defaults the request would fall back to.
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
@@ -91,11 +94,13 @@ fn output_image_price(quality: CommonQualityEnum, width: u32, height: u32) -> u6
match quality {
CommonQualityEnum::Low => prices[0],
CommonQualityEnum::Medium => prices[1],
CommonQualityEnum::High => prices[2],
CommonQualityEnum::High | CommonQualityEnum::Auto => prices[2],
CommonQualityEnum::XHigh => prices[3],
CommonQualityEnum::Max => prices[4],
}
}
fn lookup_price_row(width: u32, height: u32) -> Option<[u64; 3]> {
fn lookup_price_row(width: u32, height: u32) -> Option<[u64; 5]> {
let long_edge = width.max(height);
let short_edge = width.min(height);
OUTPUT_IMAGE_PRICE_TABLE.iter()
@@ -176,6 +181,18 @@ mod tests {
(Some(Ar::Auto4k), Some(R::OneK), [2, 3, 12]),
];
// (aspect_ratio, resolution, [xhigh, max]) — cents per image, text-to-image
const TEXT_GRID_UPPER_TIERS: &[(Option<Ar>, Option<R>, [u64; 2])] = &[
(None, None, [ 8, 16]), // landscape 4:3 default
(Some(Ar::Square), None, [11, 24]),
(Some(Ar::Square), Some(R::TwoK), [17, 37]),
(Some(Ar::Square), Some(R::FourK), [25, 55]),
(Some(Ar::WideSixteenByNine), None, [ 8, 18]),
(Some(Ar::WideSixteenByNine), Some(R::OneK), [ 6, 12]),
(Some(Ar::TallNineBySixteen), Some(R::FourK), [20, 45]),
(Some(Ar::Auto), None, [20, 45]),
];
mod text_to_image_tests {
use super::*;
@@ -189,11 +206,32 @@ mod tests {
}
}
#[test]
fn upper_tier_grid() {
for &(aspect_ratio, resolution, expected) in TEXT_GRID_UPPER_TIERS {
for (quality, expected) in [Q::XHigh, Q::Max].into_iter().zip(expected) {
let actual = cents(make_inputs(0, Some(quality), aspect_ratio, resolution, 1));
assert_eq!(actual, expected, "{aspect_ratio:?} {resolution:?} {quality:?}");
}
}
}
#[test]
fn quality_defaults_to_high() {
assert_eq!(cents(make_inputs(0, None, Some(Ar::Square), None, 1)), 6);
}
#[test]
fn auto_prices_as_high() {
for &(aspect_ratio, resolution, _) in TEXT_GRID {
assert_eq!(
cents(make_inputs(0, Some(Q::Auto), aspect_ratio, resolution, 1)),
cents(make_inputs(0, Some(Q::High), aspect_ratio, resolution, 1)),
"{aspect_ratio:?} {resolution:?}",
);
}
}
#[test]
fn batch_multiplies_per_image_cents() {
for &(aspect_ratio, resolution, _) in TEXT_GRID {
@@ -265,7 +303,7 @@ mod tests {
#[test]
fn prices_rise_with_quality_and_size() {
for &(_, _, prices) in OUTPUT_IMAGE_PRICE_TABLE {
assert!(prices[0] < prices[1] && prices[1] < prices[2], "{prices:?}");
assert!(prices.windows(2).all(|pair| pair[0] < pair[1]), "{prices:?}");
}
assert!(output_image_price(Q::High, 1024, 1024) < output_image_price(Q::High, 2048, 2048));
assert!(output_image_price(Q::High, 2048, 2048) < output_image_price(Q::High, 2880, 2880));
@@ -100,11 +100,17 @@ fn plan_num_images(
}
}
/// This endpoint only offers low / medium / high. `auto` falls back to Fal's default (`high`)
/// and the higher GPT Image 2.5 tiers (`xhigh`, `max`) clamp down to `high`.
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
match quality {
Some(RouterQuality::Low) => PlannedQuality::Low,
Some(RouterQuality::Medium) => PlannedQuality::Medium,
Some(RouterQuality::High) | None => PlannedQuality::High,
Some(RouterQuality::High)
| Some(RouterQuality::XHigh)
| Some(RouterQuality::Max)
| Some(RouterQuality::Auto)
| None => PlannedQuality::High,
}
}
@@ -337,6 +343,10 @@ mod tests {
(Some(RouterQuality::Low), "Some(Low)"),
(Some(RouterQuality::Medium), "Some(Medium)"),
(Some(RouterQuality::High), "Some(High)"),
// Tiers this endpoint lacks clamp to high.
(Some(RouterQuality::Auto), "Some(High)"),
(Some(RouterQuality::XHigh), "Some(High)"),
(Some(RouterQuality::Max), "Some(High)"),
];
for (quality, expected) in cases {
@@ -100,11 +100,17 @@ fn plan_num_images(
}
}
/// This endpoint only offers low / medium / high. `auto` falls back to Fal's default (`high`)
/// and the higher GPT Image 2.5 tiers (`xhigh`, `max`) clamp down to `high`.
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
match quality {
Some(RouterQuality::Low) => PlannedQuality::Low,
Some(RouterQuality::Medium) => PlannedQuality::Medium,
Some(RouterQuality::High) | None => PlannedQuality::High,
Some(RouterQuality::High)
| Some(RouterQuality::XHigh)
| Some(RouterQuality::Max)
| Some(RouterQuality::Auto)
| None => PlannedQuality::High,
}
}
@@ -337,6 +343,10 @@ mod tests {
(Some(RouterQuality::Low), "Some(Low)"),
(Some(RouterQuality::Medium), "Some(Medium)"),
(Some(RouterQuality::High), "Some(High)"),
// Tiers this endpoint lacks clamp to high.
(Some(RouterQuality::Auto), "Some(High)"),
(Some(RouterQuality::XHigh), "Some(High)"),
(Some(RouterQuality::Max), "Some(High)"),
];
for (quality, expected) in cases {
@@ -106,11 +106,17 @@ fn plan_num_images(
}
}
/// This endpoint only offers low / medium / high. `auto` falls back to Fal's default (`high`)
/// and the higher GPT Image 2.5 tiers (`xhigh`, `max`) clamp down to `high`.
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
match quality {
Some(RouterQuality::Low) => PlannedQuality::Low,
Some(RouterQuality::Medium) => PlannedQuality::Medium,
Some(RouterQuality::High) | None => PlannedQuality::High,
Some(RouterQuality::High)
| Some(RouterQuality::XHigh)
| Some(RouterQuality::Max)
| Some(RouterQuality::Auto)
| None => PlannedQuality::High,
}
}
@@ -362,6 +368,10 @@ mod tests {
(Some(RouterQuality::Low), "Some(Low)"),
(Some(RouterQuality::Medium), "Some(Medium)"),
(Some(RouterQuality::High), "Some(High)"),
// Tiers this endpoint lacks clamp to high.
(Some(RouterQuality::Auto), "Some(High)"),
(Some(RouterQuality::XHigh), "Some(High)"),
(Some(RouterQuality::Max), "Some(High)"),
];
for (quality, expected) in cases {
@@ -82,13 +82,16 @@ fn plan_num_images(
}
}
/// The router only exposes low / medium / high; GPT Image 2.5's `xhigh` and `max` tiers are
/// not reachable through it. Fal's default (`high`) applies when unspecified.
/// GPT Image 2.5 supports every router tier natively. Fal's default (`high`) applies when
/// unspecified.
fn plan_quality(quality: Option<RouterQuality>) -> GptImage2p5Quality {
match quality {
Some(RouterQuality::Low) => GptImage2p5Quality::Low,
Some(RouterQuality::Medium) => GptImage2p5Quality::Medium,
Some(RouterQuality::Auto) => GptImage2p5Quality::Auto,
Some(RouterQuality::Max) => GptImage2p5Quality::Max,
Some(RouterQuality::XHigh) => GptImage2p5Quality::XHigh,
Some(RouterQuality::High) | None => GptImage2p5Quality::High,
Some(RouterQuality::Medium) => GptImage2p5Quality::Medium,
Some(RouterQuality::Low) => GptImage2p5Quality::Low,
}
}
@@ -220,9 +223,12 @@ mod tests {
fn quality_maps_exhaustively_for_text_and_edit() {
let cases = [
(None, "Some(High)"),
(Some(RouterQuality::Low), "Some(Low)"),
(Some(RouterQuality::Medium), "Some(Medium)"),
(Some(RouterQuality::Auto), "Some(Auto)"),
(Some(RouterQuality::Max), "Some(Max)"),
(Some(RouterQuality::XHigh), "Some(XHigh)"),
(Some(RouterQuality::High), "Some(High)"),
(Some(RouterQuality::Medium), "Some(Medium)"),
(Some(RouterQuality::Low), "Some(Low)"),
];
for (quality, expected) in cases {
let text = unwrap_t2i(plan_gpt_image_2p5_params(&GenerateImageRequestBuilder { quality, ..base_builder() }));
@@ -53,6 +53,9 @@ mod tests {
(Some(Q::Low), Some(Ar::Square), None, 1, 1),
(Some(Q::Medium), Some(Ar::Square), None, 1, 2),
(Some(Q::High), Some(Ar::Square), None, 1, 6),
(Some(Q::Auto), Some(Ar::Square), None, 1, 6), // auto is priced as high
(Some(Q::XHigh), Some(Ar::Square), None, 1, 10),
(Some(Q::Max), Some(Ar::Square), None, 1, 22),
(Some(Q::High), Some(Ar::Square), None, 4, 24),
(Some(Q::High), Some(Ar::SquareHd), None, 1, 9),
(Some(Q::High), Some(Ar::Square), Some(R::TwoK), 1, 9),
@@ -53,6 +53,9 @@ mod tests {
(Some(Q::Low), Some(Ar::Square), None, 1, 1),
(Some(Q::Medium), Some(Ar::Square), None, 1, 2),
(Some(Q::High), Some(Ar::Square), None, 1, 6),
(Some(Q::Auto), Some(Ar::Square), None, 1, 6), // auto is priced as high
(Some(Q::XHigh), Some(Ar::Square), None, 1, 10),
(Some(Q::Max), Some(Ar::Square), None, 1, 22),
(Some(Q::High), Some(Ar::Square), None, 4, 24),
(Some(Q::High), Some(Ar::SquareHd), None, 1, 9),
(Some(Q::High), Some(Ar::Square), Some(R::TwoK), 1, 9),
@@ -122,10 +122,14 @@ pub(crate) fn plan_quality(quality: Option<RouterQuality>) -> Option<GenerateMid
// Kinovi pricing — pass through whatever the caller asked for, and let
// `None` ride Midjourney's server-side default.
match quality {
None => None,
// `auto` has no Midjourney equivalent; leave it to the provider default.
None | Some(RouterQuality::Auto) => None,
Some(RouterQuality::Low) => Some(GenerateMidjourneyV7Quality::Quarter),
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV7Quality::Half),
Some(RouterQuality::High) => Some(GenerateMidjourneyV7Quality::Full),
// Midjourney tops out at full quality; the higher GPT Image tiers clamp to it.
Some(RouterQuality::High)
| Some(RouterQuality::XHigh)
| Some(RouterQuality::Max) => Some(GenerateMidjourneyV7Quality::Full),
}
}
@@ -110,10 +110,14 @@ pub(crate) fn plan_aspect_ratio(
pub(crate) fn plan_quality(quality: Option<RouterQuality>) -> Option<GenerateMidjourneyV7NijiQuality> {
match quality {
None => None,
// `auto` has no Midjourney equivalent; leave it to the provider default.
None | Some(RouterQuality::Auto) => None,
Some(RouterQuality::Low) => Some(GenerateMidjourneyV7NijiQuality::Quarter),
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV7NijiQuality::Half),
Some(RouterQuality::High) => Some(GenerateMidjourneyV7NijiQuality::Full),
// Midjourney tops out at full quality; the higher GPT Image tiers clamp to it.
Some(RouterQuality::High)
| Some(RouterQuality::XHigh)
| Some(RouterQuality::Max) => Some(GenerateMidjourneyV7NijiQuality::Full),
}
}
@@ -110,10 +110,14 @@ pub(crate) fn plan_aspect_ratio(
pub(crate) fn plan_quality(quality: Option<RouterQuality>) -> Option<GenerateMidjourneyV8Quality> {
match quality {
None => None,
// `auto` has no Midjourney equivalent; leave it to the provider default.
None | Some(RouterQuality::Auto) => None,
Some(RouterQuality::Low) => Some(GenerateMidjourneyV8Quality::Quarter),
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV8Quality::Half),
Some(RouterQuality::High) => Some(GenerateMidjourneyV8Quality::Full),
// Midjourney tops out at full quality; the higher GPT Image tiers clamp to it.
Some(RouterQuality::High)
| Some(RouterQuality::XHigh)
| Some(RouterQuality::Max) => Some(GenerateMidjourneyV8Quality::Full),
}
}
@@ -1,9 +1,12 @@
use crate::generate::generate_video::providers::kinovi::wan_3p0::build::build_kinovi_wan_3p0;
use crate::generate::generate_video::providers::artcraft::wan_3p0::build::build_artcraft_wan_3p0;
use crate::api::audio_list_ref::AudioListRef;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_bitrate::RouterBitrate;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_video_output_format::RouterVideoOutputFormat;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
@@ -125,6 +128,10 @@ pub struct GenerateVideoRequestBuilder {
/// Not all models support this; models that don't simply ignore it.
pub bitrate: Option<RouterBitrate>,
/// Output container for Seedance 2.5 on Kinovi and Artcraft.
/// None keeps the provider default; unsupported models/providers ignore it.
pub maybe_output_format: Option<RouterVideoOutputFormat>,
/// How many seconds to generate.
pub duration_seconds: Option<u16>,
@@ -137,10 +144,12 @@ pub struct GenerateVideoRequestBuilder {
/// so it's best to be explicit.
pub generate_audio: Option<bool>,
/// CALCULATION-ONLY (never sent to any provider): the total seconds of
/// Local pricing/validation metadata (never sent to a provider): total seconds of
/// reference video input, summed across all reference videos and rounded
/// up to whole seconds per file. Models that bill reference-video input
/// seconds (e.g. Seedance 2.5) add this to the billed duration.
/// seconds (e.g. Seedance 2.5) add this to the billed duration. Wan uses
/// the ceiling of the combined measured duration to validate its output
/// budget; reference seconds do not affect Wan pricing.
pub total_reference_video_input_seconds: Option<u16>,
/// If the request is a mismatch with the (model/provider), how to mitigate it.
@@ -168,6 +177,7 @@ impl Default for GenerateVideoRequestBuilder {
resolution: None,
aspect_ratio: None,
bitrate: None,
maybe_output_format: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
@@ -186,6 +196,7 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::Artcraft, RouterVideoModel::GrokImagineVideo1p5) => build_artcraft_grok_imagine_video_1p5(self),
(RouterProvider::Artcraft, RouterVideoModel::Flux3) => build_artcraft_flux_3(self),
(RouterProvider::Artcraft, RouterVideoModel::Flux3Draft) => build_artcraft_flux_3_draft(self),
(RouterProvider::Artcraft, RouterVideoModel::Wan3p0 | RouterVideoModel::Wan3p0Prime) => build_artcraft_wan_3p0(self),
(RouterProvider::Artcraft, RouterVideoModel::HappyHorse1p0) => build_artcraft_happy_horse_1p0(self),
(RouterProvider::Artcraft, RouterVideoModel::Kling16Pro) => build_artcraft_kling_1_6_pro(self),
(RouterProvider::Artcraft, RouterVideoModel::Kling21Master) => build_artcraft_kling_2_1_master(self),
@@ -248,6 +259,7 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo) => build_grok_api_grok_imagine_video(self),
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo1p5) => build_grok_api_grok_imagine_video_1p5(self),
// Kinovi
(RouterProvider::KinoviWeb, RouterVideoModel::Wan3p0 | RouterVideoModel::Wan3p0Prime) => build_kinovi_wan_3p0(self),
(RouterProvider::KinoviWeb, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
@@ -3,10 +3,12 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonA
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_bitrate::RouterBitrate;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_output_format::RouterVideoOutputFormat;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
@@ -55,6 +57,13 @@ pub fn build_artcraft_omni_video_request(
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
let bitrate = plan_bitrate(builder.bitrate.take());
let prompt = builder.prompt.take();
let supports_output_format = matches!(model, CommonVideoModelEnum::Seedance2p5 | CommonVideoModelEnum::Seedance2p5Ultra);
let maybe_output_format = builder.maybe_output_format
.filter(|_| supports_output_format)
.map(|format| match format {
RouterVideoOutputFormat::Mp4 => CommonVideoOutputFormat::Mp4,
RouterVideoOutputFormat::Mov => CommonVideoOutputFormat::Mov,
});
let start_frame = resolve_image_ref(builder.start_frame.take())?;
let end_frame = resolve_image_ref(builder.end_frame.take())?;
@@ -77,6 +86,7 @@ pub fn build_artcraft_omni_video_request(
resolution,
aspect_ratio,
bitrate,
maybe_output_format,
duration_seconds: duration_seconds.map(|d| d as u16),
video_batch_count: Some(batch_count),
negative_prompt: None,
@@ -254,30 +264,6 @@ pub fn plan_batch_count(
}
}
/// Batch counts for the Seedance 2.0 Mini family: 1-8 are all valid
/// (wider than the shared 1/2/4 planning); higher counts clamp to 8 per the
/// mitigation strategy.
pub fn plan_mini_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<u16, ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1..=8 => Ok(count),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(8),
},
}
}
/// Translate the router-facing bitrate into the API `CommonBitrate`.
///
/// Bitrate does not affect cost. An unset value leaves the field `None` (the
@@ -317,6 +303,71 @@ pub fn plan_duration(
mod tests {
use super::*;
mod output_format {
use super::*;
use crate::api::router_provider::RouterProvider;
use crate::api::router_video_model::RouterVideoModel;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
#[test]
fn seedance_2p5_preserves_output_format_and_audio_over_http() {
for model in [RouterVideoModel::Seedance2p5, RouterVideoModel::Seedance2p5Ultra] {
for maybe_output_format in [None, Some(RouterVideoOutputFormat::Mp4), Some(RouterVideoOutputFormat::Mov)] {
for generate_audio in [None, Some(true), Some(false)] {
let builder = GenerateVideoRequestBuilder {
provider: RouterProvider::Artcraft,
model,
maybe_output_format,
generate_audio,
..Default::default()
};
let request = match builder.build2().unwrap() {
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5(state)) => state.request,
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Ultra(state)) => state.request,
other => panic!("Unexpected request: {:?}", other),
};
let json = serde_json::to_value(&request).unwrap();
let expected = match maybe_output_format {
None => None,
Some(RouterVideoOutputFormat::Mp4) => Some("mp4"),
Some(RouterVideoOutputFormat::Mov) => Some("mov"),
};
assert_eq!(json.get("output_format").and_then(|v| v.as_str()), expected);
if expected.is_none() {
assert!(json.get("output_format").is_none());
}
let decoded: OmniGenVideoCostAndGenerateRequest = serde_json::from_value(json).unwrap();
assert_eq!(decoded.maybe_output_format, request.maybe_output_format);
assert_eq!(decoded.generate_audio, generate_audio);
}
}
}
}
#[test]
fn unsupported_models_ignore_output_format_even_with_error_strategy() {
for model in [RouterVideoModel::Seedance2p0, RouterVideoModel::Seedance2p0Fast, RouterVideoModel::Seedance2p5Preview] {
for format in [RouterVideoOutputFormat::Mp4, RouterVideoOutputFormat::Mov] {
let builder = GenerateVideoRequestBuilder {
provider: RouterProvider::Artcraft,
model,
maybe_output_format: Some(format),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..Default::default()
};
let request = match builder.build2().unwrap() {
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0(state)) => state.request,
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Fast(state)) => state.request,
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Preview(state)) => state.request,
other => panic!("Unexpected request: {:?}", other),
};
assert!(serde_json::to_value(request).unwrap().get("output_format").is_none());
}
}
}
}
mod bitrate_translation {
use super::*;
@@ -59,6 +59,7 @@ mod tests {
resolution: None,
aspect_ratio: None,
bitrate: None,
maybe_output_format: None,
quality: None,
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -0,0 +1,259 @@
//! Public ArtCraft batch planning and retail price snapshots for Kinovi models.
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::video_list_ref::VideoListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
const MODELS: [RouterVideoModel; 12] = [
RouterVideoModel::Seedance2p5,
RouterVideoModel::Seedance2p0,
RouterVideoModel::Seedance2p0Fast,
RouterVideoModel::Seedance2p0Mini,
RouterVideoModel::Seedance2p5Ultra,
RouterVideoModel::Seedance2p0BytePlus,
RouterVideoModel::Seedance2p0BytePlusFast,
RouterVideoModel::Seedance2p0BytePlusMini,
RouterVideoModel::Seedance2p0BytePlusUltra,
RouterVideoModel::Seedance2p0BytePlusUltraFast,
RouterVideoModel::Seedance2p0BytePlusUltraMini,
RouterVideoModel::HappyHorse1p0,
];
mod batch_planning {
use super::*;
#[test]
fn batches_one_through_four_survive_http_serialization() {
for model in MODELS {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
for batch in [None, Some(1), Some(2), Some(3), Some(4)] {
let mut builder = builder(model, RouterResolution::SevenTwentyP, batch);
builder.request_mismatch_mitigation_strategy = strategy;
let request = http_request(builder.build2().unwrap());
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["video_batch_count"], batch.unwrap_or(1), "{model:?}");
let decoded: OmniGenVideoCostAndGenerateRequest = serde_json::from_value(json).unwrap();
assert_eq!(decoded.video_batch_count, Some(batch.unwrap_or(1)));
}
}
}
}
#[test]
fn zero_is_rejected_under_every_strategy() {
for model in MODELS {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let mut builder = builder(model, RouterResolution::SevenTwentyP, Some(0));
builder.request_mismatch_mitigation_strategy = strategy;
assert!(matches!(builder.build2(), Err(ArtcraftRouterError::Client(
ClientError::UserRequestedZeroGenerations
))), "{model:?}");
}
}
}
#[test]
fn batches_above_four_follow_the_mitigation_strategy() {
for model in MODELS {
for batch in [5, 6, 7, 8, u16::MAX] {
let builder = builder(model, RouterResolution::SevenTwentyP, Some(batch));
assert!(matches!(builder.clone().build2(), Err(ArtcraftRouterError::Client(
ClientError::ModelDoesNotSupportOption { field: "video_batch_count", .. }
))), "{model:?}, batch {batch}");
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let mut builder = builder.clone();
builder.request_mismatch_mitigation_strategy = strategy;
let request = http_request(builder.build2().unwrap());
assert_eq!(request.video_batch_count, Some(4), "{model:?}, batch {batch}");
}
}
}
}
}
mod retail_prices {
use super::*;
use RouterResolution::{FourEightyP, FourK, SevenTwentyP, TenEightyP};
use RouterVideoModel::{HappyHorse1p0, Seedance2p0, Seedance2p0Fast, Seedance2p0Mini, Seedance2p5,
Seedance2p5Ultra, Seedance2p0BytePlus, Seedance2p0BytePlusFast, Seedance2p0BytePlusMini, Seedance2p0BytePlusUltra, Seedance2p0BytePlusUltraFast, Seedance2p0BytePlusUltraMini,
};
#[test]
fn five_second_prices_for_every_resolution_and_batch() {
// Literal USD cents / ArtCraft credits for batches [1, 2, 3, 4].
// Pin the final rounded totals, not a multiple of an already-rounded price.
for (model, resolution, expected) in [
(Seedance2p5, FourEightyP, [59, 118, 177, 236]),
(Seedance2p5, SevenTwentyP, [134, 268, 401, 535]),
(Seedance2p5, TenEightyP, [309, 617, 925, 1233]),
(Seedance2p5Ultra, FourEightyP, [70, 140, 209, 279]),
(Seedance2p5Ultra, SevenTwentyP, [158, 316, 474, 632]),
(Seedance2p5Ultra, TenEightyP, [337, 673, 1009, 1345]),
(Seedance2p0, FourEightyP, [39, 78, 117, 155]),
(Seedance2p0, SevenTwentyP, [93, 185, 278, 370]),
(Seedance2p0, TenEightyP, [233, 466, 699, 933]),
(Seedance2p0, FourK, [463, 925, 1388, 1850]),
(Seedance2p0Fast, FourEightyP, [36, 71, 107, 143]),
(Seedance2p0Fast, SevenTwentyP, [64, 127, 191, 255]),
(Seedance2p0Mini, FourEightyP, [18, 35, 52, 69]),
(Seedance2p0Mini, SevenTwentyP, [45, 89, 134, 178]),
(Seedance2p0BytePlus, FourEightyP, [50, 100, 150, 200]),
(Seedance2p0BytePlus, SevenTwentyP, [125, 250, 375, 500]),
(Seedance2p0BytePlus, TenEightyP, [250, 500, 750, 1000]),
(Seedance2p0BytePlus, FourK, [475, 950, 1425, 1900]),
(Seedance2p0BytePlusUltra, FourEightyP, [50, 100, 150, 200]),
(Seedance2p0BytePlusUltra, SevenTwentyP, [125, 250, 375, 500]),
(Seedance2p0BytePlusUltra, TenEightyP, [250, 500, 750, 1000]),
(Seedance2p0BytePlusUltra, FourK, [475, 950, 1425, 1900]),
(Seedance2p0BytePlusFast, FourEightyP, [45, 90, 135, 180]),
(Seedance2p0BytePlusFast, SevenTwentyP, [100, 200, 300, 400]),
(Seedance2p0BytePlusUltraFast, FourEightyP, [45, 90, 135, 180]),
(Seedance2p0BytePlusUltraFast, SevenTwentyP, [100, 200, 300, 400]),
(Seedance2p0BytePlusMini, FourEightyP, [18, 36, 54, 71]),
(Seedance2p0BytePlusMini, SevenTwentyP, [46, 91, 137, 182]),
(Seedance2p0BytePlusUltraMini, FourEightyP, [18, 36, 54, 71]),
(Seedance2p0BytePlusUltraMini, SevenTwentyP, [46, 91, 137, 182]),
(HappyHorse1p0, SevenTwentyP, [85, 171, 256, 342]),
(HappyHorse1p0, TenEightyP, [171, 342, 513, 684]),
] {
assert_batch_prices(builder(model, resolution, None), expected);
}
}
#[test]
fn video_reference_prices_for_every_resolution_and_batch() {
// Five output seconds, plus ten measured input seconds for Seedance 2.5.
for (model, resolution, expected) in [
(Seedance2p5, FourEightyP, [109, 218, 326, 435]),
(Seedance2p5, SevenTwentyP, [238, 476, 713, 951]),
(Seedance2p5, TenEightyP, [578, 1155, 1733, 2310]),
(Seedance2p5Ultra, FourEightyP, [129, 257, 386, 514]),
(Seedance2p5Ultra, SevenTwentyP, [281, 562, 843, 1124]),
(Seedance2p5Ultra, TenEightyP, [628, 1256, 1884, 2511]),
(Seedance2p0, FourEightyP, [45, 89, 133, 177]),
(Seedance2p0, SevenTwentyP, [111, 221, 331, 441]),
(Seedance2p0, TenEightyP, [256, 511, 767, 1022]),
(Seedance2p0, FourK, [569, 1138, 1707, 2276]),
(Seedance2p0Fast, FourEightyP, [43, 85, 127, 169]),
(Seedance2p0Fast, SevenTwentyP, [78, 156, 234, 312]),
// Preserve Mini's existing floating-point round-up behavior.
(Seedance2p0Mini, FourEightyP, [22, 44, 66, 88]),
(Seedance2p0Mini, SevenTwentyP, [54, 108, 161, 215]),
(Seedance2p0BytePlus, FourEightyP, [51, 102, 153, 204]),
(Seedance2p0BytePlus, SevenTwentyP, [129, 257, 386, 514]),
(Seedance2p0BytePlus, TenEightyP, [289, 578, 867, 1156]),
(Seedance2p0BytePlus, FourK, [595, 1190, 1785, 2380]),
(Seedance2p0BytePlusUltra, FourEightyP, [51, 102, 153, 204]),
(Seedance2p0BytePlusUltra, SevenTwentyP, [129, 257, 386, 514]),
(Seedance2p0BytePlusUltra, TenEightyP, [289, 578, 867, 1156]),
(Seedance2p0BytePlusUltra, FourK, [595, 1190, 1785, 2380]),
(Seedance2p0BytePlusFast, FourEightyP, [49, 97, 145, 193]),
(Seedance2p0BytePlusFast, SevenTwentyP, [103, 205, 308, 410]),
(Seedance2p0BytePlusUltraFast, FourEightyP, [49, 97, 145, 193]),
(Seedance2p0BytePlusUltraFast, SevenTwentyP, [103, 205, 308, 410]),
(Seedance2p0BytePlusMini, FourEightyP, [23, 45, 68, 90]),
(Seedance2p0BytePlusMini, SevenTwentyP, [55, 110, 165, 219]),
(Seedance2p0BytePlusUltraMini, FourEightyP, [23, 45, 68, 90]),
(Seedance2p0BytePlusUltraMini, SevenTwentyP, [55, 110, 165, 219]),
] {
let mut builder = builder(model, resolution, None);
builder.reference_videos = Some(VideoListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_reference".to_string()),
]));
builder.total_reference_video_input_seconds = Some(10);
assert_batch_prices(builder, expected);
}
}
#[test]
fn seedance_2p5_input_duration_clamping_and_fallback_scale_with_batch() {
for (model, resolution, minimum, maximum) in [
(Seedance2p5, FourEightyP, [66, 131, 196, 261], [254, 507, 761, 1014]),
(Seedance2p5, SevenTwentyP, [143, 286, 428, 571], [555, 1110, 1664, 2219]),
(Seedance2p5, TenEightyP, [347, 693, 1040, 1386], [1348, 2695, 4043, 5390]),
(Seedance2p5Ultra, FourEightyP, [78, 155, 232, 309], [300, 600, 899, 1199]),
(Seedance2p5Ultra, SevenTwentyP, [169, 338, 506, 675], [656, 1311, 1967, 2622]),
(Seedance2p5Ultra, TenEightyP, [377, 754, 1130, 1507], [1465, 2930, 4394, 5859]),
] {
for (input_seconds, expected) in [
(Some(1), minimum), (Some(4), minimum),
(None, maximum), (Some(0), maximum), (Some(30), maximum), (Some(99), maximum),
] {
let mut builder = builder(model, resolution, None);
builder.reference_videos = Some(VideoListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_reference".to_string()),
]));
builder.total_reference_video_input_seconds = input_seconds;
assert_batch_prices(builder, expected);
}
}
}
}
fn builder(model: RouterVideoModel, resolution: RouterResolution, batch: Option<u16>) -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: RouterProvider::Artcraft,
model,
resolution: Some(resolution),
duration_seconds: Some(5),
video_batch_count: batch,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..Default::default()
}
}
fn assert_batch_prices(builder: GenerateVideoRequestBuilder, expected: [u64; 4]) {
for (index, expected_price) in expected.into_iter().enumerate() {
let batch = index as u16 + 1;
let mut builder = builder.clone();
builder.video_batch_count = Some(batch);
let context = format!("{:?}, {:?}, batch {batch}", builder.model, builder.resolution);
let request = builder.build2().unwrap();
let cost = request.estimate_cost().unwrap();
assert_eq!(cost.cost_in_credits, Some(expected_price), "{context}");
assert_eq!(cost.cost_in_usd_cents, Some(expected_price), "{context}");
assert_eq!(http_request(request).video_batch_count, Some(batch), "{context}");
}
}
fn http_request(request: VideoGenerationDraftOrRequest) -> OmniGenVideoCostAndGenerateRequest {
match request {
VideoGenerationDraftOrRequest::Request(request) => match request {
VideoGenerationRequest::ArtcraftSeedance2p5(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0Fast(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0Mini(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p5Ultra(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltra(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraFast(state) => state.request,
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(state) => state.request,
VideoGenerationRequest::ArtcraftHappyHorse1p0(state) => state.request,
other => panic!("Unexpected request: {other:?}"),
},
other => panic!("Expected ArtCraft request: {other:?}"),
}
}
@@ -2,6 +2,8 @@ pub(crate) mod build_common;
pub(crate) mod seedance_common;
#[cfg(test)]
mod parity_tests;
#[cfg(test)]
mod kinovi_batch_tests;
pub mod grok_imagine_video;
pub mod grok_imagine_video_1p5;
pub mod flux_3;
@@ -43,4 +45,4 @@ pub mod veo_3p1_fast;
pub mod veo_3p1_lite;
pub mod vidu_q3;
pub mod vidu_q3_turbo;
pub mod wan_3p0;
@@ -3,27 +3,21 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::artcraft::build_common::{
build_artcraft_omni_video_request, plan_mini_batch_count, SupportedResolutions, UltraWideSupport,
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::request::ArtcraftSeedance2p0BytePlusMiniRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_2p0_bp_mini(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
pub fn build_artcraft_seedance_2p0_bp_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
// to 720p), and all six aspect ratios including 21:9.
// Mini supports batches of 1-8, wider than the shared 1/2/4 planning.
// Take the batch out before the shared build and set it after.
let strategy = builder.request_mismatch_mitigation_strategy;
let batch_count = plan_mini_batch_count(builder.video_batch_count.take(), strategy)?;
let mut request = build_artcraft_omni_video_request(
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0BytePlusMini,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
request.video_batch_count = Some(batch_count);
let state = ArtcraftSeedance2p0BytePlusMiniRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(state)))
@@ -87,18 +81,17 @@ mod tests {
}
#[test]
fn batch_count_up_to_eight_passed_through() {
// Mini supports batches of 1-8; execution and billing must agree.
for batch in 1..=8u16 {
fn batch_count_up_to_four_passed_through() {
for batch in 1..=4u16 {
let req = unwrap_request(builder_with(move |b| { b.video_batch_count = Some(batch); }));
assert_eq!(req.request.video_batch_count, Some(batch));
}
}
#[test]
fn batch_count_above_eight_clamps_to_eight() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(9); }));
assert_eq!(req.request.video_batch_count, Some(8));
fn batch_count_above_four_clamps_to_four() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
@@ -108,18 +108,13 @@ mod tests {
mod batch_pricing {
use super::*;
/// Mini bills batches of 1-8 in full (the platform's 1/2/4 cap does not
/// apply). Once, batch 8 executed in full but billed as batch 4.
/// ArtCraft exposes batches of 1-4 and bills the entire planned batch.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 46);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 91);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 137);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 182);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 228);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 273);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 319);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 364);
}
#[test]
@@ -128,23 +123,16 @@ mod tests {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 36);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 54);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 71);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 89);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 107);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 125);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 142);
}
#[test]
fn batch_eight_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 438);
fn batch_four_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, true), 219);
}
#[test]
fn batch_above_eight_clamps_to_eight() {
assert_eq!(
cents(RouterResolution::SevenTwentyP, 5, 9, false),
cents(RouterResolution::SevenTwentyP, 5, 8, false),
);
fn batch_above_four_clamps_to_four() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 182);
}
}
@@ -160,7 +148,7 @@ mod tests {
];
for res in resolutions {
for dur in [4u16, 5, 10, 15] {
for batch in [1u16, 2, 4] {
for batch in 1..=4 {
for has_ref in [false, true] {
let cost = build_cost(res, dur, batch, has_ref);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
@@ -3,27 +3,21 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::artcraft::build_common::{
build_artcraft_omni_video_request, plan_mini_batch_count, SupportedResolutions, UltraWideSupport,
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_mini::request::ArtcraftSeedance2p0BytePlusUltraMiniRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_2p0_bpu_mini(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
pub fn build_artcraft_seedance_2p0_bpu_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
// to 720p), and all six aspect ratios including 21:9.
// Mini supports batches of 1-8, wider than the shared 1/2/4 planning.
// Take the batch out before the shared build and set it after.
let strategy = builder.request_mismatch_mitigation_strategy;
let batch_count = plan_mini_batch_count(builder.video_batch_count.take(), strategy)?;
let mut request = build_artcraft_omni_video_request(
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0BytePlusUltraMini,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
request.video_batch_count = Some(batch_count);
let state = ArtcraftSeedance2p0BytePlusUltraMiniRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(state)))
@@ -87,18 +81,17 @@ mod tests {
}
#[test]
fn batch_count_up_to_eight_passed_through() {
// Mini supports batches of 1-8; execution and billing must agree.
for batch in 1..=8u16 {
fn batch_count_up_to_four_passed_through() {
for batch in 1..=4u16 {
let req = unwrap_request(builder_with(move |b| { b.video_batch_count = Some(batch); }));
assert_eq!(req.request.video_batch_count, Some(batch));
}
}
#[test]
fn batch_count_above_eight_clamps_to_eight() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(9); }));
assert_eq!(req.request.video_batch_count, Some(8));
fn batch_count_above_four_clamps_to_four() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
@@ -108,18 +108,13 @@ mod tests {
mod batch_pricing {
use super::*;
/// Mini bills batches of 1-8 in full (the platform's 1/2/4 cap does not
/// apply). Once, batch 8 executed in full but billed as batch 4.
/// ArtCraft exposes batches of 1-4 and bills the entire planned batch.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 46);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 91);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 137);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 182);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 228);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 273);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 319);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 364);
}
#[test]
@@ -128,23 +123,16 @@ mod tests {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 36);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 54);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 71);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 89);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 107);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 125);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 142);
}
#[test]
fn batch_eight_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 438);
fn batch_four_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, true), 219);
}
#[test]
fn batch_above_eight_clamps_to_eight() {
assert_eq!(
cents(RouterResolution::SevenTwentyP, 5, 9, false),
cents(RouterResolution::SevenTwentyP, 5, 8, false),
);
fn batch_above_four_clamps_to_four() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 182);
}
}
@@ -160,7 +148,7 @@ mod tests {
];
for res in resolutions {
for dur in [4u16, 5, 10, 15] {
for batch in [1u16, 2, 4] {
for batch in 1..=4 {
for has_ref in [false, true] {
let cost = build_cost(res, dur, batch, has_ref);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
@@ -3,27 +3,21 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::artcraft::build_common::{
build_artcraft_omni_video_request, plan_mini_batch_count, SupportedResolutions, UltraWideSupport,
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::request::ArtcraftSeedance2p0MiniRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_2p0_mini(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
pub fn build_artcraft_seedance_2p0_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
// to 720p), and all six aspect ratios including 21:9.
// Mini supports batches of 1-8, wider than the shared 1/2/4 planning.
// Take the batch out before the shared build and set it after.
let strategy = builder.request_mismatch_mitigation_strategy;
let batch_count = plan_mini_batch_count(builder.video_batch_count.take(), strategy)?;
let mut request = build_artcraft_omni_video_request(
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0Mini,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
request.video_batch_count = Some(batch_count);
let state = ArtcraftSeedance2p0MiniRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Mini(state)))
@@ -87,18 +81,17 @@ mod tests {
}
#[test]
fn batch_count_up_to_eight_passed_through() {
// Mini supports batches of 1-8; execution and billing must agree.
for batch in 1..=8u16 {
fn batch_count_up_to_four_passed_through() {
for batch in 1..=4u16 {
let req = unwrap_request(builder_with(move |b| { b.video_batch_count = Some(batch); }));
assert_eq!(req.request.video_batch_count, Some(batch));
}
}
#[test]
fn batch_count_above_eight_clamps_to_eight() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(9); }));
assert_eq!(req.request.video_batch_count, Some(8));
fn batch_count_above_four_clamps_to_four() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
assert_eq!(req.request.video_batch_count, Some(4));
}
#[test]
@@ -108,18 +108,13 @@ mod tests {
mod batch_pricing {
use super::*;
/// Mini bills batches of 1-8 in full (the platform's 1/2/4 cap does not
/// apply). Once, batch 8 executed in full but billed as batch 4.
/// ArtCraft exposes batches of 1-4 and bills the entire planned batch.
#[test]
fn every_batch_size_720p_5s() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 45);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 2, false), 89);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 3, false), 134);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, false), 178);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 5, false), 223);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 6, false), 267);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 7, false), 312);
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 356);
}
#[test]
@@ -128,23 +123,16 @@ mod tests {
assert_eq!(cents(RouterResolution::FourEightyP, 5, 2, false), 35);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 3, false), 52);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 4, false), 69);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 5, false), 87);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 6, false), 104);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 7, false), 121);
assert_eq!(cents(RouterResolution::FourEightyP, 5, 8, false), 138);
}
#[test]
fn batch_eight_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, true), 429);
fn batch_four_with_video_reference() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 4, true), 215);
}
#[test]
fn batch_above_eight_clamps_to_eight() {
assert_eq!(
cents(RouterResolution::SevenTwentyP, 5, 9, false),
cents(RouterResolution::SevenTwentyP, 5, 8, false),
);
fn batch_above_four_clamps_to_four() {
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 8, false), 178);
}
}
@@ -160,7 +148,7 @@ mod tests {
];
for res in resolutions {
for dur in [4u16, 5, 10, 15] {
for batch in [1u16, 2, 4] {
for batch in 1..=4 {
for has_ref in [false, true] {
let cost = build_cost(res, dur, batch, has_ref);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
@@ -43,12 +43,11 @@ pub fn build_artcraft_seedance_2p5(mut builder: GenerateVideoRequestBuilder) ->
}
// 2.5 allows 4-30 second durations (longer than the shared 4-15 range in
// build_common) and generates a single video per request. Plan both here,
// leaving None for the shared builder, then set them after.
// build_common). Plan duration here and use the shared 1-4 batch planner.
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
plan_batch_count(builder.video_batch_count.take(), strategy)?;
let total_input_seconds = builder.total_reference_video_input_seconds.take();
let generate_audio = builder.generate_audio.take();
let mut request = build_artcraft_omni_video_request(
builder,
@@ -57,7 +56,7 @@ pub fn build_artcraft_seedance_2p5(mut builder: GenerateVideoRequestBuilder) ->
UltraWideSupport::Supported,
)?;
request.duration_seconds = duration_seconds;
request.video_batch_count = Some(1);
request.generate_audio = generate_audio;
let state = ArtcraftSeedance2p5RequestState { request, total_input_seconds };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5(state)))
@@ -85,28 +84,6 @@ fn plan_duration(
}
}
// Seedance 2.5 generates a single video per request (no batching).
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<(), ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(()),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
},
}
}
#[cfg(test)]
mod tests {
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
@@ -151,9 +128,11 @@ mod tests {
}
#[test]
fn batch_count_is_always_one() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(1));
fn batch_counts_one_through_four_are_preserved() {
for batch_count in 1..=4 {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(batch_count); }));
assert_eq!(req.request.video_batch_count, Some(batch_count));
}
}
#[test]
Loaded 100 of 1017 files, more files were not shown because too many files have changed in this diff. Show more