Compare commits

..
Author SHA1 Message Date
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
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
Tanish Manku df9654fed9 Merge remote-tracking branch 'services/main' into design/proofing 2026-09-14 15:26:26 -07:00
Tanish Manku 239ae4ada2 hero: click boost 2x -> 3x (tuner max raised to 4x)
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
2026-09-11 19:31:51 -07:00
Tanish Manku 33d50851b0 fix Firefox back-navigation dead page: bump Next 16.2.6 -> 16.2.12
Reproduced in a real Firefox (Playwright): returning to the page via
browser Back was NOT a bfcache restore (pageshow persisted=false,
navType back_forward) — it was a fresh load on which Next's client
router threw "Internal Next.js error: Router action dispatched before
initialization" three times, aborting hydration. No effects ran, so
data-ruler was never stamped, the galaxy canvas never mounted, and
data-intro never lifted (the wordmark stayed CSS-hidden at opacity 0)
— the whole page looked unmounted.

The 16.2.12 patch fixes the router-initialization race; the same repro
now comes back fully mounted. The Firefox-gated unload bfcache-buster
from the previous attempt is removed (wrong theory, and it cost
Firefox users instant-back site-wide); the pageshow persisted reload
guard stays as insurance for genuinely broken bfcache restores.
Production build verified on 16.2.12.

Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
2026-09-11 13:55:50 -07:00
243 changed files with 9707 additions and 1536 deletions

No files matched your search

@@ -94,11 +94,11 @@ mod tests {
#[test]
fn table_1080p() {
// 45.85869386 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 184);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 230);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 459);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1376);
// 61.62732357 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 247);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 309);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 617);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1849);
}
#[test]
@@ -121,16 +121,16 @@ mod tests {
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(10)), 290);
// 15.84362140 ¢/s × 40 = 633.74 → 634¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::SevenTwentyP), 30, Some(10)), 634);
// 27.39973680 ¢/s × 40 = 1095.99 → 1096¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1096);
// 38.49728574 ¢/s × 40 = 1539.89 → 1540¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1540);
}
#[test]
fn table_1080p_with_ten_input_seconds() {
// 27.39973680 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 411);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 548);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1096);
// 38.49728574 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 578);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 770);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1540);
}
#[test]
@@ -95,11 +95,11 @@ mod tests {
#[test]
fn table_1080p() {
// 50.10486922 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 201);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 251);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 502);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1504);
// 67.22980753 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 269);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 337);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 673);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 2017);
}
#[test]
@@ -141,16 +141,16 @@ mod tests {
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(10)), 343);
// 18.72427984 ¢/s × 40 = 748.97 → 749¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::SevenTwentyP), 30, Some(10)), 749);
// 29.93674947 ¢/s × 40 = 1197.47 → 1198¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1198);
// 41.84487580 ¢/s × 40 = 1673.80 → 1674¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1674);
}
#[test]
fn table_1080p_with_ten_input_seconds() {
// 29.93674947 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 450);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 599);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1198);
// 41.84487580 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 628);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 837);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1674);
}
#[test]
@@ -137,14 +137,15 @@ pub fn seedance_2p5_preview_usd_cents(
const SEEDANCE_2P5_CENTS_PER_SECOND_480P: f64 = 11.76954733;
/// Seedance 2.5 — 720p price, USD cents per second.
const SEEDANCE_2P5_CENTS_PER_SECOND_720P: f64 = 26.70781893;
/// Seedance 2.5 — 1080p price, USD cents per second.
const SEEDANCE_2P5_CENTS_PER_SECOND_1080P: f64 = 45.85869386;
/// Seedance 2.5 — 1080p price, USD cents per second (updated 2026-09-17).
const SEEDANCE_2P5_CENTS_PER_SECOND_1080P: f64 = 61.62732357;
/// Seedance 2.5 — 480p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_480P: f64 = 7.24279835;
/// Seedance 2.5 — 720p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_720P: f64 = 15.84362140;
/// Seedance 2.5 — 1080p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 27.39973680;
/// Seedance 2.5 — 1080p price with video references, USD cents per second
/// (updated 2026-09-17).
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 38.49728574;
/// ArtCraft's price (USD cents) for Seedance 2.5.
///
@@ -199,14 +200,15 @@ pub fn seedance_2p5_usd_cents(
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_480P: f64 = 13.90946502;
/// Seedance 2.5 Ultra — 720p price, USD cents per second.
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_720P: f64 = 31.56378601;
/// Seedance 2.5 Ultra — 1080p price, USD cents per second.
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_1080P: f64 = 50.10486922;
/// Seedance 2.5 Ultra — 1080p price, USD cents per second (updated 2026-09-17).
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_1080P: f64 = 67.22980753;
/// Seedance 2.5 Ultra — 480p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_480P: f64 = 8.55967078;
/// Seedance 2.5 Ultra — 720p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_720P: f64 = 18.72427984;
/// Seedance 2.5 Ultra — 1080p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 29.93674947;
/// Seedance 2.5 Ultra — 1080p price with video references, USD cents per
/// second (updated 2026-09-17).
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 41.84487580;
/// ArtCraft's price (USD cents) for Seedance 2.5 Ultra.
///
@@ -100,7 +100,7 @@ impl KinoviSeedance2p5CostState {
// per-model credit rate at our bulk credit purchase rate).
let costs = pricing_request.calculate_enterprise_costs();
// 2.5 bills whole credits at 480p/720p, so the rounding below is exact
// there; 1080p bills fractional credits (103.25/sec) and rounds to the
// there; 1080p bills fractional credits (136.23/sec) and rounds to the
// nearest credit. The USD cents (the authoritative charge) are rounded
// up.
let cost_in_credits = costs.kinovi_credits.round() as u64;
@@ -125,7 +125,7 @@ mod tests {
use super::*;
// ── Credits without video references (26/sec at 480p, 59/sec at 720p,
// 103.25/sec at 1080p enterprise) ──
// 136.23/sec at 1080p enterprise) ──
mod credits_without_video_references {
use super::*;
@@ -145,11 +145,11 @@ mod tests {
#[test]
fn credits_1080p() {
// 103.25/sec enterprise; fractional totals round to the nearest credit
// (516.25 → 516, 1032.5 → 1033, 3097.5 → 3098).
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 5, false, None), 516);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 10, false, None), 1033);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, false, None), 3098);
// 136.23/sec enterprise; fractional totals round to the nearest credit
// (681.15 → 681, 1362.3 → 1362, 4086.9 → 4087).
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 5, false, None), 681);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 10, false, None), 1362);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, false, None), 4087);
}
#[test]
@@ -164,7 +164,7 @@ mod tests {
}
// ── Credits with video references (16/sec at 480p, 35/sec at 720p,
// 61.69/sec at 1080p enterprise, over output duration + input seconds) ──
// 81.4/sec at 1080p enterprise, over output duration + input seconds) ──
mod credits_with_video_references {
use super::*;
@@ -173,8 +173,8 @@ mod tests {
fn thirty_second_output_with_ten_input_seconds_bills_forty() {
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 30, true, Some(10)), 16 * 40);
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 30, true, Some(10)), 35 * 40);
// 61.69/sec enterprise × 40 = 2467.6 → 2468 credits.
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, true, Some(10)), 2468);
// 81.4/sec enterprise × 40 = 3256 credits.
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, true, Some(10)), 3256);
}
#[test]
@@ -147,7 +147,7 @@ pub enum KinoviSeedance2p5OutputFormat {
// |------------|----------------------|------------------------|
// | 480p | 26 | (no discount) |
// | 720p | 59 | (no discount) |
// | 1080p | 107.55 | 103.25 |
// | 1080p | 143.4 | 136.23 |
//
// With video references, the rate drops but the billed seconds are the
// output duration PLUS the total seconds of reference video input
@@ -160,11 +160,10 @@ pub enum KinoviSeedance2p5OutputFormat {
// |------------|----------------------|------------------------|
// | 480p | 16 | (no discount) |
// | 720p | 35 | (no discount) |
// | 1080p | 64.26 | 61.69 |
// | 1080p | 85.68 | 81.4 |
//
// The 1080p enterprise rates were spot-checked against the Kinovi site
// (2026-08-17): 4s / 10s / 30s without a reference video showed 413 /
// 1,032.5 / 3,097.5 credits. 480p and 720p have no separately negotiated
// The 1080p rates were last updated 2026-09-17 (previously 107.55 / 103.25
// and 64.26 / 61.69 credits/sec). 480p and 720p have no separately negotiated
// enterprise credit rate, so enterprise bills the same credits as consumer
// (the tiers still convert to USD at their own purchase rates). Default
// resolution (None) is 720p.
@@ -180,8 +179,8 @@ const SEEDANCE_2P5_720P: KinoviPricingRate = KinoviPricingRate {
maybe_enterprise_credits: None,
};
const SEEDANCE_2P5_1080P: KinoviPricingRate = KinoviPricingRate {
consumer_credits: 107.55,
maybe_enterprise_credits: Some(103.25),
consumer_credits: 143.4,
maybe_enterprise_credits: Some(136.23),
};
/// Per-second credit rates when video references are attached. These REPLACE
@@ -196,8 +195,8 @@ const SEEDANCE_2P5_720P_VIDEO_REF: KinoviPricingRate = KinoviPricingRate {
maybe_enterprise_credits: None,
};
const SEEDANCE_2P5_1080P_VIDEO_REF: KinoviPricingRate = KinoviPricingRate {
consumer_credits: 64.26,
maybe_enterprise_credits: Some(61.69),
consumer_credits: 85.68,
maybe_enterprise_credits: Some(81.4),
};
impl KinoviCostCalculatorTrait for GenerateSeedance2p5Request {
@@ -375,7 +374,7 @@ mod tests {
// ── No video references: output-duration billing ──
//
// 480p = 26 credits/sec and 720p = 59 credits/sec at both tiers (no
// negotiated enterprise discount); 1080p = 107.55 consumer / 103.25
// negotiated enterprise discount); 1080p = 143.4 consumer / 136.23
// enterprise credits/sec.
mod without_video_references {
@@ -401,24 +400,23 @@ mod tests {
#[test]
fn table_1080p() {
// Enterprise 103.25 credits/sec — the 4s / 10s / 30s values were
// verified against the Kinovi site (413 / 1,032.5 / 3,097.5).
assert_eq!(t2v_1080(4).calculate_enterprise_costs().kinovi_credits, 413.0);
assert_eq!(t2v_1080(5).calculate_enterprise_costs().kinovi_credits, 516.25);
assert_eq!(t2v_1080(10).calculate_enterprise_costs().kinovi_credits, 1032.5);
assert_eq!(t2v_1080(15).calculate_enterprise_costs().kinovi_credits, 1548.75);
assert_eq!(t2v_1080(20).calculate_enterprise_costs().kinovi_credits, 2065.0);
assert_eq!(t2v_1080(25).calculate_enterprise_costs().kinovi_credits, 2581.25);
assert_eq!(t2v_1080(30).calculate_enterprise_costs().kinovi_credits, 3097.5);
// Enterprise 136.23 credits/sec (2026-09-17).
assert_eq!(t2v_1080(4).calculate_enterprise_costs().kinovi_credits, 544.92);
assert_eq!(t2v_1080(5).calculate_enterprise_costs().kinovi_credits, 681.15);
assert_eq!(t2v_1080(10).calculate_enterprise_costs().kinovi_credits, 1362.3);
assert_eq!(t2v_1080(15).calculate_enterprise_costs().kinovi_credits, 2043.45);
assert_eq!(t2v_1080(20).calculate_enterprise_costs().kinovi_credits, 2724.6);
assert_eq!(t2v_1080(25).calculate_enterprise_costs().kinovi_credits, 3405.75);
assert_eq!(t2v_1080(30).calculate_enterprise_costs().kinovi_credits, 4086.9);
// Consumer 107.55 credits/sec.
assert_eq!(t2v_1080(4).calculate_consumer_costs().kinovi_credits, 430.2);
assert_eq!(t2v_1080(5).calculate_consumer_costs().kinovi_credits, 537.75);
assert_eq!(t2v_1080(10).calculate_consumer_costs().kinovi_credits, 1075.5);
assert_eq!(t2v_1080(15).calculate_consumer_costs().kinovi_credits, 1613.25);
assert_eq!(t2v_1080(20).calculate_consumer_costs().kinovi_credits, 2151.0);
assert_eq!(t2v_1080(25).calculate_consumer_costs().kinovi_credits, 2688.75);
assert_eq!(t2v_1080(30).calculate_consumer_costs().kinovi_credits, 3226.5);
// Consumer 143.4 credits/sec.
assert_eq!(t2v_1080(4).calculate_consumer_costs().kinovi_credits, 573.6);
assert_eq!(t2v_1080(5).calculate_consumer_costs().kinovi_credits, 717.0);
assert_eq!(t2v_1080(10).calculate_consumer_costs().kinovi_credits, 1434.0);
assert_eq!(t2v_1080(15).calculate_consumer_costs().kinovi_credits, 2151.0);
assert_eq!(t2v_1080(20).calculate_consumer_costs().kinovi_credits, 2868.0);
assert_eq!(t2v_1080(25).calculate_consumer_costs().kinovi_credits, 3585.0);
assert_eq!(t2v_1080(30).calculate_consumer_costs().kinovi_credits, 4302.0);
}
#[test]
@@ -456,7 +454,7 @@ mod tests {
let mut request_1080 = keyframe_request(10);
request_1080.output_resolution = Some(KinoviSeedance2p5OutputResolution::TenEightyP);
assert_eq!(request_1080.calculate_enterprise_costs().kinovi_credits, 1032.5);
assert_eq!(request_1080.calculate_enterprise_costs().kinovi_credits, 1362.3);
}
#[test]
@@ -489,7 +487,7 @@ mod tests {
// ── With video references: input seconds are billed too ──
//
// 480p = 16 credits/sec and 720p = 35 credits/sec at both tiers; 1080p
// = 64.26 consumer / 61.69 enterprise credits/sec. All over
// = 85.68 consumer / 81.4 enterprise credits/sec. All over
// (output duration + total_input_seconds) billed seconds.
mod with_video_references {
@@ -541,21 +539,21 @@ mod tests {
// seconds: billed seconds = 20 + clamp(input, 4, 30). E.g. a 10s
// input video + 20s generation = 30 billed seconds.
//
// Enterprise 61.69 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_enterprise_costs().kinovi_credits, 1480.56);
assert_eq!(video_ref_1080(20, Some(5)).calculate_enterprise_costs().kinovi_credits, 1542.25);
assert_eq!(video_ref_1080(20, Some(10)).calculate_enterprise_costs().kinovi_credits, 1850.7);
assert_eq!(video_ref_1080(20, Some(15)).calculate_enterprise_costs().kinovi_credits, 2159.15);
assert_eq!(video_ref_1080(20, Some(20)).calculate_enterprise_costs().kinovi_credits, 2467.6);
assert_eq!(video_ref_1080(20, Some(30)).calculate_enterprise_costs().kinovi_credits, 3084.5);
// Enterprise 81.4 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_enterprise_costs().kinovi_credits, 1953.6);
assert_eq!(video_ref_1080(20, Some(5)).calculate_enterprise_costs().kinovi_credits, 2035.0);
assert_eq!(video_ref_1080(20, Some(10)).calculate_enterprise_costs().kinovi_credits, 2442.0);
assert_eq!(video_ref_1080(20, Some(15)).calculate_enterprise_costs().kinovi_credits, 2849.0);
assert_eq!(video_ref_1080(20, Some(20)).calculate_enterprise_costs().kinovi_credits, 3256.0);
assert_eq!(video_ref_1080(20, Some(30)).calculate_enterprise_costs().kinovi_credits, 4070.0);
// Consumer 64.26 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_consumer_costs().kinovi_credits, 1542.24);
assert_eq!(video_ref_1080(20, Some(5)).calculate_consumer_costs().kinovi_credits, 1606.5);
assert_eq!(video_ref_1080(20, Some(10)).calculate_consumer_costs().kinovi_credits, 1927.8);
assert_eq!(video_ref_1080(20, Some(15)).calculate_consumer_costs().kinovi_credits, 2249.1);
assert_eq!(video_ref_1080(20, Some(20)).calculate_consumer_costs().kinovi_credits, 2570.4);
assert_eq!(video_ref_1080(20, Some(30)).calculate_consumer_costs().kinovi_credits, 3213.0);
// Consumer 85.68 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_consumer_costs().kinovi_credits, 2056.32);
assert_eq!(video_ref_1080(20, Some(5)).calculate_consumer_costs().kinovi_credits, 2142.0);
assert_eq!(video_ref_1080(20, Some(10)).calculate_consumer_costs().kinovi_credits, 2570.4);
assert_eq!(video_ref_1080(20, Some(15)).calculate_consumer_costs().kinovi_credits, 2998.8);
assert_eq!(video_ref_1080(20, Some(20)).calculate_consumer_costs().kinovi_credits, 3427.2);
assert_eq!(video_ref_1080(20, Some(30)).calculate_consumer_costs().kinovi_credits, 4284.0);
}
#[test]
@@ -574,9 +572,9 @@ mod tests {
// 30-second maximum so the estimate never undershoots the charge.
assert_eq!(video_ref_480(10, None).calculate_consumer_costs().kinovi_credits, 640.0);
assert_eq!(video_ref_720(10, None).calculate_consumer_costs().kinovi_credits, 1400.0);
// 1080p: 40 billed seconds at 61.69 enterprise / 64.26 consumer.
assert_eq!(video_ref_1080(10, None).calculate_enterprise_costs().kinovi_credits, 2467.6);
assert_eq!(video_ref_1080(10, None).calculate_consumer_costs().kinovi_credits, 2570.4);
// 1080p: 40 billed seconds at 81.4 enterprise / 85.68 consumer.
assert_eq!(video_ref_1080(10, None).calculate_enterprise_costs().kinovi_credits, 3256.0);
assert_eq!(video_ref_1080(10, None).calculate_consumer_costs().kinovi_credits, 3427.2);
}
#[test]
@@ -649,47 +647,47 @@ mod tests {
#[test]
fn enterprise_cents_1080p_4s() {
// 413 credits; 41300/243.16 = 169.8470 cents.
// 544.92 credits; 54492/243.16 = 224.0994 cents.
let cost = t2v_1080(4).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 170);
assert_eq!(cost.usd_cents_rounded_down, 169);
assert!((cost.usd_cents_fractional - 169.8470).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 225);
assert_eq!(cost.usd_cents_rounded_down, 224);
assert!((cost.usd_cents_fractional - 224.0994).abs() < FLOAT_TOLERANCE);
}
#[test]
fn consumer_cents_1080p_4s() {
// 430.2 credits; 43020/192.98 = 222.9247 cents.
// 573.6 credits; 57360/192.98 = 297.2329 cents.
let cost = t2v_1080(4).calculate_consumer_costs();
assert_eq!(cost.usd_cents_rounded_up, 223);
assert_eq!(cost.usd_cents_rounded_down, 222);
assert!((cost.usd_cents_fractional - 222.9247).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 298);
assert_eq!(cost.usd_cents_rounded_down, 297);
assert!((cost.usd_cents_fractional - 297.2329).abs() < FLOAT_TOLERANCE);
}
#[test]
fn enterprise_cents_1080p_30s() {
// 3097.5 credits; 309750/243.16 = 1273.8526 cents.
// 4086.9 credits; 408690/243.16 = 1680.7452 cents.
let cost = t2v_1080(30).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 1274);
assert_eq!(cost.usd_cents_rounded_down, 1273);
assert!((cost.usd_cents_fractional - 1273.8526).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1681);
assert_eq!(cost.usd_cents_rounded_down, 1680);
assert!((cost.usd_cents_fractional - 1680.7452).abs() < FLOAT_TOLERANCE);
}
#[test]
fn enterprise_cents_1080p_video_ref_30_billed_seconds() {
// 61.69 × 30 = 1850.7 credits; 185070/243.16 = 761.1038 cents.
// 81.4 × 30 = 2442 credits; 244200/243.16 = 1004.2770 cents.
let cost = video_ref_1080(20, Some(10)).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 762);
assert_eq!(cost.usd_cents_rounded_down, 761);
assert!((cost.usd_cents_fractional - 761.1038).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1005);
assert_eq!(cost.usd_cents_rounded_down, 1004);
assert!((cost.usd_cents_fractional - 1004.2770).abs() < FLOAT_TOLERANCE);
}
#[test]
fn consumer_cents_1080p_video_ref_30_billed_seconds() {
// 64.26 × 30 = 1927.8 credits; 192780/192.98 = 998.9636 cents.
// 85.68 × 30 = 2570.4 credits; 257040/192.98 = 1331.9515 cents.
let cost = video_ref_1080(20, Some(10)).calculate_consumer_costs();
assert_eq!(cost.usd_cents_rounded_up, 999);
assert_eq!(cost.usd_cents_rounded_down, 998);
assert!((cost.usd_cents_fractional - 998.9636).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1332);
assert_eq!(cost.usd_cents_rounded_down, 1331);
assert!((cost.usd_cents_fractional - 1331.9515).abs() < FLOAT_TOLERANCE);
}
}
@@ -700,7 +698,7 @@ mod tests {
#[test]
fn video_reference_rate_is_cheaper_per_second() {
// 16 < 26, 35 < 59, and 61.69 < 103.25: the with-references rate is
// 16 < 26, 35 < 59, and 81.4 < 136.23: the with-references rate is
// lower per billed second (the input seconds are where the money
// goes). Compare at the minimum billed input (1s clamps to 4) —
// unknown or zero input assumes the 30-second maximum, which would
@@ -75,7 +75,10 @@ pub fn hydrate_to_router_request(
},
duration_seconds: request.duration_seconds,
video_batch_count: request.video_batch_count,
generate_audio: request.generate_audio,
// Seedance 2.0 / 2.5 always generate sound. Kinovi honours an explicit
// `generate_audio: false`, and the frontend was sending one, so the flag
// is dropped here and never reaches Kinovi for these models.
generate_audio: if is_seedance_2x(&model) { None } else { request.generate_audio },
// Set by the handler after probing reference video durations (only
// models that bill input seconds use it).
total_reference_video_input_seconds: None,
@@ -84,6 +87,24 @@ pub fn hydrate_to_router_request(
})
}
fn is_seedance_2x(model: &RouterVideoModel) -> bool {
matches!(
model,
RouterVideoModel::Seedance2p0
| RouterVideoModel::Seedance2p0Fast
| RouterVideoModel::Seedance2p0Mini
| RouterVideoModel::Seedance2p0BytePlus
| RouterVideoModel::Seedance2p0BytePlusFast
| RouterVideoModel::Seedance2p0BytePlusMini
| RouterVideoModel::Seedance2p0BytePlusUltra
| RouterVideoModel::Seedance2p0BytePlusUltraFast
| RouterVideoModel::Seedance2p0BytePlusUltraMini
| RouterVideoModel::Seedance2p5Preview
| RouterVideoModel::Seedance2p5
| RouterVideoModel::Seedance2p5Ultra
)
}
fn convert_model(
model: &CommonVideoModelEnum,
) -> Result<RouterVideoModel, CommonWebError> {
@@ -144,7 +165,7 @@ mod tests {
use serde_json::{json, Value};
#[tokio::test]
async fn artcraft_http_request_reaches_kinovi_with_format_and_audio() {
async fn artcraft_http_request_reaches_kinovi_with_format_and_without_audio_flag() {
// No media inputs or send: finalization needs a client but makes no HTTP calls.
let client = RouterClient::KinoviWeb(RouterKinoviWebClient::new(
KinoviWebSession::from_cookies_string(String::new()),
@@ -183,7 +204,7 @@ mod tests {
});
let expected_format = output_format.as_ref().and_then(Value::as_str).unwrap_or("mp4");
assert_eq!(actual_format, Some(expected_format), "model: {model}");
assert_eq!(request.maybe_generate_audio, generate_audio);
assert_eq!(request.maybe_generate_audio, None, "generate_audio: {generate_audio:?}");
}
}
}
@@ -210,6 +231,44 @@ mod tests {
}
}
mod generate_audio {
use super::*;
#[test]
fn seedance_2x_drops_generate_audio() {
for model in [
CommonVideoModelEnum::Seedance2p0,
CommonVideoModelEnum::Seedance2p0Fast,
CommonVideoModelEnum::Seedance2p0Mini,
CommonVideoModelEnum::Seedance2p5,
CommonVideoModelEnum::Seedance2p5Ultra,
] {
for generate_audio in [None, Some(true), Some(false)] {
let request = OmniGenVideoCostAndGenerateRequest {
model: Some(model),
generate_audio,
..base_request()
};
let builder = hydrate_to_router_request(&request).expect("hydrate should succeed");
assert_eq!(builder.generate_audio, None, "model: {model:?}, generate_audio: {generate_audio:?}");
}
}
}
#[test]
fn other_models_keep_generate_audio() {
for generate_audio in [None, Some(true), Some(false)] {
let request = OmniGenVideoCostAndGenerateRequest {
model: Some(CommonVideoModelEnum::Veo3p1),
generate_audio,
..base_request()
};
let builder = hydrate_to_router_request(&request).expect("hydrate should succeed");
assert_eq!(builder.generate_audio, generate_audio);
}
}
}
mod bitrate_hydration {
use super::*;
@@ -12,8 +12,8 @@ use enums::common::generation::common_video_model::CommonVideoModel;
use artcraft_api_keys::ArtcraftApiKey;
use crate::http_server::endpoints::omni_gen::generate::video::tests::support::{
base_generate_request, to_omni_api_request, ExpectedCredits, Seconds, TestHarness,
STARTING_CREDITS,
base_generate_request, stub_traffic, to_omni_api_request, ExpectedCredits, Seconds,
TestHarness, STARTING_CREDITS,
};
mod billing_parity {
@@ -46,6 +46,26 @@ mod billing_parity {
}
}
mod generate_audio {
use super::*;
/// Seedance 2.0 / 2.5 always produce sound. Even when a caller explicitly
/// sends `generate_audio: false`, neither endpoint forwards the flag to
/// Kinovi — the key must be absent from the captured request body so Kinovi
/// falls back to its default of sound on.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn seedance_2p0_never_forwards_generate_audio_on_either_endpoint() {
assert_endpoints_omit_generate_audio(CommonVideoModel::Seedance2p0).await;
}
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn seedance_2p5_never_forwards_generate_audio_on_either_endpoint() {
assert_endpoints_omit_generate_audio(CommonVideoModel::Seedance2p5).await;
}
}
mod url_ingestion {
use super::*;
@@ -195,3 +215,47 @@ async fn assert_endpoints_bill_identically(
"{:?}: omni_api debited the wrong amount", model,
);
}
/// Run `model` with an explicit `generate_audio: false` through omni_gen
/// (session cookie) and omni_api (API key); assert both succeed and neither
/// captured Kinovi request carries a `generate_audio` key.
async fn assert_endpoints_omit_generate_audio(model: CommonVideoModel) {
let harness = TestHarness::create().await;
// omni_gen, session-cookie user.
let web_user = harness.create_funded_user(STARTING_CREDITS).await;
let mut request = base_generate_request(model);
let web_prompt = format!("audio flag test web {}", request.idempotency_token.as_deref().unwrap());
request.prompt = Some(web_prompt.clone());
request.generate_audio = Some(false);
let response = harness
.post_generate(&web_user, request)
.await
.unwrap_or_else(|err| panic!("{:?}: omni_gen generation failed: {:?}", model, err));
assert!(response.success);
// omni_api, API-key user.
let api_user = harness.create_funded_user(STARTING_CREDITS).await;
let api_key = harness.create_api_key(&api_user).await;
let mut request = base_generate_request(model);
let api_prompt = format!("audio flag test api {}", request.idempotency_token.as_deref().unwrap());
request.prompt = Some(api_prompt.clone());
request.generate_audio = Some(false);
let response = harness
.post_generate_via_api_key(&api_key, request)
.await
.unwrap_or_else(|err| panic!("{:?}: omni_api generation failed: {:?}", model, err));
assert!(response.success);
let traffic = stub_traffic().lock().unwrap();
for (endpoint, prompt) in [("omni_gen", &web_prompt), ("omni_api", &api_prompt)] {
let params = traffic.workflows.iter()
.map(|request| &request["0"]["json"]["apiParams"])
.find(|params| params["prompt"].as_str() == Some(prompt))
.unwrap_or_else(|| panic!("{:?}: no captured Kinovi request for {}", model, endpoint));
assert!(
params.get("generate_audio").is_none(),
"{:?}: {} forwarded generate_audio to Kinovi: {}", model, endpoint, params,
);
}
}
@@ -74,7 +74,8 @@ async fn quicktime_uploads_are_video_references_with_mp4_or_mov_output() {
.find(|params| params["prompt"].as_str() == Some(&prompt))
.expect("captured Kinovi request");
assert_eq!(params["output_format"], expected);
assert_eq!(params["generate_audio"], true);
// Seedance 2.x never forwards the audio flag; Kinovi defaults to sound on.
assert!(params.get("generate_audio").is_none(), "{}", params);
assert_eq!(params["mode"], "reference");
assert!(params["videoUrls"][0].as_str().is_some_and(|url| url.ends_with(".mov")), "{}", params);
assert_eq!(traffic.uploads.get("fixture.mov").unwrap(), &bytes);
@@ -4,12 +4,12 @@
//!
//! Rate cards (ceil-rounded once):
//! - Seedance 2.5: 480p 11.76954733 ¢/s, 720p 26.70781893 ¢/s,
//! 1080p 45.85869386 ¢/s; with video refs 480p 7.24279835 ¢/s,
//! 720p 15.84362140 ¢/s, 1080p 27.39973680 ¢/s over (output + input)
//! 1080p 61.62732357 ¢/s; with video refs 480p 7.24279835 ¢/s,
//! 720p 15.84362140 ¢/s, 1080p 38.49728574 ¢/s over (output + input)
//! seconds
//! - Seedance 2.5 Ultra: 480p 13.90946502 ¢/s, 720p 31.56378601 ¢/s,
//! 1080p 50.10486922 ¢/s; with video refs 480p 8.55967078 ¢/s,
//! 720p 18.72427984 ¢/s, 1080p 29.93674947 ¢/s
//! 1080p 67.22980753 ¢/s; with video refs 480p 8.55967078 ¢/s,
//! 720p 18.72427984 ¢/s, 1080p 41.84487580 ¢/s
//!
//! Both models offer 480p/720p/1080p (4K downgrades to 1080p), durations
//! run 4-30s, and every request generates a single video regardless of the
@@ -90,60 +90,60 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(748)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(775)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(184)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(276)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(322)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(367)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(413)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(459)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(505)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(551)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(597)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(643)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(688)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(734)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(780)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(826)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(872)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(918)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(964)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1055)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1101)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1147)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1193)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1239)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1285)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1330)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1376)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(184)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(276)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(322)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(367)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(413)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(459)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(505)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(551)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(597)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(643)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(688)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(734)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(780)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(826)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(872)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(918)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(964)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1055)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1101)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1147)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1193)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1239)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1285)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1330)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1376)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(247)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(370)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(432)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(494)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(555)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(617)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(678)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(863)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(925)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1110)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1171)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1295)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1356)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1418)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1541)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1603)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1664)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1726)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1788)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1849)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(247)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(370)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(432)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(494)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(555)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(617)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(678)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(863)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(925)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1110)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1171)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1295)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1356)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1418)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1541)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1603)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1664)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1726)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1788)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1849)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -182,26 +182,26 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(134)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(309)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -230,18 +230,18 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(294)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(348)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(401)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(276)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(322)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(413)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(505)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(597)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(688)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(276)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(322)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(413)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(505)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(597)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(688)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(370)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(432)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(555)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(678)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(925)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(370)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(432)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(555)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(678)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(925)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -313,60 +313,60 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(919)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(935)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(951)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(932)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1014)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1042)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1069)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1096)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1151)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1179)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1206)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1261)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1316)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1343)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1370)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1398)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1453)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1535)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1562)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1590)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1644)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(932)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1014)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1042)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1069)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1096)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1151)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1179)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1206)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1261)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1316)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1343)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1370)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1398)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1453)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1535)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1562)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1590)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1644)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1386)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1463)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1502)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1540)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1579)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1656)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1694)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1733)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1771)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1810)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1848)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1887)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1964)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(2002)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(2041)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(2079)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(2118)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(2156)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(2195)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(2233)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(2272)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2310)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1386)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1463)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1502)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1540)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1579)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1656)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1694)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1733)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1771)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1810)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1848)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1887)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1964)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(2002)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(2041)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(2079)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(2118)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(2156)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(2195)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(2233)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(2272)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2310)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -405,26 +405,26 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(555)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1348)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -454,18 +454,18 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(650)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(682)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(713)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(987)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1014)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1069)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1179)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1014)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1069)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1124)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1179)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1233)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(1386)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1502)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1579)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1656)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1733)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1386)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1502)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1579)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1656)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1733)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -546,60 +546,60 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(884)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(916)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(947)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(201)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(301)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(351)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(401)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(451)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(502)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(552)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(602)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(652)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(702)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(752)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(852)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(902)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(952)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1003)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1053)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1103)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1153)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1203)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1253)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1303)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1353)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1403)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1454)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1504)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(201)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(301)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(351)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(401)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(451)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(502)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(552)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(602)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(652)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(702)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(752)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(852)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(902)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(952)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1003)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1053)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1103)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1153)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1203)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1253)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1303)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1353)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1403)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1454)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1504)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(269)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(404)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(471)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(538)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(606)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(673)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(807)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(874)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(942)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1076)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1143)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1211)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1278)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1345)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1412)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1547)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1614)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1681)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1748)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1816)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1883)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1950)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2017)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(269)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(404)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(471)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(538)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(606)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(673)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(807)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(874)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(942)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1076)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1143)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1211)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1278)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1345)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1412)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1547)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1614)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1681)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1748)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1816)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1883)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1950)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2017)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -638,26 +638,26 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(158)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(337)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -686,18 +686,18 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(348)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(411)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(474)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(301)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(351)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(451)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(552)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(652)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(752)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(301)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(351)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(451)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(552)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(652)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(752)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(404)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(471)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(606)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(874)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(404)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(471)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(606)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(874)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1009)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -769,60 +769,60 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(1087)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(1105)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1018)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1078)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1108)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1138)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1168)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1198)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1228)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1258)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1318)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1378)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1408)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1437)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1467)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1497)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1527)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1557)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1587)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1647)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1677)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1707)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1737)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1767)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1797)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1018)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1078)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1108)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1138)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1168)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1198)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1228)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1258)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1318)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1378)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1408)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1437)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1467)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1497)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1527)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1557)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1587)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1647)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1677)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1707)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1737)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1767)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1797)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1423)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1549)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1591)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1632)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1674)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1716)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1758)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1800)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1842)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1884)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1967)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(2009)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(2051)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(2093)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(2135)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(2176)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(2218)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(2260)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(2302)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(2344)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(2386)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(2428)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(2469)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2511)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1423)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1549)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1591)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1632)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1674)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1716)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1758)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1800)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1842)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1884)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1967)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(2009)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(2051)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(2093)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(2135)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(2176)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(2218)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(2260)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(2302)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(2344)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(2386)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(2428)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(2469)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2511)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -861,26 +861,26 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(656)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1465)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -910,18 +910,18 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(768)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(806)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(843)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(1078)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1108)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1168)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1228)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1078)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1108)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1168)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1228)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1549)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1632)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1716)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1800)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1884)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1549)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1632)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1716)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1800)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1884)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -1059,7 +1059,7 @@ mod input_video_duration_billing {
// 6.5s → 7 → 37 billed × 7.24279835 = 267.98 → 268
// 10.5s → 11 → 41 billed × 7.24279835 = 296.95 → 297
// 6.5s → 7 → 37 billed × 15.84362140 = 586.21 → 587 (720p)
// 6.5s → 7 → 37 billed × 27.39973680 = 1013.79 → 1014 (1080p)
// 6.5s → 7 → 37 billed × 38.49728574 = 1424.40 → 1425 (1080p)
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5, Some(CommonResolution::FourEightyP),
Seconds(30), &[4_500], ExpectedCredits(254),
@@ -1078,7 +1078,7 @@ mod input_video_duration_billing {
).await;
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5, Some(CommonResolution::TenEightyP),
Seconds(30), &[6_500], ExpectedCredits(1014),
Seconds(30), &[6_500], ExpectedCredits(1425),
).await;
}
@@ -1122,12 +1122,12 @@ mod input_video_duration_billing {
let harness = TestHarness::create().await;
// 30s output, ultra with-refs 480p 8.55967078 ¢/s / 720p 18.72427984 ¢/s
// / 1080p 29.93674947 ¢/s:
// / 1080p 41.84487580 ¢/s:
// 2.5s → total 3 → clamps to 4 → 34 billed → 291.03 → 292
// 10.5s → 11 → 41 billed → 350.95 → 351
// 2.5s + 6.5s → 3+7=10 → 40 billed → 342.39 → 343 (total-based, no per-video clamp)
// 6.5s → 7 → 37 billed × 18.72427984 = 692.80 → 693 (720p)
// 6.5s → 7 → 37 billed × 29.93674947 = 1107.66 → 1108 (1080p)
// 6.5s → 7 → 37 billed × 41.84487580 = 1548.26 → 1549 (1080p)
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5Ultra, Some(CommonResolution::FourEightyP),
Seconds(30), &[2_500], ExpectedCredits(292),
@@ -1146,7 +1146,7 @@ mod input_video_duration_billing {
).await;
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5Ultra, Some(CommonResolution::TenEightyP),
Seconds(30), &[6_500], ExpectedCredits(1108),
Seconds(30), &[6_500], ExpectedCredits(1549),
).await;
}
}
@@ -1172,10 +1172,10 @@ mod premium {
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(268), ExpectedCredits(316), CreditsDelta(48)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(401), ExpectedCredits(474), CreditsDelta(73)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(802), ExpectedCredits(947), CreditsDelta(145)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230), ExpectedCredits(251), CreditsDelta(21)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1376), ExpectedCredits(1504), CreditsDelta(128)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309), ExpectedCredits(337), CreditsDelta(28)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1849), ExpectedCredits(2017), CreditsDelta(168)),
// 4K downgrades to 1080p on both models, so the premium matches 1080p.
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230), ExpectedCredits(251), CreditsDelta(21)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309), ExpectedCredits(337), CreditsDelta(28)),
];
for (resolution, seconds, batch, base, variant, delta) in cases {
+1 -1
View File
@@ -60,7 +60,7 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/svg+xml" href="/artcraft-icon.svg" />
<link rel="stylesheet" href="/src/styles.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.34 97.5">
<defs>
<style>
.cls-1 {
fill: #4e7bfb;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<path class="cls-1" d="M104.28,49.49L81.55,0h-31.23l-3.17,4.76L14.75,53.63,0,75.85l21.55,21.55,63.79-36.94,16.99,37.04,14.01-21.74-12.06-26.27ZM32.89,65.66l32.42-48.87,10.91,23.77-43.32,25.09Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 483 B

@@ -0,0 +1,9 @@
export const AUTH_LABEL_CLASSES =
"block font-mono text-[11px] font-semibold leading-4 uppercase tracking-[0.12em] text-white/70";
export const AUTH_INPUT_CLASSES =
"h-10 w-full rounded-[3px] border border-white/15 bg-white/5 px-4 py-2 text-sm font-normal leading-5 text-white placeholder-white/20 outline-none transition-colors focus:border-white/40";
export const AUTH_PASSWORD_INPUT_CLASSES = `${AUTH_INPUT_CLASSES} pr-12`;
export const AUTH_FORM_PADDING = "px-6 py-10 sm:px-10 sm:py-12";
@@ -1,11 +1,11 @@
import { AUTH_FORM_PADDING } from "./auth-form-styles";
import { ReactNode } from "react";
import { Outlet, useNavigate, Navigate } from "react-router-dom";
import { ArrowLeftIcon, LoaderCircleIcon } from "lucide-react";
import { TruchetPattern } from "@storyteller/ui-vfx";
import { Outlet, Navigate, Link } from "react-router-dom";
import { LoaderCircleIcon } from "lucide-react";
import { AuthShowcase } from "./auth-showcase";
import { AuthPageFrame } from "./auth-page-frame";
import { useMediaQuery } from "../ui/use-media-query";
import { useSession } from "../../lib/session";
import { Reveal, RevealGroup } from "../motion/reveal";
/**
* Persistent shell for the auth pages. Rendered as a layout route so the
@@ -18,52 +18,21 @@ export const AuthLayout = () => {
// Only mount the showcase on wide screens (matches the `lg` breakpoint) so
// mobile never downloads the demo videos.
const showShowcase = useMediaQuery("(min-width: 1024px)");
const navigate = useNavigate();
const { loggedIn, authChecked } = useSession();
const handleBack = () => {
navigate("/");
};
// Already signed in? Never show the auth form — go straight home.
if (loggedIn) {
return <Navigate to="/" replace />;
}
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-ui-background p-4 text-white">
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0"
style={{
maskImage:
"radial-gradient(ellipse 70% 60% at 50% 40%, black 25%, transparent 80%)",
WebkitMaskImage:
"radial-gradient(ellipse 70% 60% at 50% 40%, black 25%, transparent 80%)",
}}
>
<TruchetPattern
intensity={0.5}
className="absolute inset-0 h-full w-full"
/>
</div>
<div className="relative z-10 flex w-full max-w-5xl overflow-hidden border border-white/15 bg-[#101014] lg:min-h-[640px]">
<AuthPageFrame wide>
{/* Form pane — only the inner content swaps per route (via Outlet), so
this pane and the showcase beside it stay mounted across the
login/signup toggle. */}
<div className="relative flex w-full flex-col lg:w-1/2">
<button
type="button"
onClick={handleBack}
aria-label="Go back"
className="absolute left-5 top-5 z-20 flex h-9 w-9 items-center justify-center text-white/60 transition-colors hover:bg-white/10 hover:text-white"
>
<ArrowLeftIcon />
</button>
<div className="flex flex-1 flex-col justify-center px-8 py-10 sm:px-10">
<div className="mx-auto w-full max-w-sm">
<div className={`flex flex-1 flex-col justify-center ${AUTH_FORM_PADDING}`}>
<div className="w-full">
{authChecked ? (
<Outlet />
) : (
@@ -76,44 +45,39 @@ export const AuthLayout = () => {
</div>
</div>
<div className="px-8 pb-8 text-center text-xs text-white/20">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
{/* Showcase pane (desktop only) */}
{showShowcase && (
<div className="relative lg:w-1/2">
<div className="relative border-l border-white/15 lg:w-1/2">
<AuthShowcase />
</div>
)}
</div>
</div>
</AuthPageFrame>
);
};
interface AuthHeaderProps {
title: string;
title: ReactNode;
subtitle: string;
}
export const AuthHeader = ({ title, subtitle }: AuthHeaderProps) => (
<RevealGroup inView={false} stagger={0.08} className="mb-8 text-center">
<Reveal y={10}>
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
</Reveal>
<Reveal as="h1" y={10} className="mb-2 text-2xl font-semibold">
<div className="mb-8 text-left">
<Link
to="/"
aria-label="ArtCraft home"
className="mb-8 inline-flex rounded-sm transition-opacity hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white/60"
>
<img src="/artcraft-icon.svg" alt="ArtCraft" className="h-8 w-8" />
</Link>
<h1 className="mb-3 text-balance font-display text-3xl leading-[1.05] tracking-tight sm:text-4xl">
{title}
</Reveal>
<Reveal as="p" y={10} className="text-sm text-white/60">
</h1>
<p className="text-sm leading-relaxed text-white/60">
{subtitle}
</Reveal>
</RevealGroup>
</p>
</div>
);
export const AuthFooter = ({ children }: { children: ReactNode }) => (
@@ -0,0 +1,22 @@
import { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { FrameCornerMarks } from "../ui/frame-corner-marks";
/** A contained section frame shared by the auth pages. */
export function AuthPageFrame({ children, wide = false }: {
children: ReactNode;
wide?: boolean;
}) {
return (
<div className="relative isolate flex min-h-dvh items-center justify-center overflow-hidden bg-ui-background p-4 text-white sm:py-10">
<div className={twMerge("relative w-full", wide ? "max-w-lg lg:max-w-5xl" : "max-w-lg")}>
<div className="relative border border-white/20 bg-ui-controls">
<div className={twMerge("flex", wide ? "lg:min-h-[560px]" : "flex-col")}>
{children}
</div>
</div>
<FrameCornerMarks />
</div>
</div>
);
}
@@ -3,7 +3,7 @@ const FEATURED_VIDEO_URL =
export const AuthShowcase = () => {
return (
<div className="absolute inset-2 overflow-hidden bg-black">
<div className="absolute inset-0 overflow-hidden bg-ui-controls">
{/* Muted background video (no controls) cropped to cover the pane */}
<video
src={FEATURED_VIDEO_URL}
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES } from "./auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -14,7 +15,6 @@ import {
import { refreshSession } from "../../lib/session";
import { hasActiveSubscription } from "../../lib/billing";
import { GoogleLoginButton } from "./GoogleLoginButton";
import { Reveal, RevealGroup } from "../motion/reveal";
interface SignupFormProps {
onSuccess: (isNewUser?: boolean) => void;
@@ -98,7 +98,7 @@ export const SignupForm = ({
};
return (
<div className={`space-y-4 ${className}`}>
<div className={className}>
<form
className="space-y-4"
onSubmit={(e) => {
@@ -111,19 +111,11 @@ export const SignupForm = ({
{error}
</div>
)}
{/* Fields cascade in as their own beat, leading with the email field on
an almost-zero delay so the primary input lands immediately — the
entrance never gates typing (inputs stay focusable mid-cascade).
Mirrors the login page. */}
<RevealGroup
inView={false}
delayChildren={0.04}
stagger={0.08}
<div
className="space-y-4"
>
<Reveal className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
<div className="space-y-2">
<label className={AUTH_LABEL_CLASSES}>
Email
</label>
<Input
@@ -132,13 +124,13 @@ export const SignupForm = ({
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
autoFocus={autoFocus}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 rounded-[3px] px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</Reveal>
</div>
<Reveal className="space-y-2">
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
</div>
@@ -148,7 +140,7 @@ export const SignupForm = ({
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 rounded-[3px] px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -159,9 +151,9 @@ export const SignupForm = ({
<DynamicIcon icon={showPassword ? EyeOffIcon : EyeIcon} />
</button>
</div>
</Reveal>
</div>
<Reveal className="pt-2">
<div className="pt-2">
<Button
className="w-full justify-center h-10"
type="submit"
@@ -173,36 +165,25 @@ export const SignupForm = ({
"Create account"
)}
</Button>
</Reveal>
</RevealGroup>
</div>
</div>
</form>
{/* Secondary sign-up options continue the same cascade just after the
fields (delay continues from the form group's three children above:
0.04 + 3 × 0.08). */}
<RevealGroup
inView={false}
delayChildren={0.28}
stagger={0.08}
className="space-y-4"
>
<Reveal className="relative flex items-center justify-center py-2">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/10" />
</div>
<span className="relative bg-[#101014] px-4 font-mono text-[11px] uppercase tracking-[0.12em] text-white/40">
<div>
<div className="my-6 flex items-center gap-4 before:h-px before:flex-1 before:bg-white/15 after:h-px after:flex-1 after:bg-white/15">
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
or
</span>
</Reveal>
</div>
<Reveal>
<div>
<GoogleLoginButton
mode="signup"
onSuccess={handleGoogleSuccess}
onError={handleGoogleError}
/>
</Reveal>
</RevealGroup>
</div>
</div>
</div>
);
};
@@ -0,0 +1,25 @@
import { type ReactNode } from "react";
import { FrameCornerMarks } from "../ui/frame-corner-marks";
interface CreateEmptyStateProps {
title: string;
subtitle: string;
children?: ReactNode;
compact?: boolean;
}
/** A quiet editorial section with a fine border and corner registration marks. */
export function CreateEmptyState({ title, subtitle, children, compact = false }: CreateEmptyStateProps) {
return (
<section className={`create-empty-section relative w-full max-w-3xl px-6 text-left sm:px-10 ${compact ? "py-8" : "py-10 sm:py-14"}`}>
<h1 className={`text-balance font-display leading-[1.05] tracking-[-0.035em] text-ui-ink ${compact ? "text-2xl" : "text-3xl md:text-5xl"}`}>
{title}
</h1>
<p className="mt-4 max-w-xl text-pretty text-sm leading-relaxed text-white/60 sm:text-base">
{subtitle}
</p>
{children && <div className="mt-6">{children}</div>}
<FrameCornerMarks />
</section>
);
}
@@ -1,9 +1,8 @@
import { CreateEmptyState } from "./CreateEmptyState";
import { type ReactNode, useState } from "react";
import { LoaderCircleIcon } from "lucide-react";
import { isMobile } from "react-device-detect";
import { type PopoverItem } from "@storyteller/ui-popover";
import { TabSelector } from "@storyteller/ui-tab-selector";
import { TruchetPattern } from "@storyteller/ui-vfx";
import Seo from "../../components/seo";
import { useIsMobile } from "../ui/use-mobile";
import {
@@ -20,7 +19,7 @@ interface CreateMediaPageShellProps {
// SEO
title: string;
description: string;
// Auth state — `authChecked` gates the initial spinner so we don't flash
// Auth state - `authChecked` gates the initial spinner so we don't flash
// logged-out chrome while the session resolves. Pages stay viewable for
// logged-out users; the signup CTA modal is triggered at generate time.
authChecked: boolean;
@@ -41,8 +40,6 @@ interface CreateMediaPageShellProps {
// Model selector
modelItems: PopoverItem[];
onModelChange: (item: PopoverItem) => void;
// Glow orb overrides (optional - defaults provided)
glowOrbs?: ReactNode;
// Children slots
gridContent: ReactNode;
promptBox: ReactNode;
@@ -69,7 +66,6 @@ export function CreateMediaPageShell({
emptyStateSubtitle,
emptyStateCta,
bottomOffset,
glowOrbs,
gridContent,
promptBox,
promptForm,
@@ -103,9 +99,9 @@ export function CreateMediaPageShell({
<Seo title={title} description={description} />
{/* Flex (not absolute) keeps the toggle cluster from overlapping
the tabs — the tabs stay centered via the equal flex-1 sides and
the tabs - the tabs stay centered via the equal flex-1 sides and
just shift left if the cluster outgrows its half. */}
<div className="flex items-center border-b border-ui-panel-border px-3 py-2">
<div className="flex flex-wrap items-center gap-y-2 border-b border-ui-panel-border bg-ui-panel px-3 py-2">
<div className="flex-1" />
<TabSelector
tabs={MOBILE_TABS}
@@ -132,13 +128,8 @@ export function CreateMediaPageShell({
<div className="px-3">{gridContent}</div>
</div>
) : (
<div className="flex h-full flex-col items-center justify-center px-6 text-center">
<span className="text-lg font-semibold text-white">
{emptyStateTitle}
</span>
<span className="pt-1 text-sm text-white/60">
Your generations will appear here.
</span>
<div className="flex h-full items-center justify-center overflow-y-auto px-6 py-8">
<CreateEmptyState compact title={emptyStateTitle} subtitle="Your generations will appear here." />
</div>
)}
</div>
@@ -150,51 +141,16 @@ export function CreateMediaPageShell({
}
return (
<div className="flex h-full w-full bg-ui-background text-white">
<div className="relative isolate flex h-full w-full bg-ui-background text-white">
<Seo title={title} description={description} />
{/* Decorative background, empty state only. Not rendered on mobile
devices: the truchet uses mask-image, a full-screen GPU layer that
iOS Safari must re-rasterize when an overlay (mobile menu, modal)
composites over it, causing multi-second hangs. (This is distinct
from backdrop-filter.) We gate on the actual device
(react-device-detect's user-agent check) rather than a viewport
breakpoint so the layers never mount on a real phone/tablet, and a
narrow desktop window still gets the decoration. */}
{!hasContent && !isMobile && (
<>
{glowOrbs}
<div
aria-hidden
className="pointer-events-none fixed inset-0 z-0"
style={{
maskImage:
"radial-gradient(ellipse 70% 60% at 50% 50%, black 20%, transparent 80%)",
WebkitMaskImage:
"radial-gradient(ellipse 70% 60% at 50% 50%, black 20%, transparent 80%)",
}}
>
<TruchetPattern
intensity={0.5}
className="absolute inset-0 h-full w-full"
/>
</div>
</>
)}
<div className="relative z-[1] h-full w-full">
<div className="flex h-full w-full flex-col">
{!hasContent && (
<div className="flex flex-1 items-center justify-center">
<div className="animate-fade-in-up relative z-20 mb-32 flex flex-col items-center justify-center text-center">
<h1 className="text-5xl font-semibold text-white md:text-7xl">
{emptyStateTitle}
</h1>
<span className="pt-2 text-lg text-white/80 md:text-xl">
{emptyStateSubtitle}
</span>
{emptyStateCta && <div className="pt-6">{emptyStateCta}</div>}
</div>
<div className="flex flex-1 items-center justify-center px-6" style={{ paddingBottom: bottomOffset }}>
<CreateEmptyState title={emptyStateTitle} subtitle={emptyStateSubtitle}>
{emptyStateCta}
</CreateEmptyState>
</div>
)}
@@ -45,11 +45,11 @@ import {
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 last:border-0">
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60">
<div className="flex items-start justify-between gap-4 px-4 py-3 border-b border-white/10 last:border-0">
<span className="shrink-0 pt-0.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60">
{label}
</span>
<span className="text-sm text-white font-medium flex items-center gap-2">
<span className="min-w-0 text-right text-sm text-white font-medium flex items-center justify-end gap-2">
{value}
</span>
</div>
@@ -426,7 +426,14 @@ export function LightboxDetails({
{createdAt && (
<InfoRow
label="Created"
value={dayjs(createdAt).format("MMMM D, YYYY h:mm:ss A")}
value={
<time dateTime={createdAt} className="flex flex-col items-end gap-0.5 tabular-nums">
<span>{dayjs(createdAt).format("MMM D, YYYY")}</span>
<span className="text-xs text-white/60 whitespace-nowrap">
{dayjs(createdAt).format("h:mm:ss A")}
</span>
</time>
}
/>
)}
</div>
@@ -357,7 +357,7 @@ export function Lightbox({
<Modal
isOpen={isOpen}
onClose={onClose}
className="h-[90vh] sm:h-[680px] w-full sm:w-[1100px] max-w-[95vw] max-h-[90vh] p-0 border-white/15"
className="h-[90dvh] sm:h-[92dvh] w-full sm:w-[1440px] max-w-[96vw] max-h-[92dvh] p-0 border-white/15"
backdropClassName="!bg-black/80"
showClose={false}
>
@@ -1,5 +1,6 @@
import { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { NEUTRAL_BUTTON_HOVER_CLASSES } from "@storyteller/ui-button";
import { ChevronRightIcon } from "lucide-react";
interface MobileFieldButtonProps {
@@ -27,7 +28,8 @@ export function MobileFieldButton({
disabled={disabled}
className={twMerge(
"flex w-full items-center gap-3 border border-ui-panel-border bg-ui-controls px-3.5 py-3 text-left transition-colors",
"hover:bg-ui-controls/80 active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-50",
NEUTRAL_BUTTON_HOVER_CLASSES,
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-ui-controls disabled:hover:border-ui-panel-border",
className,
)}
>
@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { Link, useLocation } from "react-router-dom";
import { motion, useReducedMotion } from "framer-motion";
import { SidebarActiveIndicator } from "./sidebar-active-indicator";
import {
BoxIcon,
CircleHelpIcon,
@@ -161,7 +161,6 @@ function NavMenuItem({
pathname: string;
onClick: () => void;
}) {
const reduceMotion = useReducedMotion();
const active = !item.external && isActive(pathname, item.href);
const inner = (
<>
@@ -187,21 +186,7 @@ function NavMenuItem({
);
return (
<SidebarMenuItem>
{/* Brand accent bar that glides between nav rows as the active route
changes (shared-element layout animation via `layoutId`). One is
mounted at a time, so framer-motion tweens it from the old row to the
new one. */}
{active && (
<motion.span
layoutId="sidebar-active-indicator"
className="pointer-events-none absolute inset-y-1.5 left-0 z-10 w-0.5 bg-white"
transition={
reduceMotion
? { duration: 0 }
: { type: "spring", stiffness: 520, damping: 40, mass: 0.7 }
}
/>
)}
{active && <SidebarActiveIndicator />}
<SidebarMenuButton asChild isActive={active} tooltip={item.label}>
{item.external ? (
<a
@@ -1,3 +1,4 @@
import { SidebarActiveIndicator } from "./sidebar-active-indicator";
import { useMemo, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { AnimatePresence, motion } from "framer-motion";
@@ -90,6 +91,7 @@ export function LibraryFoldersNav({
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
{onUnsorted && <SidebarActiveIndicator />}
<SidebarMenuButton asChild isActive={onUnsorted} tooltip="Library">
<Link to="/library" onClick={onNavClick}>
<Grid3x3Icon />
@@ -98,6 +100,7 @@ export function LibraryFoldersNav({
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
{onFolderless && <SidebarActiveIndicator />}
<SidebarMenuButton
asChild
isActive={onFolderless}
@@ -110,6 +113,7 @@ export function LibraryFoldersNav({
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
{onFolders && !activeFolderId && <SidebarActiveIndicator />}
<SidebarMenuButton
asChild
isActive={onFolders && !activeFolderId}
@@ -147,8 +151,9 @@ export function LibraryFoldersNav({
<ul className="flex w-full min-w-0 flex-col gap-0.5">
{rootFolders.map((folder) => (
<SidebarMenuItem key={folder.id}>
{onFolders && activeRootId === folder.id && <SidebarActiveIndicator />}
<SidebarMenuButton
isActive={activeRootId === folder.id}
isActive={onFolders && activeRootId === folder.id}
tooltip={folder.name}
data-folder-id={folder.id}
onClick={() => goToFolder(folder.id)}
@@ -1,3 +1,4 @@
import { SidebarActiveIndicator } from "./sidebar-active-indicator";
import { useMemo, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { AnimatePresence, motion } from "framer-motion";
@@ -48,6 +49,7 @@ export function LibraryTagsNav({
return (
<>
<SidebarMenuItem>
{pathname === "/library/tags" && <SidebarActiveIndicator />}
<SidebarMenuButton
asChild
isActive={pathname === "/library/tags"}
@@ -85,6 +87,7 @@ export function LibraryTagsNav({
<ul className="flex w-full min-w-0 flex-col gap-0.5">
{topTags.map((tag) => (
<SidebarMenuItem key={tag.token}>
{pathname === `/library/${tag.token}` && <SidebarActiveIndicator />}
<SidebarMenuButton
isActive={pathname === `/library/${tag.token}`}
tooltip={tag.value}
@@ -24,16 +24,19 @@ export function MobileBottomNav() {
const { pathname } = useLocation();
return (
<nav className="flex shrink-0 items-stretch border-t border-ui-panel-border bg-ui-panel pb-[env(safe-area-inset-bottom)]">
<nav aria-label="Main navigation" className="flex shrink-0 items-stretch border-t border-ui-panel-border bg-ui-panel pb-[env(safe-area-inset-bottom)]">
{ITEMS.map((item) => {
const active = isActive(pathname, item.href);
return (
<Link
key={item.href}
to={item.href}
aria-current={active ? "page" : undefined}
className={twMerge(
"flex flex-1 flex-col items-center justify-center gap-1 py-2 font-mono text-[10px] font-semibold uppercase tracking-[0.12em] transition-colors",
active ? "text-white" : "text-base-fg/55 hover:text-base-fg/80",
"flex min-h-14 flex-1 flex-col items-center justify-center gap-1 border-t-2 py-2 font-mono text-[10px] font-semibold uppercase tracking-[0.12em] transition-colors",
active
? "border-primary bg-primary/10 text-ui-accent-ink"
: "border-transparent text-base-fg/70 hover:text-base-fg",
)}
>
<DynamicIcon icon={item.icon} className="h-5 w-5" />
@@ -0,0 +1,18 @@
import { motion, useReducedMotion } from "framer-motion";
export function SidebarActiveIndicator() {
const reduceMotion = useReducedMotion();
return (
<motion.span
aria-hidden="true"
layoutId="sidebar-active-indicator"
className="pointer-events-none absolute inset-y-1.5 left-0 z-10 w-0.5 bg-white"
transition={
reduceMotion
? { duration: 0 }
: { type: "spring", stiffness: 520, damping: 40, mass: 0.7 }
}
/>
);
}
@@ -1,5 +1,6 @@
import { useLocation, Link } from "react-router-dom";
import { Fragment } from "react";
import { ChevronRightIcon } from "lucide-react";
type Crumb = { label: string; href?: string };
@@ -77,26 +78,27 @@ export function Breadcrumbs() {
if (crumbs.length === 0) return null;
return (
<nav className="hidden md:flex items-center gap-1.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/45 min-w-0">
<nav aria-label="Breadcrumb" className="hidden md:flex items-center gap-1.5 hud-label text-base-fg/70 min-w-0">
{crumbs.map((crumb, i) => {
const isLast = i === crumbs.length - 1;
return (
<Fragment key={`${crumb.label}-${i}`}>
{i > 0 && (
<span className="text-white/25" aria-hidden>
›
</span>
<ChevronRightIcon
aria-hidden="true"
className="h-3 w-3 shrink-0 text-base-fg/50"
/>
)}
{crumb.href && !isLast ? (
<Link
to={crumb.href}
className="truncate hover:text-white/70 transition-colors"
className="truncate px-1.5 py-1 hover:bg-ui-ink hover:text-ui-panel transition-colors"
>
{crumb.label}
</Link>
) : (
<span
className={isLast ? "text-white/85 truncate" : "truncate"}
className={isLast ? "bg-ui-ink px-1.5 py-1 text-ui-panel truncate" : "truncate"}
aria-current={isLast ? "page" : undefined}
>
{crumb.label}
@@ -24,7 +24,7 @@ export function TopBar() {
const showSelectToggle = showViewToggle;
return (
<header className="sticky top-0 z-20 flex items-center gap-3 border-b border-white/15 bg-ui-background px-3 pb-4 pt-3 sm:pt-6">
<header className="sticky top-0 z-20 flex items-center gap-3 border-b border-ui-border bg-ui-panel px-3 py-3 sm:px-5">
{/* Left: sidebar trigger (mobile only) + logo (when sidebar closed) + breadcrumbs */}
<div className="flex items-center gap-2 min-w-0 shrink-0">
<SidebarTrigger className="md:hidden" />
@@ -0,0 +1,8 @@
const CORNERS = ["top-0 left-0", "top-0 right-0", "bottom-0 left-0", "bottom-0 right-0"];
/** Decorative registration marks for a positioned panel. */
export function FrameCornerMarks() {
return CORNERS.map((corner) => (
<span key={corner} aria-hidden="true" className={`frame-corner-mark ${corner}`} />
));
}
@@ -1,5 +1,6 @@
import { CreateEmptyState } from "../../components/generation-gallery/CreateEmptyState";
import { useCallback, useEffect, useRef, useState } from "react";
import { LoaderCircleIcon, SparklesIcon } from "lucide-react";
import { LoaderCircleIcon } from "lucide-react";
import {
PromptBoxVFX,
VFXResultCard,
@@ -267,7 +268,7 @@ export default function CreateVFX() {
className="relative z-10 flex h-full items-center justify-center px-3 sm:px-6"
style={{ paddingBottom: Math.max(promptBoxHeight + 32, 240) }}
>
<EmptyState
<CreateEmptyState
title="No background changes yet"
subtitle="Upload a source video and a reference image, then optionally add a prompt."
/>
@@ -330,18 +331,3 @@ export default function CreateVFX() {
</div>
);
}
interface EmptyStateProps {
title: string;
subtitle: string;
}
const EmptyState = ({ title, subtitle }: EmptyStateProps) => (
<div className="flex max-w-md flex-col items-center gap-4 text-center">
<div className="flex h-12 w-12 items-center justify-center border border-white/15 bg-white/5">
<SparklesIcon className="text-2xl text-white/40" />
</div>
<h3 className="text-2xl font-bold text-white">{title}</h3>
<p className="text-sm text-white/60 max-w-xs">{subtitle}</p>
</div>
);
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import { ArrowLeftIcon, LoaderCircleIcon, MailIcon } from "lucide-react";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
@@ -5,6 +6,8 @@ import { useState } from "react";
import { Link } from "react-router-dom";
import { PasswordResetApi } from "@storyteller/api";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
const ForgotPassword = () => {
@@ -41,28 +44,17 @@ const ForgotPassword = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Reset Password - ArtCraft"
description="Reset your ArtCraft password."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
{!submitted ? (
<>
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">Reset Password</h1>
<p className="text-white/60 text-sm">
Enter your email to receive reset instructions
</p>
</div>
<AuthHeader title={<>Reset your <span className="font-serif-italic">password.</span></>} subtitle="Enter your email or username to receive a reset code." />
<form
className="space-y-4"
@@ -78,7 +70,7 @@ const ForgotPassword = () => {
)}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Email or Username
</label>
<Input
@@ -87,7 +79,7 @@ const ForgotPassword = () => {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</div>
@@ -134,18 +126,14 @@ const ForgotPassword = () => {
<div className="mt-8 text-center text-sm">
<Link
to="/login"
className="text-white/40 hover:text-white transition-colors flex items-center justify-center gap-2"
className="text-white/60 hover:text-white transition-colors flex items-center justify-center gap-2"
>
<ArrowLeftIcon /> Back to Log in
</Link>
</div>
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import {
ArrowLeftIcon,
CircleCheckIcon,
@@ -12,6 +13,8 @@ import { useState } from "react";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { PasswordResetApi, BillingApi } from "@storyteller/api";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
const VerifyReset = () => {
@@ -102,30 +105,17 @@ const VerifyReset = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Verify Password Reset - ArtCraft"
description="Enter your verification code and new password."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
{!success ? (
<>
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">
Password Reset Verification
</h1>
<p className="text-white/60 text-sm">
Enter the code sent to your email address.
</p>
</div>
<AuthHeader title={<>Set a new <span className="font-serif-italic">password.</span></>} subtitle="Enter your reset code and choose a new password." />
<form
className="space-y-4"
@@ -142,7 +132,7 @@ const VerifyReset = () => {
{/* Verification Code */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Verification Code
</label>
<Input
@@ -152,10 +142,10 @@ const VerifyReset = () => {
onChange={(e) => setVerificationCode(e.target.value)}
placeholder="Enter verification code"
isError={!!fieldErrors.verificationCode}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
{fieldErrors.verificationCode && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.verificationCode}
</p>
)}
@@ -163,7 +153,7 @@ const VerifyReset = () => {
{/* New Password */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
New Password
</label>
<div className="relative">
@@ -174,7 +164,7 @@ const VerifyReset = () => {
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password"
isError={!!fieldErrors.newPassword}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -187,7 +177,7 @@ const VerifyReset = () => {
</button>
</div>
{fieldErrors.newPassword && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.newPassword}
</p>
)}
@@ -195,7 +185,7 @@ const VerifyReset = () => {
{/* Verify New Password */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Verify New Password
</label>
<div className="relative">
@@ -206,7 +196,7 @@ const VerifyReset = () => {
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Enter new password again"
isError={!!fieldErrors.confirmPassword}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -221,7 +211,7 @@ const VerifyReset = () => {
</button>
</div>
{fieldErrors.confirmPassword && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.confirmPassword}
</p>
)}
@@ -246,7 +236,7 @@ const VerifyReset = () => {
<div className="mt-8 text-center text-sm">
<Link
to="/forgot-password"
className="text-white/40 hover:text-white transition-colors flex items-center justify-center gap-2"
className="text-white/60 hover:text-white transition-colors flex items-center justify-center gap-2"
>
<ArrowLeftIcon /> Request a new code
</Link>
@@ -277,11 +267,7 @@ const VerifyReset = () => {
)}
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -22,7 +22,7 @@ type AppCard = {
href: string;
icon: LucideIcon;
iconTile: string;
hoverBorder: string;
hoverStyle: string;
badge?: string;
};
@@ -33,7 +33,7 @@ const APPS: AppCard[] = [
href: "/create-image",
icon: ImageIcon,
iconTile: "border-blue-400/40 bg-blue-500/20 text-blue-300",
hoverBorder: "hover:border-blue-400/60",
hoverStyle: "hover:border-blue-400/60 hover:bg-blue-500/10",
},
{
label: "Video",
@@ -41,7 +41,7 @@ const APPS: AppCard[] = [
href: "/create-video",
icon: VideoIcon,
iconTile: "border-purple-400/40 bg-purple-500/20 text-purple-300",
hoverBorder: "hover:border-purple-400/60",
hoverStyle: "hover:border-purple-400/60 hover:bg-purple-500/10",
},
{
label: "Audio",
@@ -49,7 +49,7 @@ const APPS: AppCard[] = [
href: "/create-audio",
icon: MusicIcon,
iconTile: "border-pink-400/40 bg-pink-500/20 text-pink-300",
hoverBorder: "hover:border-pink-400/60",
hoverStyle: "hover:border-pink-400/60 hover:bg-pink-500/10",
},
{
label: "3D Object",
@@ -57,7 +57,7 @@ const APPS: AppCard[] = [
href: "/create-object",
icon: BoxIcon,
iconTile: "border-cyan-400/40 bg-cyan-500/20 text-cyan-300",
hoverBorder: "hover:border-cyan-400/60",
hoverStyle: "hover:border-cyan-400/60 hover:bg-cyan-500/10",
},
{
label: "3D World",
@@ -65,7 +65,7 @@ const APPS: AppCard[] = [
href: "/create-world",
icon: GlobeIcon,
iconTile: "border-teal-400/40 bg-teal-500/20 text-teal-300",
hoverBorder: "hover:border-teal-400/60",
hoverStyle: "hover:border-teal-400/60 hover:bg-teal-500/10",
},
{
label: "Edit 3D",
@@ -73,7 +73,7 @@ const APPS: AppCard[] = [
href: "/edit-3d",
icon: BoxIcon,
iconTile: "border-amber-400/40 bg-amber-500/20 text-amber-300",
hoverBorder: "hover:border-amber-400/60",
hoverStyle: "hover:border-amber-400/60 hover:bg-amber-500/10",
},
{
label: "Background Change",
@@ -81,7 +81,7 @@ const APPS: AppCard[] = [
href: "/background-change",
icon: WandSparklesIcon,
iconTile: "border-emerald-400/40 bg-emerald-500/20 text-emerald-300",
hoverBorder: "hover:border-emerald-400/60",
hoverStyle: "hover:border-emerald-400/60 hover:bg-emerald-500/10",
},
{
label: "Edit Video",
@@ -89,7 +89,7 @@ const APPS: AppCard[] = [
href: "/video-editor",
icon: FilmIcon,
iconTile: "border-rose-400/40 bg-rose-500/20 text-rose-300",
hoverBorder: "hover:border-rose-400/60",
hoverStyle: "hover:border-rose-400/60 hover:bg-rose-500/10",
badge: "BETA",
},
{
@@ -98,7 +98,7 @@ const APPS: AppCard[] = [
href: "/moodboard",
icon: GroupIcon,
iconTile: "border-indigo-400/40 bg-indigo-500/20 text-indigo-300",
hoverBorder: "hover:border-indigo-400/60",
hoverStyle: "hover:border-indigo-400/60 hover:bg-indigo-500/10",
badge: "BETA",
},
{
@@ -107,33 +107,38 @@ const APPS: AppCard[] = [
href: "/frame-extractor",
icon: ImagesIcon,
iconTile: "border-orange-400/40 bg-orange-500/20 text-orange-300",
hoverBorder: "hover:border-orange-400/60",
hoverStyle: "hover:border-orange-400/60 hover:bg-orange-500/10",
badge: "NEW",
},
];
export function Home() {
return (
<div className="min-h-full px-6 sm:px-10 py-10 sm:py-16 max-w-6xl mx-auto w-full">
<div className="min-h-full px-5 py-8 sm:px-8 sm:py-12 max-w-6xl mx-auto w-full">
<Seo
title="ArtCraft - Create AI Images and Video"
description="Generate AI images and video with ArtCraft."
/>
<p className="hud-label mb-4 text-ui-accent-ink">Your creative workspace</p>
<Reveal
as="h1"
inView={false}
y={20}
className="text-center font-display text-4xl sm:text-6xl mx-auto font-semibold tracking-tight"
className="max-w-3xl font-display text-3xl leading-tight sm:text-5xl tracking-tight"
>
What will you <span className="text-primary">craft</span> today?
What will you <span className="text-ui-accent-ink">craft</span> today?
</Reveal>
<section className="py-12">
<p className="mt-4 max-w-xl text-sm leading-relaxed text-base-fg/70 sm:text-base">
Start with an idea. Choose a tool to bring it to life.
</p>
<section className="mt-8 border-t border-ui-border pt-6 sm:mt-10">
<Reveal
as="h2"
inView={false}
delay={0.08}
className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 mb-4"
className="hud-label text-base-fg/70 mb-4"
>
Create
</Reveal>
@@ -141,33 +146,29 @@ export function Home() {
inView={false}
delayChildren={0.12}
stagger={0.06}
className="grid gap-3 auto-rows-fr sm:grid-cols-2 lg:grid-cols-3"
className="grid gap-3 auto-rows-fr md:grid-cols-2 xl:grid-cols-3"
>
{APPS.map((app) => (
<Reveal key={app.href} y={20}>
<Link
to={app.href}
className={`group relative flex h-full border rounded-[3px] border-white/10 bg-white/5 p-5 transition-colors duration-150 ${app.hoverBorder}`}
className={`group relative flex h-full rounded-[3px] border border-ui-border bg-white/5 p-5 transition-colors duration-150 focus-visible:border-primary ${app.hoverStyle}`}
>
<div className="relative flex w-full items-start gap-4">
<div
className={`flex h-11 w-11 shrink-0 items-center justify-center border ${app.iconTile}`}
className={`flex h-10 w-10 shrink-0 items-center justify-center border ${app.iconTile}`}
>
<DynamicIcon icon={app.icon} className="text-base" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<h3 className=" text-sm font-bold uppercase tracking-[0.08em] text-white truncate">
<div className="flex flex-wrap items-center gap-2 min-w-0">
<h3 className="text-base font-bold uppercase tracking-tight text-ui-ink">
{app.label}
</h3>
{app.badge && (
<span
className={`shrink-0 border px-1.5 py-0.5 font-mono text-[9px] font-semibold uppercase tracking-[0.12em] leading-none ${
app.badge === "NEW"
? "border-purple-400/40 text-purple-300"
: "border-amber-400/40 text-amber-300"
}`}
className="shrink-0 border border-primary/30 px-1.5 py-1 font-mono text-[9px] font-medium tracking-wider leading-none text-ui-accent-ink"
>
{app.badge}
</span>
@@ -177,7 +178,7 @@ export function Home() {
<ArrowRightIcon className="text-xs" />
</span>
</div>
<p className="mt-1 text-sm text-white/55 leading-snug">
<p className="mt-2 text-sm text-base-fg/70 leading-relaxed">
{app.description}
</p>
</div>
@@ -1375,7 +1375,7 @@ export default function Library() {
</span>
))}
<span className="text-white/30">/</span>
<h1 className="text-lg sm:text-xl font-medium text-white truncate max-w-[16rem]">
<h1 className="font-sans text-sm font-semibold tracking-normal [font-stretch:normal] text-white truncate max-w-[16rem]">
{activeFolder?.name}
</h1>
<button
@@ -1405,11 +1405,11 @@ export default function Library() {
Tags
</button>
<span className="text-white/30">/</span>
<h1 className="text-lg sm:text-xl font-medium text-white truncate max-w-[16rem]">
<h1 className="font-sans text-sm font-semibold tracking-normal [font-stretch:normal] text-white truncate max-w-[16rem]">
{activeTag?.value ?? "Tag"}
</h1>
{activeTag && (
<span className="text-white/40 text-sm pt-1 ps-1.5">
<span className="text-white/40 text-xs ps-1.5">
{activeTag.useCount} file
{activeTag.useCount === 1 ? "" : "s"}
</span>
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES } from "../../components/auth/auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -11,7 +12,6 @@ import {
GoogleLoginButton,
} from "../../components/auth";
import Seo from "../../components/seo";
import { Reveal, RevealGroup } from "../../components/motion/reveal";
import { refreshSession } from "../../lib/session";
import { hasActiveSubscription } from "../../lib/billing";
@@ -70,7 +70,7 @@ const Login = () => {
title="Login - ArtCraft"
description="Login to your ArtCraft account."
/>
<AuthHeader title="Welcome Back" subtitle="Log in to your account" />
<AuthHeader title={<>Welcome <span className="font-serif-italic">back.</span></>} subtitle="Log in to your creative workspace." />
<form
className="space-y-4"
@@ -84,20 +84,11 @@ const Login = () => {
{error}
</div>
)}
{/* The form fields cascade in as their own beat, distinct from the
shell/header settling. We lead with the email field on an almost-zero
delay so the primary input lands immediately — the entrance never
gates typing: inputs stay focusable and accept keystrokes while the
password/button below are still cascading in. */}
<RevealGroup
inView={false}
delayChildren={0.04}
stagger={0.08}
<div
className="space-y-4"
>
<Reveal className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70 ml-1">
<div className="space-y-2">
<label className={AUTH_LABEL_CLASSES}>
Email or Username
</label>
<Input
@@ -105,12 +96,12 @@ const Login = () => {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com or username"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</Reveal>
<Reveal className="space-y-2">
<div className="flex justify-between items-center ml-1">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
<Link
@@ -126,7 +117,7 @@ const Login = () => {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -137,9 +128,9 @@ const Login = () => {
<DynamicIcon icon={showPassword ? EyeOffIcon : EyeIcon} />
</button>
</div>
</Reveal>
</div>
<Reveal className="pt-2">
<div className="pt-2">
<Button
className="w-full justify-center h-10"
type="submit"
@@ -151,32 +142,25 @@ const Login = () => {
"Log in"
)}
</Button>
</Reveal>
</RevealGroup>
</form>
{/* Secondary sign-in options pick up the same cascade just after the
fields (delay continues from the form group's three children above:
0.04 + 3 × 0.08). */}
<RevealGroup inView={false} delayChildren={0.28} stagger={0.08}>
<Reveal className="relative my-6 flex items-center justify-center">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/15" />
</div>
<span className="relative bg-[#101014] px-4 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
</div>
</form>
<div>
<div className="my-6 flex items-center gap-4 before:h-px before:flex-1 before:bg-white/15 after:h-px after:flex-1 after:bg-white/15">
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
or
</span>
</Reveal>
</div>
<Reveal>
<div>
<GoogleLoginButton
mode="login"
onSuccess={handleGoogleSuccess}
onError={handleGoogleError}
/>
</Reveal>
</div>
<Reveal>
<div>
<AuthFooter>
Don't have an account?{" "}
<Link
@@ -186,8 +170,8 @@ const Login = () => {
Sign up
</Link>
</AuthFooter>
</Reveal>
</RevealGroup>
</div>
</div>
</>
);
};
@@ -22,7 +22,7 @@ export function SceneSplashModal({
<Modal
isOpen={isOpen}
onClose={close}
className="w-full max-w-3xl overflow-hidden border border-white/15 bg-ui-modal p-0"
className="w-full max-w-3xl overflow-hidden border border-white/15 bg-ui-controls p-0"
childPadding={false}
backdropClassName="!bg-black/80"
closeOnOutsideClick
@@ -1,86 +1,12 @@
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { CoinsIcon } from "lucide-react";
import { ReactNode, useEffect, useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { CoinsIcon, TagIcon } from "lucide-react";
import { Button } from "@storyteller/ui-button";
import { BillingApi, UsersApi } from "@storyteller/api";
import { PricingTable, PROMO_PCT } from "@storyteller/ui-pricing-table";
import "@storyteller/ui-pricing-table/styles.css";
import Seo from "../../components/seo";
import {
PricingTable,
PricingPromoBanner,
} from "@storyteller/ui-pricing-table";
import { CreditsModal } from "../../components/credits-modal";
import { Reveal } from "../../components/motion/reveal";
const SeedanceBanner = () => (
<div className="flex flex-col gap-5">
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white bg-white/10 border border-white/30 px-3 py-1">
Early access
</span>
<span className="inline-flex items-center font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/65 bg-white/5 border border-white/15 px-3 py-1">
Available today in ArtCraft
</span>
</div>
<div>
<h1 className="text-3xl md:text-4xl lg:text-5xl tracking-[-0.035em] font-medium leading-[1.05] mb-3">
Seedance 2.0 is <span className="font-serif-italic">here</span>
</h1>
<p className="text-white/55 text-base md:text-lg leading-relaxed">
Generate jaw-dropping AI videos with Seedance 2.0 before it's available
anywhere else. Every paid plan includes video credits, so you can start
creating right now.
</p>
</div>
<div className="relative w-full overflow-hidden bg-[#080808] border border-white/15">
<div style={{ paddingTop: "56.25%" }} className="relative">
<iframe
src="https://player.vimeo.com/video/1169289718?autoplay=1&muted=1&loop=1&background=0&byline=0&portrait=0&title=0"
className="absolute inset-0 w-full h-full"
allow="autoplay; fullscreen; picture-in-picture"
allowFullScreen
title="Seedance in ArtCraft"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="bg-[#101014] border border-white/30 p-4">
<div className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white mb-1">
Seedance video credits
</div>
<div className="text-white/55 text-sm leading-snug">
Included with every paid ArtCraft plan
</div>
</div>
<div className="bg-[#101014] border border-white/15 p-4">
<div className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70 mb-1">
First in the world
</div>
<div className="text-white/55 text-sm leading-snug">
Seedance launches in ArtCraft ahead of anywhere else
</div>
</div>
</div>
</div>
);
// Compact "top up" CTA shown above the plan table for users who already have a
// plan, so returning subscribers can buy one-time credit packs without scrolling.
const BuyMoreCreditsCta = ({ onClick }: { onClick: () => void }) => (
<div className="mb-8 flex flex-col items-center text-center">
<p className="text-base text-white/65">Need more credits?</p>
<Button
variant="secondary"
className="mt-3 gap-2 border border-white/15 bg-transparent hover:bg-white/10 px-5 py-2 h-11 text-white"
onClick={onClick}
>
<CoinsIcon className="text-[13px]" />
Buy more credits
</Button>
</div>
);
const Pricing = () => {
const [searchParams] = useSearchParams();
@@ -118,85 +44,153 @@ const Pricing = () => {
title="Pricing - ArtCraft"
description="Simple, transparent pricing for ArtCraft. Start for free and scale as you grow."
/>
<main className="relative z-10 px-4 sm:px-8 pt-10">
{isSeedanceRef ? (
<div className="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-[5fr_7fr] gap-8 xl:gap-12 items-start">
<SeedanceBanner />
<div className="w-full">
{hasPlan && (
<BuyMoreCreditsCta onClick={() => setCreditsModalOpen(true)} />
)}
<PricingTable
showHeader={false}
unifiedTheme
compact
showSeedanceFeatures
showEnterprise
/>
<main className="pb-16">
{!isSeedanceRef && (
<div className="border-t border-white/15 bg-primary text-white">
<div className="mx-auto flex max-w-[1280px] flex-wrap items-center justify-between gap-x-6 gap-y-1 px-6 py-2.5 md:px-10">
<p className="pricing-hud flex items-center gap-2 font-bold">
<TagIcon aria-hidden className="h-3.5 w-3.5" />
Limited-time offer
</p>
<p className="text-sm">
Save {PROMO_PCT}% on every plan, monthly or yearly — lock in the
lowest price today.
</p>
</div>
</div>
) : (
<div className="max-w-6xl mx-auto">
<PricingPromoBanner className="mb-10 sm:mb-12" />
<Reveal
as="div"
inView={false}
y={20}
className="text-center mb-14"
>
<h1 className="text-4xl sm:text-5xl md:text-6xl tracking-[-0.035em] font-medium leading-[1.02] mb-5">
Invest in <span className="font-serif-italic">yourself</span>.
</h1>
<p className="max-w-xl mx-auto text-base sm:text-lg text-white/55 leading-relaxed">
Get a ton of generations and invest in a tool you'll always own.
Your subscription helps keep ArtCraft free and open for
everyone.
</p>
</Reveal>
{hasPlan && (
<BuyMoreCreditsCta onClick={() => setCreditsModalOpen(true)} />
)}
<PricingTable
showHeader={false}
unifiedTheme
showSeedanceFeatures
showEnterprise
/>
</div>
)}
</main>
{isLoggedIn && !hasPlan && (
<div className="relative z-10 flex flex-col items-center px-4 pb-4 sm:px-8 pt-6">
<div className="inline-flex items-center gap-2 text-white/40">
<div className="h-px w-8 bg-white/20" />
<span className="text-sm">Or</span>
<div className="h-px w-8 bg-white/20" />
<PricingSection>
<SectionLabel
index="01"
label={isSeedanceRef ? "Early access" : "Pricing"}
annotation={
isSeedanceRef
? "Available today in ArtCraft"
: "Free & open source · Subscriptions optional†"
}
/>
<div className="px-6 py-14 md:px-10 md:py-20">
<h1 className="max-w-3xl font-display text-4xl font-medium leading-[1.02] tracking-[-0.035em] sm:text-5xl md:text-6xl">
{isSeedanceRef ? (
<>
Seedance 2.0 is{" "}
<span className="font-serif-italic">here</span>.
</>
) : (
"Pick a plan"
)}
</h1>
<p className="mt-5 max-w-xl text-lg leading-relaxed text-white/55">
{isSeedanceRef
? "Generate jaw-dropping AI videos with Seedance 2.0 before it's available anywhere else. Every paid plan includes video credits, so you can start creating right now."
: "Get a ton of generations and invest in a tool you'll always own. Your subscription helps keep ArtCraft free and open for everyone."}
</p>
{hasPlan && (
<Button
variant="secondary"
className="mt-6 gap-2 rounded-none text-[11px]"
onClick={() => setCreditsModalOpen(true)}
>
<CoinsIcon aria-hidden className="h-4 w-4" />
Buy more credits
</Button>
)}
</div>
<p className="mt-3 text-base text-white/65">
Purchase one-time credit packs
</p>
<Button
variant="secondary"
className="mt-4 gap-2 border border-white/15 bg-transparent hover:bg-white/10 px-5 py-2 h-11 text-white"
onClick={() => setCreditsModalOpen(true)}
>
<CoinsIcon className="text-[13px]" />
Buy Credits
</Button>
</div>
)}
{/* Footnote */}
<div className="relative z-10 max-w-3xl mx-auto px-4 sm:px-8 pb-16 text-center mt-6">
<p className="text-sm text-white/45 leading-relaxed">
&dagger; ArtCraft can be used without paying for a subscription. You
can bring your own compute and third party subscriptions. We hope
you'll subscribe, though, as that helps accelerate our development.
</p>
</div>
</PricingSection>
{isSeedanceRef && (
<PricingSection>
<div className="grid lg:grid-cols-[3fr_2fr]">
<figure className="min-w-0">
<figcaption className="pricing-hud flex justify-between border-b border-white/15 px-6 py-2.5 text-white/55 md:px-8">
<span>Seedance in ArtCraft</span>
<span>Reel</span>
</figcaption>
<div className="relative aspect-video overflow-hidden bg-black">
<iframe
src="https://player.vimeo.com/video/1169289718?autoplay=1&muted=1&loop=1&background=0&byline=0&portrait=0&title=0"
className="absolute inset-0 h-full w-full"
allow="autoplay; fullscreen; picture-in-picture"
allowFullScreen
title="Seedance in ArtCraft"
/>
</div>
</figure>
<div className="grid border-t border-white/15 sm:grid-cols-2 lg:grid-cols-1 lg:border-l lg:border-t-0">
<div className="p-6 md:p-8">
<p className="pricing-hud text-primary">
Seedance video credits
</p>
<p className="mt-3 leading-relaxed text-white/55">
Included with every paid ArtCraft plan.
</p>
</div>
<div className="border-t border-white/15 p-6 sm:border-l sm:border-t-0 md:p-8 lg:border-l-0 lg:border-t">
<p className="pricing-hud text-white/55">
First in the world
</p>
<p className="mt-3 leading-relaxed text-white/55">
Seedance launches in ArtCraft ahead of anywhere else.
</p>
</div>
</div>
</div>
</PricingSection>
)}
<PricingSection id="plans">
<SectionLabel
index="02"
label="Choose your plan"
annotation="Every paid plan includes video credits"
/>
<PricingTable
showHeader={false}
websiteLayout
showSeedanceFeatures
showEnterprise
/>
</PricingSection>
<PricingSection id="credits">
<div className="grid md:grid-cols-2">
<div className="p-6 md:p-10">
<p className="pricing-hud text-white/40">
{isLoggedIn ? "Need more credits?" : "Already have an account?"}
</p>
<h2 className="mt-4 font-display text-2xl font-medium tracking-[-0.02em]">
One-time credit packs
</h2>
<p className="mt-2 max-w-md leading-relaxed text-white/55">
Top up without changing your plan. Credit packs never expire.
</p>
{isLoggedIn ? (
<Button
variant="secondary"
className="mt-6 gap-2 rounded-none text-[11px]"
onClick={() => setCreditsModalOpen(true)}
>
<CoinsIcon aria-hidden className="h-4 w-4 text-primary" />
Buy credits
</Button>
) : (
<Link
to="/login?from=%2Fpricing"
className="pricing-hud mt-6 inline-flex border border-white/15 px-4 py-3 hover:bg-white/10"
>
Sign in to buy credits
</Link>
)}
</div>
<div className="border-t border-white/15 p-6 md:border-l md:border-t-0 md:p-10">
<p className="pricing-hud text-white/40">† Footnote</p>
<p className="mt-4 max-w-md leading-relaxed text-white/55">
ArtCraft can be used without paying for a subscription. You can
bring your own compute and third-party subscriptions. We hope
you’ll subscribe, though, as that helps accelerate our
development.
</p>
</div>
</div>
</PricingSection>
</main>
{isLoggedIn && (
<CreditsModal
isOpen={creditsModalOpen}
@@ -207,4 +201,40 @@ const Pricing = () => {
);
};
function PricingSection({
id,
children,
}: {
id?: string;
children: ReactNode;
}) {
return (
<section id={id} className="border-t border-white/15">
<div className="relative mx-auto max-w-[1280px] border-x border-white/15">
{children}
</div>
</section>
);
}
function SectionLabel({
index,
label,
annotation,
}: {
index: string;
label: string;
annotation: string;
}) {
return (
<div className="pricing-hud flex items-center justify-between gap-4 border-b border-white/15 px-6 py-3 text-white/55 md:px-10">
<p>
<span className="text-white/40">{index} / </span>
{label}
</p>
<p className="hidden text-white/40 sm:block">{annotation}</p>
</div>
);
}
export default Pricing;
@@ -145,7 +145,7 @@ export default function Referrals() {
<span className="inline-block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 mb-5">
Share the craft
</span>
<h1 className="text-5xl tracking-[-0.035em] font-medium leading-[1.02] mb-5">
<h1 className="font-display text-3xl sm:text-4xl tracking-tight leading-[1.02] mb-5">
Referrals
</h1>
<p className="mx-auto text-base sm:text-lg text-white/55 leading-relaxed">
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_PASSWORD_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -6,6 +7,8 @@ import { useState } from "react";
import { useNavigate, Navigate } from "react-router-dom";
import { UsersApi } from "@storyteller/api";
import { useSession, refreshSession } from "../../lib/session";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
import { toast } from "../../components/toast/toast";
@@ -66,26 +69,15 @@ const SetPassword = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Set Password - ArtCraft"
description="Set a password for your ArtCraft account."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">Set a password</h1>
<p className="text-white/60 text-sm">
Create a password so you can also log into the desktop app
</p>
</div>
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
<AuthHeader title={<>Set your <span className="font-serif-italic">password.</span></>} subtitle="Create a password to sign in on the web and desktop." />
<form
className="space-y-4"
@@ -101,7 +93,7 @@ const SetPassword = () => {
)}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
<div className="relative">
@@ -111,7 +103,7 @@ const SetPassword = () => {
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
autoFocus
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -125,7 +117,7 @@ const SetPassword = () => {
</div>
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Confirm password
</label>
<div className="relative">
@@ -134,7 +126,7 @@ const SetPassword = () => {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Re-enter your password"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -165,11 +157,7 @@ const SetPassword = () => {
</form>
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -1,7 +1,6 @@
import { Link, useNavigate } from "react-router-dom";
import { AuthHeader, AuthFooter, SignupForm } from "../../components/auth";
import Seo from "../../components/seo";
import { Reveal } from "../../components/motion/reveal";
const Signup = () => {
const navigate = useNavigate();
@@ -12,17 +11,13 @@ const Signup = () => {
title="Sign Up - ArtCraft"
description="Create your ArtCraft account."
/>
<AuthHeader title="Create an Account" subtitle="Join thousands of creators" />
{/* SignupForm runs its own field cascade internally (see signup-form.tsx).
The footer picks up the tail of that cascade — the form's six staggered
steps land by ~0.36s, so the footer follows at ~0.44s. */}
<AuthHeader title={<>Make it <span className="font-serif-italic">yours.</span></>} subtitle="Create your account and start crafting." />
<SignupForm
onSuccess={() => navigate("/welcome")}
signupSource="artcraft"
/>
<Reveal inView={false} delay={0.44}>
<div>
<AuthFooter>
Already have an account?{" "}
<Link
@@ -32,7 +27,7 @@ const Signup = () => {
Log in
</Link>
</AuthFooter>
</Reveal>
</div>
</>
);
};
@@ -44,12 +44,12 @@ const Support = () => {
"Get help with ArtCraft: browse the FAQ, join our Discord community, or email us directly.";
return (
<div className="relative min-h-screen bg-ui-background text-white">
<div className="relative min-h-full bg-ui-background text-white">
<Seo title={title} description={description} />
<main className="relative z-10 mx-auto w-full max-w-4xl px-4 sm:px-6 pt-24 sm:pt-32 pb-20">
<header className="mb-8 sm:mb-10 flex flex-col items-center text-center gap-3">
<h1 className="text-3xl sm:text-5xl font-semibold tracking-[-0.02em]">
<main className="relative z-10 mx-auto w-full max-w-4xl px-5 py-8 sm:px-8 sm:py-12">
<header className="mb-8 flex flex-col gap-3 border-b border-ui-border pb-8">
<h1 className="font-display text-3xl sm:text-4xl tracking-tight">
Support
</h1>
<p className="text-[15px] sm:text-base text-white/60 max-w-xl">
+41 -6
View File
@@ -7,21 +7,22 @@
/* Sidebar tokens (consumed by tailwind colors in tailwind.config.js).
Brutalist system: near-black surfaces, white/15 hairlines, white
accent for selection/focus. */
color-scheme: dark;
--sidebar-bg: #1e1f22;
--sidebar-fg: rgba(255, 255, 255, 0.72);
--sidebar-border: rgba(255, 255, 255, 0.15);
--sidebar-accent: rgba(255, 255, 255, 0.08);
--sidebar-accent-fg: #ffffff;
--sidebar-ring: rgba(255, 255, 255, 0.4);
--sidebar-ring: #74aaff;
/* shadcn design tokens consumed by @storyteller/ui-video-editor.
Dark-theme palette (the editor only runs in dark mode today).
--primary matches the webapp's brand blue (#2d81ff). */
--background: 240 5% 8%;
--background: 225 10% 8%;
--foreground: 0 0% 98%;
--card: 240 11% 7%;
--card-foreground: 0 0% 98%;
--popover: 240 4% 5%;
--popover: 240 11% 7%;
--popover-foreground: 0 0% 98%;
--primary: 216 100% 59%;
--primary-foreground: 0 0% 98%;
@@ -35,7 +36,7 @@
--destructive-foreground: 0 0% 98%;
--border: 0 0% 100% / 0.15;
--input: 0 0% 100% / 0.15;
--ring: 0 0% 100% / 0.6;
--ring: 217 100% 73%;
/* ── Motion tokens ──────────────────────────────────────────────────
Shared easing curves + durations so motion across the app feels like
@@ -144,6 +145,7 @@
h1,
h2 {
font-stretch: 118%;
font-weight: 620;
}
body {
@@ -156,12 +158,22 @@
"Helvetica Neue",
Arial,
sans-serif;
color: #ffffff;
background-color: #0b0b0c;
color: #f2f1ee;
background-color: #121316;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::selection {
background: #2d81ff;
color: #ffffff;
}
:focus-visible {
outline: 2px solid #74aaff;
outline-offset: 3px;
}
/* Mono HUD micro-label, matching the marketing site's .hud-label. */
.hud-label {
font-family:
@@ -514,3 +526,26 @@
vertical-align: -0.2em;
}
}
/* Fine registration marks echo the landing page section corners. */
.frame-corner-mark {
position: absolute;
width: 11px;
height: 11px;
margin: -5px;
pointer-events: none;
background:
linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 1px 11px no-repeat,
linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 11px 1px no-repeat;
}
/* Contained section borders match the landing without decorative extensions. */
.create-empty-section {
margin-inline: 12px;
border: 1px solid rgba(242, 241, 238, 0.2);
background: rgba(242, 241, 238, 0.015);
}
@media (min-width: 640px) {
.create-empty-section { margin-inline: 32px; }
}
@@ -1,6 +1,6 @@
const { createGlobPatternsForDependencies } = require("@nx/react/tailwind");
const { join } = require("path");
import colors, { teal } from "tailwindcss/colors";
import colors from "tailwindcss/colors";
/** @type {import('tailwindcss').Config} */
module.exports = {
@@ -167,7 +167,7 @@ module.exports = {
900: "#3d3d3d",
950: "#3E3E41",
},
"base-fg": "rgb(255 255 255 / <alpha-value>)",
"base-fg": "rgb(242 241 238 / <alpha-value>)",
sidebar: {
DEFAULT: "var(--sidebar-bg)",
foreground: "var(--sidebar-fg)",
@@ -180,17 +180,18 @@ module.exports = {
// Brutalist system: near-black surfaces with white/15 hairline
// borders, aligned with artcraft-website + artcraft-website-next.
background: "#121316",
panel: "#0b0b0c",
panel: "#101014",
"panel-border": "rgb(255 255 255 / 0.15)",
border: "rgb(255 255 255 / 0.15)",
// Raised control surface — a step above the #0b0b0c page/panel bg
// (matches the sidebar's #1e1f22) so buttons and inputs read as
// surfaces instead of blending into the background.
// Raised controls stay distinct from the website-matched panel surface.
controls: "#1e1f22",
"controls-button": "#1e1f22",
"controls-border": "rgb(255 255 255 / 0.15)",
divider: "rgb(255 255 255 / 0.15)",
modal: "#0b0b0c",
modal: "#101014",
ink: "#f2f1ee",
"accent-ink": "#74aaff",
sunken: "#060607",
},
"axis-x": "#D33242",
"axis-y": "#308752",
+3 -2
View File
@@ -68,8 +68,9 @@ Then read values where they're used:
The panel picks up new groups automatically — no panel changes needed.
Registered groups: Intro (page-intro choreography beats; `src/lib/intro.ts`),
Wordmark (logo-A optics, blade tuck, contrast scrim;
`src/components/landing/hero-wordmark.tsx`), Galaxy layout, Galaxy motion,
Galaxy pointer, Galaxy look (hero galaxy;
`src/components/landing/hero-wordmark.tsx`), Galaxy layout, Galaxy mobile
(small-screen spiral profile — replaces the layout dials below its area
threshold), Galaxy motion, Galaxy pointer, Galaxy look (hero galaxy;
`src/components/landing/hero-galaxy-tunables.ts`) and Ruler layout, Ruler
motion, Ruler look (scroll ruler;
`src/components/ruler/ruler-tunables.ts`).
@@ -2,8 +2,8 @@
# Base directory: frontend/apps/artcraft-website-next
# (this app is standalone — its own package-lock.json, not the Nx workspace)
# Manage the build command and publish directory here; leave those UI fields empty.
# The Next.js runtime handles routing, the /cdn-media and /ext-media proxy
# rewrites, and the legacy-route redirects declared in next.config.ts.
# The Next.js runtime handles routing, the /cdn-media proxy rewrite, and the
# legacy-route redirects declared in next.config.ts. Other media is in public/.
[build]
command = "npm run build"
@@ -1,19 +1,5 @@
import type { NextConfig } from "next";
// Marketing routes the nav/footer reference but this app doesn't serve yet.
// They bounce to the currently shipping site (same dependency as MEDIA_BASE —
// this app already assumes the Vite site stays live) so no link dead-ends.
// Delete a route from this list when its page migrates here.
const LEGACY_ROUTES = [
"download",
"pricing",
"tutorials",
"news",
"faq",
"press-kit",
"support",
];
const nextConfig: NextConfig = {
reactStrictMode: true,
// Pin the project root: the repo has lockfiles both here and in the Nx
@@ -22,16 +8,21 @@ const nextConfig: NextConfig = {
turbopack: { root: __dirname },
outputFileTracingRoot: __dirname,
async redirects() {
return LEGACY_ROUTES.map((route) => ({
source: `/${route}`,
destination: `https://getartcraft.com/${route}`,
permanent: false,
}));
// Legacy Stripe return paths from the Vite site; checkout now lives in
// the webapp, which serves the canonical slash forms.
return ["checkout_success", "checkout_cancel", "portal_closed"].map(
(route) => ({
source: `/${route}`,
destination: `https://app.getartcraft.com/checkout/${
route === "checkout_success" ? "success" : "cancel"
}`,
permanent: false,
}),
);
},
// Same-origin proxies for large media served from other origins. The hero
// wall draws its clips as WebGL video textures, which need CORS-clean
// sources, and neither CDN sends Access-Control-Allow-Origin, so those
// requests route through our own origin.
// The hero wall uses WebGL video textures, which need CORS-clean sources.
// The showcase CDN sends no Access-Control-Allow-Origin header, so these
// clips use a same-origin proxy. Other marketing media lives in public/.
async rewrites() {
return [
// Showcase clips for the hero wall (see cdnMediaUrl in lib/links.ts).
@@ -39,11 +30,6 @@ const nextConfig: NextConfig = {
source: "/cdn-media/:path*",
destination: "https://frontend-cdn.fakeyou.com/:path*",
},
// Feature footage and other assets still hosted on the live site.
{
source: "/ext-media/:path*",
destination: "https://getartcraft.com/:path*",
},
];
},
};
+52 -40
View File
@@ -15,7 +15,7 @@
"gsap": "3.15.0",
"lenis": "1.3.23",
"lucide-react": "^1.31.0",
"next": "16.2.6",
"next": "^16.2.12",
"react": "19.2.6",
"react-dom": "19.2.6",
"tailwind-merge": "^3.6.0",
@@ -608,15 +608,15 @@
}
},
"node_modules/@next/env": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.12.tgz",
"integrity": "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.12.tgz",
"integrity": "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==",
"cpu": [
"arm64"
],
@@ -630,9 +630,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.12.tgz",
"integrity": "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==",
"cpu": [
"x64"
],
@@ -646,12 +646,15 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.12.tgz",
"integrity": "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -662,12 +665,15 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.12.tgz",
"integrity": "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -678,12 +684,15 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.12.tgz",
"integrity": "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -694,12 +703,15 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.12.tgz",
"integrity": "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -710,9 +722,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.12.tgz",
"integrity": "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==",
"cpu": [
"arm64"
],
@@ -726,9 +738,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.12.tgz",
"integrity": "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==",
"cpu": [
"x64"
],
@@ -2209,12 +2221,12 @@
}
},
"node_modules/next": {
"version": "16.2.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
"version": "16.2.12",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.12.tgz",
"integrity": "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==",
"license": "MIT",
"dependencies": {
"@next/env": "16.2.6",
"@next/env": "16.2.12",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -2228,14 +2240,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.6",
"@next/swc-darwin-x64": "16.2.6",
"@next/swc-linux-arm64-gnu": "16.2.6",
"@next/swc-linux-arm64-musl": "16.2.6",
"@next/swc-linux-x64-gnu": "16.2.6",
"@next/swc-linux-x64-musl": "16.2.6",
"@next/swc-win32-arm64-msvc": "16.2.6",
"@next/swc-win32-x64-msvc": "16.2.6",
"@next/swc-darwin-arm64": "16.2.12",
"@next/swc-darwin-x64": "16.2.12",
"@next/swc-linux-arm64-gnu": "16.2.12",
"@next/swc-linux-arm64-musl": "16.2.12",
"@next/swc-linux-x64-gnu": "16.2.12",
"@next/swc-linux-x64-musl": "16.2.12",
"@next/swc-win32-arm64-msvc": "16.2.12",
"@next/swc-win32-x64-msvc": "16.2.12",
"sharp": "^0.34.5"
},
"peerDependencies": {
@@ -19,7 +19,7 @@
"gsap": "3.15.0",
"lenis": "1.3.23",
"lucide-react": "^1.31.0",
"next": "16.2.6",
"next": "^16.2.12",
"react": "19.2.6",
"react-dom": "19.2.6",
"tailwind-merge": "^3.6.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Qwen</title><path d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72.22 65.68">
<g id="Layer_1-2" data-name="Layer 1">
<path d="M69.38,45.46c-3.18-6.24-6.58-12.49-9.86-18.53-3.31-6.08-6.72-12.37-9.91-18.63l-.05-.09C46.44,3.07,41.24,0,35.66,0s-10.34,3-13.13,8.23L.97,48.96l-.03.07c-1.51,3.8-1.2,8.03.82,11.31,1.9,3.08,5.01,4.96,8.76,5.28.38.03.75.05,1.1.05,2.94,0,4.98-1.12,7.33-2.41,7.13-3.91,19.45-11.21,27.61-16.04,2.6-1.54,4.95-2.93,6.31-3.73l10.71,20.43c.2.39.54.7.95.86.46.19.98.2,1.43,0,1.39-.66,3.05-2.47,3.07-2.49,5.22-5.9,3.03-11.55.34-16.83ZM34.53,14.8c.31-.46.77-.99,1.32-.99.58,0,1.29.57,1.85,1.49,1.57,2.56,2.99,5.44,4.35,8.22,1.34,2.73,2.72,5.55,4.29,8.15l.02.05-29.6,16.99L34.53,14.8Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 781 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Flux</title><path d="M0 20.683L12.01 2.5 24 20.683h-2.233L12.009 5.878 3.471 18.806h12.122l1.239 1.877H0z"></path><path d="M8.069 16.724l2.073-3.115 2.074 3.115H8.069zM18.24 20.683l-5.668-8.707h2.177l5.686 8.707h-2.196zM19.74 11.676l2.13-3.19 2.13 3.19h-4.26z"></path></svg>

After

Width:  |  Height:  |  Size: 438 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ByteDance</title><path d="M14.944 18.587l-1.704-.445V10.01l1.824-.462c1-.254 1.84-.461 1.88-.453.032 0 .056 2.235.056 4.972v4.973l-.176-.008c-.104 0-.952-.207-1.88-.446z"></path><path d="M7 16.542c0-2.736.024-4.98.064-4.98.032-.008.872.2 1.88.454l1.816.461-.016 4.05-.024 4.049-1.632.422c-.896.23-1.736.445-1.856.469L7 21.523v-4.98z"></path><path d="M19.24 12.477c0-9.03.008-9.515.144-9.475.072.024.784.207 1.576.406.792.207 1.576.405 1.744.445l.296.08-.016 8.56-.024 8.568-1.624.414c-.888.23-1.728.437-1.856.47l-.24.055v-9.523z"></path><path d="M1 12.509c0-4.678.024-8.505.064-8.505.032 0 .872.207 1.872.454l1.824.461v7.582c0 4.16-.016 7.574-.032 7.574-.024 0-.872.215-1.88.47L1 21.013v-8.505z"></path></svg>

After

Width:  |  Height:  |  Size: 873 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Fal</title><path clip-rule="evenodd" d="M15.477 0c.415 0 .749.338.788.752a7.775 7.775 0 006.985 6.984c.413.04.752.373.752.788v6.952c0 .415-.338.748-.752.788a7.775 7.775 0 00-6.985 6.984c-.04.414-.373.752-.788.752H8.525c-.416 0-.749-.338-.789-.752a7.775 7.775 0 00-6.984-6.984c-.414-.04-.752-.373-.752-.788V8.524c0-.415.338-.748.752-.788A7.775 7.775 0 007.736.752C7.776.338 8.11 0 8.526 0h6.95zM4.819 11.98a7.226 7.226 0 007.223 7.23 7.226 7.226 0 007.223-7.23c0-3.994-3.234-7.23-7.223-7.23a7.227 7.227 0 00-7.223 7.23z"></path></svg>

After

Width:  |  Height:  |  Size: 697 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 144C64 117.5 85.5 96 112 96L208 96C234.5 96 256 117.5 256 144L256 160L384 160L384 144C384 117.5 405.5 96 432 96L528 96C554.5 96 576 117.5 576 144L576 240C576 266.5 554.5 288 528 288L432 288C405.5 288 384 266.5 384 240L384 224L256 224L256 240C256 247.3 254.3 254.3 251.4 260.5L320 352L400 352C426.5 352 448 373.5 448 400L448 496C448 522.5 426.5 544 400 544L304 544C277.5 544 256 522.5 256 496L256 400C256 392.7 257.7 385.7 260.6 379.5L192 288L112 288C85.5 288 64 266.5 64 240L64 144z"/></svg>

After

Width:  |  Height:  |  Size: 717 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z"></path></svg>

After

Width:  |  Height:  |  Size: 651 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>

After

Width:  |  Height:  |  Size: 756 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M.03,2.19c.14.03.29.07.43.09,1.26.16,1.76-1.08,2.78-1.5,1.26-.52,2.77-.09,2.67,1.5s-1.89,2.3-2.88,3.2c-.38.34-.74.91-.36,1.4.5.64,1.25.06,1.65-.36,1.18-1.22,2.38-3.76,4.38-3.57,1.47.14,2.2,1.65,2.32,2.97h.96l.03.03v1.36h-.99c-.09,1.37-.54,3.03-1.78,3.79-.9.55-2.02.51-2.73-.31-.78-.89-.43-1.96.16-2.84.71-1.06,1.85-1.83,3.12-2.04.04-.61-.34-1.2-.94-1.35-1.15-.3-2.03.74-2.65,1.54-.79,1-1.87,2.88-3.33,2.8-1.19-.07-1.99-1.08-1.93-2.24.05-1.04,1.04-1.89,1.83-2.46.52-.38,1.71-1.05,1.81-1.72.06-.41-.18-.75-.61-.65-.59.13-1.03.85-1.49,1.22-.69.56-1.57.85-2.43.48v-1.3ZM9.73,7.28c-.28.07-.56.16-.81.3-.58.31-1.35,1.09-1.45,1.77-.08.58.45.87.95.62.87-.43,1.26-1.79,1.31-2.69Z"/>
</svg>

After

Width:  |  Height:  |  Size: 824 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kling</title><path clip-rule="evenodd" d="M5.493 21.234c-1.112-1.451-1.109-4.263-.081-7.459l-4.557-2.63a1.683 1.683 0 01-.85-1.304 1.505 1.505 0 01.08-.622 13.18 13.18 0 011.037-2.255c3.476-6.02 10.916-8.23 16.619-4.938.46.266.82.67 1.081 1.184.785 1.545.685 4.096-.234 6.954l4.557 2.631c.339.196.596.492.736.832a1.53 1.53 0 01.034 1.093 13.146 13.146 0 01-1.037 2.255c-3.476 6.02-10.916 8.23-16.619 4.938a2.6 2.6 0 01-.766-.68zm11.096-6.615c-2.073 3.591-5.808 5.316-8.343 3.852-1.267-.731-1.994-2.122-2.145-3.778-.095-1.035.036-2.173.4-3.32.217-.684.517-1.37.902-2.039l.008-.014c2.073-3.59 5.808-5.315 8.343-3.852.633.366 1.13.895 1.49 1.54.986 1.772.922 4.415-.285 6.914-.111.23-.232.457-.362.683l-.008.014z"></path></svg>

After

Width:  |  Height:  |  Size: 888 B

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<defs>
<style>
.cls-1 {
fill: #010101;
}
</style>
</defs>
<path d="M9.26.37c.36.29.62.65.72,1.11.01.14.01.27.01.41t0,.06c0,.51-.07.99-.22,1.47t-.02.06c-.24.83-.68,1.56-1.25,2.21-.02.02-.04.05-.06.07-.21.23-.42.43-.66.63-.02.02-.04.03-.06.05-.87.72-1.93,1.12-3.04,1.29-.03,0-.06.01-.09.02-.63.1-1.3.08-1.85-.27-.11-.08-.21-.18-.31-.28-.02-.02-.03-.03-.05-.05-.32-.34-.43-.82-.43-1.27.02-.45.27-.87.59-1.17.36-.31.81-.47,1.29-.48.62-.01,1.16-.23,1.64-.62.02-.02.04-.03.07-.05.39-.33.67-.89.73-1.39,0-.11.02-.22.02-.33.03-.51.2-.93.57-1.29.07-.06.15-.12.23-.18.02-.01.03-.02.05-.04.63-.43,1.49-.36,2.09.06Z"/>
<path d="M8.61,6.51c.06,0,.1.02.14.06t.04.05s.03.04.04.06c.01.02.03.04.04.06.01.02.03.04.04.06.4.57.68,1.19.88,1.85t.02.05c.22.71.32,1.62-.02,2.29-.24.47-.63.72-1.11.91-.48.14-1.02.06-1.46-.17-.47-.28-.74-.67-.89-1.19-.03-.12-.04-.25-.04-.37-.02-.65-.25-1.22-.71-1.68-.06-.05-.12-.11-.19-.16-.03-.03-.05-.05-.08-.08.13-.04.25-.08.38-.12.99-.29,2.17-.79,2.86-1.61l.04-.03Z"/>
<path class="cls-1" d="M4.96.4c.34.27.63.67.69,1.1.02.29.02.58-.08.85t-.03.08c-.16.44-.5.77-.92.97-.29.13-.53.17-.85.18-.68.04-1.21.26-1.72.69-.02.02-.05.04-.07.06,0-.36,0-.71,0-1.07,0-.17,0-.33,0-.5,0-.16,0-.32,0-.48,0-.06,0-.12,0-.18,0-.63.12-1.11.56-1.58.67-.65,1.69-.66,2.43-.13Z"/>
<path d="M1.98,7.67s.07.05.1.07c.02.01.04.03.06.04.04.03.09.07.13.1.41.3.88.5,1.39.51.58.01,1.1.17,1.52.59.35.4.5.86.47,1.39-.05.49-.31.89-.67,1.21-.46.35-1.01.43-1.57.36-.47-.1-.88-.4-1.15-.79-.07-.12-.13-.23-.18-.36,0-.02-.02-.05-.03-.07-.1-.31-.09-.61-.09-.93,0-.06,0-.12,0-.18,0-.16,0-.32,0-.48,0-.16,0-.33,0-.49,0-.32,0-.64,0-.96Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Midjourney</title><path d="M22.369 17.676c-1.387 1.259-3.17 2.378-5.332 3.417.044.03.086.057.13.083l.018.01.019.012c.216.123.42.184.641.184.222 0 .426-.061.642-.184l.018-.011.019-.011c.14-.084.266-.178.492-.366l.178-.148c.279-.232.426-.342.625-.456.304-.174.612-.266.949-.266.337 0 .645.092.949.266l.023.014c.188.109.334.219.602.442l.178.148c.221.184.346.278.483.36l.028.017.018.01c.21.12.407.181.62.185h.022a.31.31 0 110 .618c-.337 0-.645-.092-.95-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.02-.014a5.356 5.356 0 01-.49-.377l-.159-.132a3.836 3.836 0 00-.483-.36l-.027-.017-.019-.01a1.256 1.256 0 00-.641-.185c-.222 0-.426.061-.641.184l-.02.011-.018.011c-.14.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.51.39l-.022.014-.022.014-.09.054a1.868 1.868 0 01-.95.266c-.337 0-.644-.092-.949-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.026-.017a4.881 4.881 0 01-.425-.325.308.308 0 01-.12-.1l-.098-.081a3.836 3.836 0 00-.483-.36l-.027-.017-.019-.01a1.256 1.256 0 00-.641-.185c-.222 0-.426.061-.642.184l-.018.011-.019.011c-.14.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.51.39l-.023.014-.022.014-.09.054A1.868 1.868 0 0112 22c-.337 0-.645-.092-.949-.266a3.137 3.137 0 01-.09-.054l-.022-.014-.022-.013-.021-.014a5.356 5.356 0 01-.49-.377l-.158-.132a3.836 3.836 0 00-.483-.36l-.028-.017-.018-.01a1.256 1.256 0 00-.642-.185c-.221 0-.425.061-.641.184l-.019.011-.018.011c-.141.084-.266.178-.492.366l-.158.132a5.125 5.125 0 01-.511.39l-.022.014-.022.014-.09.054a1.868 1.868 0 01-.986.264c-.746-.09-1.319-.38-1.89-.866l-.035-.03c-.047-.041-.118-.106-.192-.174l-.196-.181-.107-.1-.011-.01a1.531 1.531 0 00-.336-.253.313.313 0 00-.095-.03h-.005c-.119.022-.238.059-.361.11a.308.308 0 01-.077.061l-.008.005a.309.309 0 01-.126.034 5.66 5.66 0 00-.774.518l-.416.324-.055.043a6.542 6.542 0 01-.324.236c-.305.207-.552.315-.8.315a.31.31 0 01-.01-.618h.01c.09 0 .235-.062.438-.198l.04-.027c.077-.054.163-.117.27-.199l.385-.301.06-.047c.268-.206.506-.373.73-.505l-.633-1.21a.309.309 0 01.254-.451l20.287-1.305a.309.309 0 01.228.537zm-1.118.14L2.369 19.03l.423.809c.128-.045.256-.078.388-.1a.31.31 0 01.052-.005c.132 0 .26.032.386.093.153.073.294.179.483.35l.016.015.092.086.144.134.097.089c.065.06.125.114.16.144.485.418.948.658 1.554.736h.011a1.25 1.25 0 00.6-.172l.021-.011.019-.011.018-.011c.141-.084.266-.178.492-.366l.178-.148c.279-.232.426-.342.625-.456.305-.174.612-.266.95-.266.336 0 .644.092.948.266l.023.014c.188.109.335.219.603.442l.177.148c.222.184.346.278.484.36l.027.017.019.01c.215.124.42.185.641.185.222 0 .426-.061.641-.184l.019-.011.018-.011c.141-.084.267-.178.493-.366l.177-.148c.28-.232.427-.342.626-.456.304-.174.612-.266.949-.266.337 0 .644.092.949.266l.025.015c.187.109.334.22.603.443 1.867-.878 3.448-1.811 4.73-2.832l.02-.016zM3.653 2.026C6.073 3.06 8.69 4.941 10.8 7.258c2.46 2.7 4.109 5.828 4.637 9.149a.31.31 0 01-.421.335c-2.348-.945-4.54-1.258-6.59-1.02-1.739.2-3.337.792-4.816 1.703-.294.182-.62-.182-.405-.454 1.856-2.355 2.581-4.99 2.343-7.794-.195-2.292-1.031-4.61-2.284-6.709a.31.31 0 01.388-.442zM10.04 4.45c1.778.543 3.892 2.102 5.782 4.243 1.984 2.248 3.552 4.934 4.347 7.582a.31.31 0 01-.401.38l-.022-.01-.386-.154a10.594 10.594 0 00-.291-.112l-.016-.006c-.68-.247-1.199-.291-1.944-.101a.31.31 0 01-.375-.218C15.378 11.123 13.073 7.276 9.775 5c-.291-.201-.072-.653.266-.55zM4.273 2.996l.008.015c1.028 1.94 1.708 4.031 1.885 6.113.213 2.513-.31 4.906-1.673 7.092l-.02.031.003-.001c1.198-.581 2.47-.969 3.825-1.132l.055-.006c1.981-.23 4.083.029 6.309.837l.066.025-.007-.039c-.593-2.95-2.108-5.737-4.31-8.179l-.07-.078c-1.785-1.96-3.944-3.6-6.014-4.65l-.057-.028zm7.92 3.238l.048.048c2.237 2.295 3.885 5.431 4.974 9.191l.038.132.022-.004c.71-.133 1.284-.063 1.963.18l.027.01.066.024.046.018-.025-.073c-.811-2.307-2.208-4.62-3.936-6.594l-.058-.065c-1.02-1.155-2.103-2.132-3.15-2.856l-.015-.011z"></path></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<defs>
<style>
.cls-1 {
fill-rule: evenodd;
}
</style>
</defs>
<path class="cls-1" d="M8.6,2.63c.89,0,1.75.35,2.39.99.63.63.99,1.49,1,2.38,0,.66-.2,1.3-.56,1.84-.36.55-.87.98-1.47,1.24-.6.26-1.27.34-1.91.24-.65-.11-1.25-.4-1.73-.84-.59-.5-1.19-.98-1.79-1.47l.86-.69c.84.67,1.57,1.27,1.85,1.5.39.29.87.45,1.37.45,1.23,0,2.32-1.02,2.3-2.28,0-.42-.13-.84-.35-1.19s-.54-.65-.92-.84c-.38-.19-.8-.27-1.22-.23-.42.04-.82.19-1.17.44-1.82,1.51-3.71,2.97-5.56,4.44-.75.53-1.64,0-1.64-.87C.01,6.58.01,5.42.02,4.26c0-.88.89-1.41,1.65-.88l2.03,1.64-.84.68-1.75-1.38c0,1.12,0,2.24,0,3.36,1.75-1.36,3.49-2.74,5.2-4.15.62-.57,1.44-.89,2.29-.89h0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 807 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Recraft</title><path d="M19.667 8.275c0-4.57-4.15-8.275-9.27-8.275-1.774 0-3.213 3.705-3.213 8.275 0 1.143.09 2.233.253 3.224H4.29L1 23h9.4v-6.447c5.117 0 9.266-3.707 9.266-8.275l.001-.002zm-9.27-6.76c.93 0 1.682 3.028 1.682 6.76 0 3.733-.752 6.76-1.681 6.76-.93 0-1.681-3.027-1.681-6.76 0-3.732.752-6.76 1.68-6.76z"></path><path d="M19.848 16.552h-9.44L14.028 23h9.438l-3.618-6.448z"></path></svg>

After

Width:  |  Height:  |  Size: 562 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Replicate</title><path d="M22 10.552v2.26h-7.932V22H11.54V10.552H22zM22 2v2.264H4.528V22H2V2h20zm0 4.276V8.54H9.296V22H6.768V6.276H22z"></path></svg>

After

Width:  |  Height:  |  Size: 313 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Suno</title><path d="M16.5 0C20.642 0 24 5.373 24 12h-9c0 6.627-3.358 12-7.5 12C3.358 24 0 18.627 0 12h9c0-6.627 3.358-12 7.5-12z"></path></svg>

After

Width:  |  Height:  |  Size: 308 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Hunyuan</title><path d="M12 0c6.627 0 12 5.373 12 12s-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0zm1.652 1.123l-.01-.001c.533.097 1.023.233 1.41.404 6.084 2.683 7.396 9.214 1.601 14.338a3.781 3.781 0 01-5.337-.328 3.654 3.654 0 01-.884-3.044c-1.934.6-3.295 2.305-3.524 4.45-.204 1.912.324 4.044 2.056 5.634l.245.067C10.1 22.876 11.036 23 12 23c6.075 0 11-4.925 11-11 0-5.513-4.056-10.08-9.348-10.877zM2.748 6.21c-.178.269-.348.536-.51.803l-.235.394.078-.167A10.957 10.957 0 001 12c0 4.919 3.228 9.083 7.682 10.49l.214.065C3.523 18.528 2.84 14.149 6.47 8.68A2.234 2.234 0 102.748 6.21zm10.157-5.172c4.408 1.33 3.61 5.41 2.447 6.924-.86 1.117-2.922 1.46-3.708 2.238-.666.657-1.077 1.462-1.212 2.291A5.303 5.303 0 0112 12.258a5.672 5.672 0 001.404-11.169 10.51 10.51 0 00-.5-.052z"></path></svg>

After

Width:  |  Height:  |  Size: 954 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M7.51,3.04q.14,0,.27,0c.1,0,.2,0,.3,0,.1,0,.21,0,.31,0,.1,0,.21,0,.31,0,.26,0,.51,0,.77,0,.29.67.58,1.34.87,2.01.15.34.29.67.44,1.01.15.35.3.7.45,1.05.06.14.12.27.18.41.08.19.17.38.25.57q.04.09.08.17c.11.25.17.42.27.69h-2.36c-.45-1.18-.89-2.37-1.3-3.56-.31.75-.58,1.51-.85,2.27-.04.13-.09.25-.13.38-.11.3-.22.61-.32.92h-2.26c.29-.76.59-1.51.91-2.26.04-.1.09-.2.13-.31.09-.21.18-.43.27-.64.12-.27.23-.55.35-.82.11-.26.22-.53.34-.79.04-.1.08-.2.13-.3q.06-.14.12-.27c.03-.08.07-.16.1-.24.07-.15.12-.28.12-.28,0,0,.14,0,.26,0Z"/>
<path d="M1.65,2.96h3.82v1.65h-1.74v4.43H1.65V2.96Z"/>
<path d="M0,2.96h1.3v1.74H0v-1.74Z"/>
<path d="M8.25,6.78h.17c.23.72.46,1.43.7,2.17h-1.56c.21-.73.45-1.45.7-2.17Z"/>
</svg>

After

Width:  |  Height:  |  Size: 854 B

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Vidu</title><path d="M20.607 2.247c-2.917-.966-5.426 1.084-6.011 2.96 0 0-2.105 6.76-3.002 9.58-.428 1.346-1.489 3.548-3.487 3.548-1.627 0-2.463-1.527-2.816-2.437L2.865 9.431c-.281-.681.013-2.04 1.14-2.447 1.204-.432 1.978.575 2.178 1.11l3.022 7.74c.72-.928 1.178-2.438 1.476-3.507l-1.984-5.21C7.756 4.686 5.267 3.58 2.962 4.43 1.095 5.118-.702 7.474.275 10.435l2.504 6.44c.38.976 1.881 4.163 5.275 4.163 4.073 0 5.601-3.473 6.449-6.218.424-1.373 2.749-8.797 2.749-8.797.338-1.109 1.71-1.428 2.568-1.148.605.196 1.698 1.031 1.345 2.325-.066.236-1.92 6.209-2.604 8.026-.357.948-1.262 3.006-3.324 2.72-.628 1.39-1.15 2.199-1.94 2.925 2.572 1.218 6.32-.009 7.898-4.776.586-1.773 2.644-8.166 2.644-8.166.598-1.963-.469-4.768-3.232-5.682z"></path></svg>

After

Width:  |  Height:  |  Size: 912 B

Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff. Show more