Hermes adaptation note: upstream auteur generated assets through several local image CLIs. In Hermes, read every generation instruction as a call to the built-in image_generate tool with the same prompt (then move the returned file into the project's assets/gen/ path), use the terminal tool for ffmpeg/node/npx, and browser_exec or Playwright-via-terminal for screenshot loops. The per-CLI routing/strength tables below are upstream reference material — the taste guidance transfers, the CLI names do not.
scroll-flight — photoreal scroll-scrubbed video ("fly through the world")
auteur's video-scrub tier. The hero is a pre-rendered camera flight whose
currentTime is driven by scroll — the viewer pilots a photoreal world.
Complementary to the real-time WebGL recipes in scroll-cinema.md, not a
replacement.
the world must look photoreal — a real place, product, interior, landscape
the look is generative/abstract — fluid, particles, shaders, type
the motion is a camera flight through a fixed scene
the motion is procedural and reacts to cursor/audio/data live
you can generate/shoot video clips of it
you can express it as math in one WebGL context
They compose: a WebGL hero can hand off into a scrubbed-video mid-section.
⚠️ AI clips barely move the camera — the scroll-dolly does the travelling
The single most important thing to know here. image→video models (all the
common ones) animate a still ambiently — light shimmers, water
drifts, particles float — but they do NOT fly the camera through the scene.
Verified on disk: a clip's first and last frame are near-identical. So a scene
built only from a raw AI clip reads as a slightly-moving photo, not a journey.
This is the #1 reason a scroll-flight looks like a slideshow.
The fix lives in the engine, not the prompt: scroll-flight-engine.js applies a
scroll-driven camera dolly — as you scroll through a scene, it pushes the
whole scene IN (scale) and drifts it down, inside the scale overscan so edges
never reveal. That is what manufactures forward/descent travel; the clip's
currentTime scrub only adds the ambient life on top. Consequences:
Don't over-invest in per-clip motion. A near-static clip + the dolly looks
the same as an aggressively-prompted clip + the dolly. Prompt for mood and
ambient life (drifting particles, light, creatures), not "fast camera dash"
you won't get.
Stills and clips are interchangeable. A scene with only a still gets the
same dolly, so it reads identically to a video scene. Mix freely — ship the
clips you reliably get, fill the rest with stills, the dolly unifies them.
(ABYSS showcase: 2 video scenes + 3 stills, indistinguishable.)
Pick worlds whose medium drifts — underwater, clouds, space, dust, smoke.
Ambient drift hides the lack of camera-baked motion; a dry static landscape
exposes it.
Real camera travel baked into the footage needs a true flythrough model
(Higgsfield/Runway/Veo) or the 2.5D depth-parallax route (still → depth map →
WebGL camera through the layers). The dolly is the pragmatic default that
needs neither.
The pipeline (auteur's toolchain)
Scene stills — one anchor still first, get art-direction approval, then
batch the rest style-locked to the anchor (pass the approved still as the
style reference). A style miss caught on the anchor costs 1 gen, not N.
Source: image_generate — see assets.md.
Dive clips — animate each still into a short camera push-in (any
image→video model). One clip per scene.
Seams — two ways, pick by what your video model can do:
Crossfade seams (default, start-image-only models). Leave
connectors empty/null; the engine crossfades directly between adjacent
dives. Ship-safe, always works, reads clean. This is auteur's baseline.
Seamless flight (only with an end-image model — Higgsfield seedance /
kling). Generate connector clips whose start = prev dive's last frame,
end = next dive's first frame (extract the actual rendered frames, never
the stills). Verify every seam with the SSIM gate below before eyeballing.
Encode for scrubbing (§ below) — the single most important step.
Posters — extract each encoded clip's first frame (§ below).
Wire the engine config; run the motion + slopscan gates.
auteur has no Higgsfield account by default; a start-image-only
image→video model is the baseline. So the honest default is crossfade-seam photoreal scrub — still
Apple-tier. Fully-seamless chaining is an upgrade you unlock only with an
end-image-capable video model.
Encode for scrubbing — the -g 8 recipe (critical)
Scrubbing sets currentTime every frame; a decoder's seek cost scales with how
many frames it must decode from the nearest keyframe. A tiny GOP (keyframe
every 8 frames) is what makes frame-accurate seeking cheap. Native res, crf 20,
no audio, faststart, light sharpen:
Never upscale — encode what ffprobe reports (some models return 720p).
Mobile sibling (-m.mp4): 720p, -g 4 (twice the keyframes = ~half the
seek-decode work), crf 23. Wire as clipMobile/connectorsMobile. Still
choppy on a low-end phone → -g 2, or -g 1 (all-intra = instant seeks,
bigger files).
The engine loads each clip as a Blob (always seekable) and scrubs — it does
NOT rely on HTTP byte-range. Do not "optimize" that away, or you get frozen-at-
frame-0 on hosts that don't serve ranges.
Posters — from the ENCODED clip's first frame
The still is 3:2, the clip is a 16:9 re-render — if the still is the loading
poster, the video paints with a visible crop/render pop on the first scene a
visitor sees. Hand off the actual frame:
Keep the source still too: it's the reduced-motion artwork and the no-clip
fallback.
Seam QA — SSIM gate (before any eyeballing)
Seamlessness is the product; don't ship it on a squint. A true actual-frame
handoff scores SSIM ≥0.95 even after encoding.
seam_ssim() { # clipA clipB — last frame of A vs first of B
ffmpeg -v error -y -sseof -0.05 -i "$1" -frames:v 1 _a.png
ffmpeg -v error -y -ss 0 -i "$2" -frames:v 1 _b.png
ffmpeg -v info -i _a.png -i _b.png -lavfi ssim -f null - 2>&1 | grep -o 'All:[0-9.]*' | cut -d: -f2
}
# ≥0.90 pass · 0.75–0.90 warn (crossfade usually hides it) · <0.75 FAIL:
# an endpoint was a still, not the neighbour's frame — regenerate, don't rationalize.
Re-run after every re-roll: replacing one clip can silently break BOTH its seams.
Chain architecture — A vs B
A — one continuous forward take. Legs chained from actual last frames, no
pull-back, no end-image. Use for any grounded walkthrough. No rewind risk.
B — dive + connector interleave. More cinematic, but if a connector's
camera velocity reverses (dive pushes in, connector pulls back out) it
reads as a rewind even with a perfect frame-match seam. Inherent to B — keep
connector motion continuing forward, or use A.
Mobile & iOS — the hard gotchas (the engine handles these; don't undo them)
Blank/black scene on iOS → a muted video never played won't paint a seeked
frame. Engine keeps the poster up until a real frame paints and primes each
clip (muted play→pause) on first touch. Don't hide the poster on
loadedmetadata; don't strip playsinline/muted.
Frozen on iOS Low Power Mode → LPM rejects even muted play() and
currentTime scrubbing dies — no video technique survives it. Engine detects
the rejected prime and flips the whole page to stills-with-crossfades. Keep
that .catch() fallback if you adapt it.
Phone stutters on a fast flick → seeks pile up. Engine coalesces seeks
(never issues a new currentTime while seeking); ship the -m.mp4 tier as
the other half.
iPad gets blurry 720p → tier by screen short side (≤600 CSS px = phone),
never by pointer type or UA (iPadOS lies on both).
Page jumps while scrolling → the mobile URL bar fires resize; the engine
ignores height-only resizes (relayout on width change / orientationchange).
Copy behind the notch/URL bar → engine uses env(safe-area-inset-bottom) +
dvh; keep <meta viewport … viewport-fit=cover>.
Accessibility / SEO (auteur floor — enforced)
prefers-reduced-motion, data-saver → stills mode (no video load/decode),
cross-dissolving as you scroll. Never blank.
The engine builds its DOM in JS, so put a plain-markup copy block marked
data-sw-seo in the container — crawlers, link previews, and no-JS visitors
read it; the engine hides it on mount. Readable with JS off.
Upgrade path — canvas frame-sequence (Apple's actual technique)
For butter on low-end devices or when the blob payload (~8 MB × clips) is too
heavy: pre-extract N frames per clip (webp/avif), draw to <canvas> (WebCodecs
to decode ahead). Frame paint becomes deterministic — no decoder seek latency.
The seam doctrine, chain math, and pacing knobs all carry over; only the "paint
frame at time t" primitive swaps. More build tooling, more requests — reach for
it only when video-scrub genuinely stutters on the target hardware.
Technique & engine adapted from scroll-world by cyw
(github.com/cth9191/scroll-world), MIT. auteur pairs it with image_generate asset
production and its own slopscan / motionqa gates.