Motion reel
A film is a pure function of time. window.seek(t) paints frame t in headless Chromium, and ffmpeg encodes it. Picture and sound share one timeline.json in beats on a measured grid.
Read before building: reference/RULES.md (contract, look, rhythm, motion, sound).
Also here: reference/ENGINE.md (engine API, patterns, commands), reference/AUDIO.md (music, grid, SFX, VO, mix), reference/CRITIQUE.md (critic prompt).
Toolchain: node + playwright (chromium), ffmpeg, python with numpy, scipy, soundfile, librosa and pillow. init.mjs checks them.
Platform notes (Windows, macOS, Linux)
- No
shneeded. Every command in this skill isnode …orpython ….init.mjsscaffolds a film on any OS. - Python command: commands here say
python.init.mjsfinds the interpreter that has the audio libraries and writes it to.pythonin the film folder (python3on macOS/Linux,pythonon Windows; thepython3Microsoft Store stub never works). Use that command wherever these docs saypython. Check the libraries with<python> -c "import numpy,scipy,soundfile,librosa,PIL". - Windows shell: use PowerShell. If
nodeis only available through fnm/nvm, load it first in the same call, e.g.fnm env --use-on-cd --shell power-shell | Out-String | Invoke-Expression; fnm use 24 | Out-Null. <skill>=~/.claude/skills/motion-reel($HOME\.claude\skills\motion-reelon Windows), or<project>/.claude/skills/motion-reelfor a project-only install. Playwright + Chromium live in the skill'snode_modules;init.mjslinks them into each film (symlink, or a junction on Windows).- The ffmpeg scene-cut command in step 2 pipes to
grep; in PowerShell useSelect-String pts_timeinstead.
0. Intake
Fill brief.md (template in templates/). Required inputs:
- product and URL
- duration
- formats
- brand colours and fonts (default: measured from the site)
- reference film (optional)
- music: a supplied file or
synth - voiceover: Fish Audio (voice) or none
If the user names a preset, apply it in step 1. --preset <name> looks in the project's presets/ first, then in this skill's own presets/ (blank, lukas-yt, plus any the user added): it fills the brand, colours, fonts, timeline defaults, voiceover and house notes. Then ask only for what it left as ?.
Ask for every missing input in ONE AskUserQuestion round. Use a default only where the user says "your call".
1. Scaffold
node <skill>/scripts/init.mjs videos/<distinctive-slug> [--preset presets/<name>]
--preset runs scripts/preset.mjs. It copies the preset's fonts to assets/fonts/, writes its colour tokens into film/index.html, merges its timeline defaults and prefills brief.md. Start from presets/blank/preset.jsonc, which documents every field.
Parallel sessions may run the same brief. init.mjs refuses an existing folder, so never write into one you didn't create. Work from the project root from here on.
2. Assets
node scripts/capture.mjs <url> --sections "Feature A,Feature B,Get started"
This gathers real screenshots, font files, colours, CSS variables, logos and the OG image into assets/. Add anything the user supplies. Point film/index.html @font-face at the real display and UI faces, and set the :root tokens.
If a reference film is given, extract frames and cuts:
ffmpeg -i ref.mp4 -vf fps=2 refs/frames/f_%03d.jpg
ffmpeg -i ref.mp4 -vf "select='gt(scene,0.3)',showinfo" -f null - 2>&1 | grep pts_time
3. Style guide
Write docs/style_guide.md from the template: palette (measured hex and source), type, rhythm (the reference's measured shot table), transitions, camera, texture, text in/out, and sound. Take the reference's grammar, never its content.
4. Beat grid
Set the bpm, duration and marks in timeline.json. Then:
- Synth music:
python scripts/music.py, thenpython scripts/beats.py audio/music.wav --stem audio/drums.wav. - Supplied music: copy it to
audio/music.wav, thenpython scripts/beats.py audio/music.wav.
Then run node scripts/sync.mjs. Details: AUDIO.md §1.
5. Shotlist, then STOP for the OK
Write docs/shotlist.md from the template. Each shot needs beats and time, exact on-screen text, motion and transition, the SFX mark, the exact VO line, and 9:16 notes. It must satisfy the rules: hook by 2 s, something new every 2–4 s, end card ≤ 2 s.
Send the user a short summary: shotlist table, palette, fonts, music plan, VO voice, and the VO credit cost if any. Wait for an explicit OK. Build nothing until then. Mark the shotlist APPROVED, and reflect every change the user asks for.
6. Voiceover (only if requested)
Fish Audio MCP: test the tightest line on 2–3 voices, generate one take per line, download it, run vo.py --scan, write vo.json, run vo.py, then sync.mjs. Details: AUDIO.md §3.
7. Build with springs
Replace the starter scenes in film/film.js. One C.scene() per shot; marks and cues live in timeline.json. Use:
C.spHitfor anything with a soundTYPE.risefor typeC.pickfor per-format layoutMotion.indicator/trackfor multi-target motion
Declare every SFX in timeline.sfx as you build it. After each edit, check with:
node scripts/render.mjs --sheet --all # LOOK at review/sheets/*.jpg (Read the images)
node scripts/render.mjs --at 3.2,3.25 [--fmt 9x16]
node scripts/render.mjs --range 3,5
Verify determinism once per project: node scripts/render.mjs --verify --all (must report all probes identical).
Rendering uses the GPU first and the CPU only as the fallback: every render.mjs run picks the backend itself, so just run it, never add --gpu off or --encoder cpu to avoid trouble. Read its [render] painting: and [render] encoding: lines, keep one backend for the whole film (do not mix GPU and CPU renders), and tell the user which one was used and, if it was the CPU, why. Details: RULES.md → Render contract.
8. Critique loop: at least 3 rounds, until every score is ≥ 8
Each round:
node scripts/sfx.mjs && python scripts/mix.py(draft sound, so sync can be scored)node scripts/render.mjs --sheet --all && node scripts/render.mjs --draft --allpython scripts/review.py <N> --draft- Critic: spawn a fresh subagent with
reference/CRITIQUE.md, the project path and N. It LOOKS at every sheet and strip, scores the 8 criteria with evidence, and appends the round todocs/review_log.md. - Fix the 3 worst problems, verify each fix with stills or clips, and log what changed.
Stop only when the verdict is SHIP: every score ≥ 8 and at least 3 rounds done. Never show the user a film before that.
9. Final render, SFX, mix, all formats
- Render the primary format:
node scripts/render.mjs(60 fps, adaptive 180° motion blur; GPU painting and hardware encode first, CPU fallback; report the backend it prints). Watch it viareview.pyon the final:python scripts/review.py final. - SFX: re-check
metrics.syncon the final. Nudge marks or gains intimeline.sfx, then runsync.mjsandsfx.mjs. - Mix:
python scripts/mix.py. It must reach -14 LUFS with true peak ≤ -1 dBTP and print no WARNING. - All formats:
node scripts/render.mjs --all(or--mux --allif only the audio changed). Runpython scripts/review.py finalagain and look at everyphone_*.jpgandsafe_9x16.jpg.
10. Deliver
Report:
renders/<fmt>.mp4paths, duration, loudness, and final scores (the table fromreview_log.md)- the contact sheet
- anything knowingly left imperfect
Offer tweaks. Changes go through timeline.json / film.js, followed by --draft and review.py before any re-final.