Communitygithub.com

yazzang-homelab/motion-studio

Build, debug or speed up the seek(t) render engine: render contract, canvas vs page capture, workers/subframes/chunks, determinism fixes, Remotion or HyperFrames handoff. Use for 'render is slow', 'frames differ', 'use Remotion'.

Was ist motion-studio?

motion-studio is a Claude Code agent skill that build, debug or speed up the seek(t) render engine: render contract, canvas vs page capture, workers/subframes/chunks, determinism fixes, Remotion or HyperFrames handoff. Use for 'render is slow', 'frames differ', 'use Remotion'.

Funktioniert mit✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/yazzang-homelab/motion-studio/tree/HEAD/skills/seek-engine

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Seek engine (course step 07)

A film here is a program, not a video. The page paints the exact frame for any time t; the renderer walks time, captures each frame and pipes it into ffmpeg. Nothing depends on a clock, so a render is identical on every run and a fix is a one-line edit plus a re-render. This is route A, the route Opus picks by default. Use it unless the user names Remotion or HyperFrames (then read references/route-b.md).

Run everything from the film project root (the folder with studio.json). If there is none, run /motion-studio:studio-init first. Files of this skill live in ${CLAUDE_SKILL_DIR}; the relative links below point there (${CLAUDE_SKILL_DIR}/references/determinism.md, ${CLAUDE_SKILL_DIR}/references/route-b.md).

1. The two halves

HalfFilesJob
Pageindex.html, film/film.js, lib/runtime.js (+ lib/*.js)boot(film) loads studio.json, audio/beats.json (optional) and bundled fonts, then exposes window.seek(t) and window.__studio
Drivertools/render.mjs (+ tools/studio.mjs)serves the project on http://127.0.0.1:<random>, launches a headless browser (Windows: Playwright's headless shell, never an installed Chrome or Edge; macOS and Linux: Chrome, Edge, then the bundled one), opens N worker pages, captures frames in order, encodes one H.264 stream

The page is always served over http. file:// breaks fetch('studio.json'), ES modules and paths containing # or %.

2. The render contract

MemberContract
?format=9x16&scale=1&render=1query the driver sends; format defaults to studio.primaryFormat, scale 0.1–1
window.seek(t, blur?)synchronous paint of time t; loop films wrap with loopT, others clamp to [0, duration]; returns true. The optional second argument { frameT, sub, subs } says this paint is subframe sub of subs of the output frame centred on frameT (page capture sends it for every screenshot)
__studio.readyPromise; resolves after config, beat grid, fonts (loadFonts asserts every face) and setup() are done
__studio.errorstring stack when boot failed (the driver prints it)
__studio.meta{ title, duration, fps, format, width, height, logicalWidth, logicalHeight, scale, bpm, loop, capture }
__studio.frame({ t, sub, shutter, fps, wrap })PNG data URL of frame t; sub > 1 accumulates centred subframes in the page (motion blur); wrap: false paints the raw time clamped to [0, duration] even in a loop film
__studio.hash(t, { wrap, sub, shutter, fps })sha256 of the raw pixels after paint(t) (determinism checks); hash({ t, wrap: false }) is accepted too. hash(0) versus hash(duration, { wrap: false }) is the loop-closure invariant
__studio.pixels(t, w)small RGBA thumbnail as base64 (critique metrics); always wraps in loop films
__studio.cues() / shots() / gridSFX cues, shot windows, beat grid as JSON (read by sfx.mjs, stills.mjs --shots, critique.mjs). shots() is in time order and lists scenes only, never overlays; grid carries hits
__studio.textUse() / coverage(family, weight, style, chars) / textTrackthe critique's glyph check. With window.__TEXT_TRACK__ = true before load (or ?texttrack=1) lib/draw.js records the characters text(), kinetic() and textWidth() draw per font; textUse() resolves [{ family, weight, style, chars }] for the frames painted so far (seek() through the film first), coverage() resolves { missing, checked } for one face. Off by default, never changes a pixel; text() and kinetic() do not throw for an unregistered family

What a scene sees in c while a motion-blurred frame is made: c.t is the time being painted (the SUBFRAME time, so motion blurs by itself), c.frameT the output frame's centre time (the same for every subframe), c.frame the output frame index floor(frameT x fps) (the same for every subframe), c.sub / c.subs the subframe index and count. Frame-indexed noise uses c.frame (grain(g, W, H, c.frame) then looks the same in stills, preview and the final), and "on twos" uses stepTime(c.frameLt, 12). Details: docs/ARCHITECTURE.md section 5.2 in the plugin.

Film code obeys the rules in references/determinism.md. The short form: every value is a function of t; seeded randomness per element with rngFor(name, i); no Math.random, Date, performance.now, timers, requestAnimationFrame, CSS transitions or state carried between frames; fonts and assets are local files. npm run lint checks this, and the plugin hook lints each edit to film/, lib/ and index.html.

3. Commands

GoalCommand
Live preview (scrub, Space, arrows, [/] shots, F formats, G safe guides)npm run preview
Environment check (Node, Playwright, browser, ffmpeg, fonts, keys present)npm run doctor
Install the Windows default browser (Playwright's headless shell, about 115 MB)npx playwright install chromium-headless-shell (npm run setup:browser)
Fast pacing check (half scale, 30 fps, no blur)npm run animatic (writes out/animatic/<fmt>/silent.mp4)
Render the primary formatnpm run render
Render a time range for a checknode tools/render.mjs --from 4 --to 6 --draft --out out/check (writes out/check/<fmt>/clip_4.00-6.00.mp4)
Render every formatnpm run render:all
Final render, all formats (blocked until the critique gate passes; about 4 min for the 12 s demo, so run it in the background)npm run render:final
Long form, resumable segmentsnode tools/render.mjs --format all --final --chunk 10
Per-frame hash manifestnode tools/render.mjs --hash (writes frames.sha256; a range writes clip_<from>-<to>.sha256)

Output per format: out/<fmt>/silent.mp4, out/<fmt>/poster.png (at studio.poster or 0.35 x duration) and out/<fmt>/render.json (fps, frames, workers, browser version, capture, encode settings, seconds). mix.mjs reads render.json for the exact audio length. Everything is built in out/<fmt>/.staging/ and published as one unit: the old files move aside first, the new ones move in with silent.mp4 last, and any failure puts the old ones back. If another program holds an output open (a video player, an image viewer on Windows), the render waits up to 30 s (MOTION_PUBLISH_WAIT_MS), warns at the start which files are open, and otherwise stops with cannot replace <file>: EBUSY ... and KEEPS the finished render in out/<fmt>/.staging/ (the previous outputs stay untouched). Close the program and copy the files from .staging/, or render again. A full render with --no-poster removes the previous poster.png, and one without --hash removes the previous frames.sha256, so no output describes an older cut.

A range render is partial only when --from/--to leave frames out; it writes clip_<from>-<to>.mp4 with its own .json and .sha256 instead, never replaces silent.mp4, and cannot be --final. A range that covers the whole film (or a --to past the end) is a full render. Keep check renders under --out out/check so out/<fmt>/ holds only the real film.

4. Capture modes

ModeSet byHow a frame is madeUse for
canvas (default)nothing__studio.frame paints sub subframes at t + ((j + 0.5) / sub - 0.5) * shutter / fps, sums pixels in the page, returns one PNGeverything drawn on <canvas id="stage">
pagefactory returns capture: 'page' or studio.json "capture": "page"seek(t) then page.screenshot per subframe; ffmpeg tmix + select blends each groupDOM films (styled elements updated from t)

shutter 0.5 is a 180-degree shutter: the blur spans half a frame interval. Fast whips and pans can show ghost copies at --sub 4; raise to --sub 8 for those films. Page capture is slower (one screenshot per subframe) and inherits DOM rendering differences, so prefer canvas.

5. Performance knobs

Paint count = frames x subframes. A 12 s film at 60 fps with --sub 4 is 2,880 paints per format.

FlagDefaultEffectWhen
--workers Nmin(4, max(1, floor(cpus / 3)))parallel pages in one browser feed one ordered encoderraise on big machines; lower if RAM is tight
--scale s1canvas = even(round(W x s)); 0.5 = a quarter of the pixelsdrafts, stills of long films
--sub Nstudio.subframes (4)subframes per frame--sub 1 for drafts; 8 for fast motion
--fps Nstudio.fps (60)output frame rate30 for animatics
--draftoffencode.previewPreset + CRF 23any non-final render
--crf N / --preset p16 / slowx264 quality / speedleave for finals
--chunk Soffresumable out/<fmt>/.parts/seg_<a>_<b>.mp4, then concat -c copyfilms over ~30 s, a final of three formats, overnight runs (an interruption then resumes instead of restarting)
--no-posterposter onskip the poster stillbatch checks

Measured on the reference laptop (12-thread i7-1255U, installed Chrome, 1080x1920, 60 fps, 4 subframes, 4 workers; the Windows default is now the headless shell, whose per-frame speed was not re-measured, and which adds 15 to 25 s per tool run for start-up on a machine with real-time antivirus, measured):

SetupTime per output frameTime for 15 s at 60 fps
the course's screenshot-per-subframe loop (capture only, synthetic scene of 200 arcs, text and a gradient)about 1,575 msabout 24 min
in-page accumulation with 4 pages (the same synthetic scene, capture only, pixel-identical)about 148 msabout 2.2 min
this renderer end to end on the template's 12 s demo film, final x264 encode123 ms in 9x16, 75 ms in 1x1, 118 ms in 16x91.8 min in 9x16 (about 4 min for the three formats of the 12 s film)

The first two rows are the capture-only benchmark that chose the design; the last row is what a real film costs, and is the figure to quote as an estimate (it rises with scene complexity and with other programs loading the machine, up to about 5 min for the three formats). x264 -preset slow encodes about 18 fps at that size, so capture is the bottleneck. Progress, fps and ETA print on stderr once per film-second. Start a final render in the background and poll the log; never wait for it in the foreground.

6. Proving determinism

  1. npm run critique runs the determinism check: __studio.hash at times that straddle every shot boundary and cue (plus and minus one frame), pass A in order, pass B shuffled, pass C after a reload. out/review/<fmt>/metrics.json lists every mismatching t.
  2. After a range render, prove the range is deterministic with two hashed renders of the same range. Vary the worker count so parallelism is covered too: node tools/render.mjs --from 0 --to 2 --hash --draft --no-poster --json --workers 1 --out out/check/a and node tools/render.mjs --from 0 --to 2 --hash --draft --no-poster --json --workers 4 --out out/check/b. A range never touches silent.mp4. Each run writes out/check/<a|b>/<fmt>/clip_0.00-2.00.mp4 plus .json and .sha256 (one line per frame: index t sha256), and its --json line reports framesDigest, the sha256 of the hash column joined by newlines. Equal framesDigest values in every format mean identical frames. If they differ, compare the two .sha256 files: git diff --no-index --quiet out/check/a/<fmt>/clip_0.00-2.00.sha256 out/check/b/<fmt>/clip_0.00-2.00.sha256 exits 0 when they match and git diff --no-index shows the first frame that differs. Never md5, which does not exist on Windows or Linux.
  3. Do not hash the mp4: x264 output changes with the thread count, so mp4 hashes differ across machines. The framesDigest covers the frame PNGs that go into the encoder, which is what determinism is about.

The promise is bit-identical frames on the same machine and browser build (recorded in render.json). Across machines, fonts rasterize differently (DirectWrite, CoreText, FreeType); compare visually.

7. Troubleshooting

SymptomCauseFix
Executable doesn't exist ... chromium_headless_shell, or could not launch a browser: no safe browser found. (Windows)Playwright's headless shell is not downloaded (npm install does not fetch it)npx playwright install chromium-headless-shell. On macOS and Linux an installed Chrome or Edge also works; on Windows an installed one is a guarded opt-in (studio.json "browser": "chrome", MOTION_BROWSER=chrome) because each fresh-profile launch counts as a failed Windows logon
Windows account locked, Security log events 4625 from chrome.exe, or refusing to start chrome: N launches ...an installed Chrome or Edge was started with a fresh profile (blank-password test = failed logon)switch to the headless shell as above and unset MOTION_BROWSER / MOTION_CHROME_PATH; the confirm commands and the reasons are in ${CLAUDE_SKILL_DIR}/../studio-init/references/troubleshooting.md, "Windows account gets locked out while rendering or testing"
ffmpeg not foundnot on PATH, ffmpeg-static missingnpm i -D ffmpeg-static or set FFMPEG_PATH; npm run doctor prints per-OS commands
spawn EINVAL in your own script on Windowsspawning npm/npx .cmd shims without a shelluse spawn('npm.cmd', args, { shell: true }); the tools already do
font missing: 700 64px "X" or ready timeoutfamily not bundled or misspellednpm run fonts -- add "X:400,700"; use the names in studio.json brand.fonts
blank or partial frames, page errors in the logexception inside a scene drawthe driver prints __studio.error and console errors; reproduce with npm run preview at that time
an element is missing only on some framesa NaN input (draw helpers skip NaN draws)log the input in preview; usually t - from before a scene starts or a division by zero
hashes differ in pass B (shuffled)state carried between framesmove caches and counters into the factory or setup() (determinism)
still elements jitter while others moveone RNG stream shared across elementsrngFor(name, i) per element, created inside the draw
accent color shifted in a playerencode without BT.709 tagsuse tools/render.mjs; if you re-encode, keep the four bt709 tags
width not divisible by 2 in custom encodesodd dimensionsthe renderer pads to even; keep scales that give even sizes
film failed to load (<url>): the page threw while booting: ... or window.__studio was never created within 15 san exception at import (bad import path, syntax error) or index.html never calls boot(film)fix the first pageerror: / console: line the message lists; a 404 on an import shows up there too
film failed to load ...: __studio.ready rejected: <text>setup(), the beat grid or loadFonts threw (font missing: <spec>)the text names the cause; __studio.error carries the stack
note: the browser did not close within N s; killed itthe installed Chrome's profile cleanup is slow on Windows (measured); the tools bound every browser and context closeharmless, the output is already written; MOTION_CLOSE_TIMEOUT_MS (default 8000) changes the wait
a frame takes longer than 2 minutes and the render abortsa scene draw is extremely slow or stuckreproduce that time in npm run preview; MOTION_FRAME_TIMEOUT_MS (default 120000) changes the limit
--final renders the whole film; drop --from/--toa range combined with --finalfinal renders are always the full film; use a range without --final for checks
cannot replace <file>: EBUSY ... The finished render is kept in out/<fmt>/.staginga player or viewer holds silent.mp4, poster.png or render.json open (Windows)close it and copy the files from .staging/, or render again; MOTION_PUBLISH_WAIT_MS (default 30000) sets how long a held file is waited for
film failed to load ...: __studio.ready rejected: <cause> (__studio.error: ...)the boot promise rejected; the cause comes firstfix the named cause; a the page threw while booting: ... line means an import or syntax error before boot() finished
a request for /.env, /.git/config or an 8.3 short name of one returns 404the tools' static server hides every dot-file and dot-directory of the project, also through symlinks, junctions and 8.3 names, and on Windows any request path containing a backslash or :expected; keep film assets out of dot-folders
hero type looks softthe context was scaled to size the text, or DOM will-change on a scaled subtreeset the font size instead of scaling; never will-change on scaled elements

For hard cases delegate to the render-engineer subagent with the failing command, its stderr and render.json.

8. Route B handoff

Only when the user names Remotion or HyperFrames. Both need Node 22 or newer; Remotion needs a Company License for for-profit organizations with more than 3 employees. Follow references/route-b.md step by step: pre-flight, install, explicit invocation, preview in the background, render only after the user's OK.

Next: /motion-studio:springs to make the motion feel physical, then /motion-studio:critique-loop to review the frames.

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

yazzang-homelab/motion-studio

Make Claude watch its own frames: critique.mjs evidence, harsh 7-axis scores, timestamped P0/P1/P2 problems in docs/review_log.md, fix the 3 worst, re-check, pass the gate. Use for 'critique it', 'review the frames', 'is it good'.

yazzang-homelab/motion-studio

Plan long-form or overnight films: director's brief (PLAN FIRST or GO, budget, definition of done), ANIMATION_GUIDE + STORYBOARD, chapter subagents, gates, chunked render. Use for 'overnight film', 'music video', '2-minute film'.

yazzang-homelab/motion-studio

Make a launch video, app or product reel, explainer or motion ad from code, end to end: brand assets from a URL, beat-grid shot list, synced sound, critique loop, all formats. Default for new video requests; not footage edits.

yazzang-homelab/motion-studio

Brand-asset stage of a product film (motion-reel runs the whole film): capture real screenshots, logo, colors and fonts from a URL, never redraw UI; brand story beats; voice + mascot. Use to grab or fix a brand's assets.

yazzang-homelab/motion-studio

Turn a reference (a frame, a video or an image library) into docs/style_guide.md and a beat-grid shot list: measure cuts, palette and pace, take grammar not content, wait for OK. Use when the user supplies or names a look.

yazzang-homelab/motion-studio

Ship every format from one timeline, then package it: layout() reframing (never crop), render all formats, per-format checks, deliver.mjs, a brand skill, a service offer. Use for 'export 9:16 and 16:9', 'deliver', 'make a skill'.

yazzang-homelab/motion-studio

The viral one-line 'showreel for a résumé' prompt: why it works (anatomy), the credited quote, and seeded variants that avoid look-alike reels; runs it as an engine test. Use for that prompt, its variations, or a showreel.

yazzang-homelab/motion-studio

Score a film to its beat grid: measure a track (beats.mjs) or synthesize one (score.mjs), cue SFX from the film, add voice, mix to -14 LUFS / -1 dBTP at exact length. Use for 'add music', 'sync to the beat', 'SFX', 'loudness'.

yazzang-homelab/motion-studio

Make motion feel physical with closed-form springs from lib/motion.js: presets, springFromFeel, track() for multi-target values, loopTrack loops, indicators, text swaps. Use for 'feels linear', 'add bounce', 'easing to springs'.

yazzang-homelab/motion-studio

Scaffold a motion-studio film project (seek(t) renderer, springs, beat grid, synthesized sound, critique tools), install it, set up the browser, run doctor. Use for 'set up the motion studio', 'new film project', 'init studio'.

yazzang-homelab/motion-studio

Write a six-part XML state spec for a one-shape UI morph film (inputs, direction, structure, build, gotchas, start): states on downbeats, cursor-driven changes, seamless loop, approval gate. Use for 'UI morph', 'state list'.

Verwandte Skills