Communitygithub.com

HusseinRagab/motion-reels

Make 9:16 motion-graphics Reels (1080x1920) for an interior-design page with code-drawn animation - headlines, plans that draw on, before/after wipes, photo push-ins, material swatches - rendered frame by frame with Playwright and ffmpeg. Use when asked to make, storyboard, render or fix a Reel, Short or vertical motion video.

What is motion-reels?

motion-reels is a Claude Code agent skill that make 9:16 motion-graphics Reels (1080x1920) for an interior-design page with code-drawn animation - headlines, plans that draw on, before/after wipes, photo push-ins, material swatches - rendered frame by frame with Playwright and ffmpeg. Use when asked to make, storyboard, render or fix a Reel, Short or vertical motion video.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/HusseinRagab/reel-motion-kit/tree/HEAD/motion-reels

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

What does motion-reels do?

Code-drawn motion graphics for vertical Reels. Each reel is one or more blocks. A block is a small JavaScript file that places headlines, drawings, photos and swatches in a world and says when each one appears. Every time comes from a timeline of words: either a beat sheet (no voice) or the word timings of a voice take. The engine renders every frame on its own fresh browser page, checks the layout, and assembles the result with voice and/or music.

The person who owns the page decides the look, the words and what gets posted. Claude never posts anything.

Setup (once)

  • Python 3.10+ and ffmpeg/ffprobe on PATH. Then the repo's setup.ps1 (Windows) or setup.sh (macOS/Linux), or pip install -r requirements.txt + python -m playwright install chromium (or env MG_CHROME = an existing Chrome).
  • Voice only: pip install -r requirements-voice.txt (faster-whisper; first run downloads the speech model).
  • Test: python engine/selftest.py must print SELFTEST OK (checks pass on the examples, negative tests fail as they should, Arabic captions split right, and a short render + fault scan + assemble reaches -14 LUFS). --quick skips the render.

Engine files (all in engine/): render.py (check / storyboard / stills / video), beats.py, words.py, frames.py, faultscan.py, assemble.py, storyboard_sheet.py, make_grain.py, templates/ (shell, reel.js, reel.css, fonts).

One folder per reel

<reel name>/
  brief.md          what the reel is for, the one idea, the call to action, photos available
  beats.txt         the words on screen, one beat per line (or script.txt + take.wav when there is a voice)
  words.json        timeline (from beats.py or words.py)
  photos/           the owner's photos (only ones they have the rights to)
  A.js, B.js ...    blocks
  build/            check json, storyboard stills, block videos, render.json files, contact sheets
  <Reel>_REV01_<date>.mp4   the deliverable; a later fix ships as _REV02, never overwrites

Gates (never skip or reorder; stop at each ⛔ for the owner's OK)

  1. Brief. One idea per reel (a room reveal, a material story, a plan change, a tip). Ask for the photos and the handle. List what each photo shows. Hook in the first 2 seconds.
  2. Beat sheet (beats.txt): the exact on-screen words, in order, one beat per line. Keep a beat short (2–7 words). Put each item of a list on its own line, so each has time to land. ⛔ Owner signs the words. After sign-off a word change means re-timing the block (and re-voicing it, if voiced): say so before changing anything.
  3. Timeline.
    • No voice: python engine/beats.py beats.txt words.json (default pace 2.6 words/s + 0.7 s after each beat; [+1.5] at the end of a line holds longer; [pause 2] holds with nothing new).
    • Voice (any TTS or a recording, one take per block, never edited): python engine/words.py take.wav words.json, then compare the transcript with the script word for word. The picture is fitted to the take, never the reverse.
  4. ⛔ Storyboard stills. Build the block layouts (positions only are enough), run check, then render.py A.js words.json storyboard build/sb and storyboard_sheet.py build/storyboard.png build/sb. Self-review the sheet (list below), then send it. Nothing is animated before the owner OKs the layout.
  5. Build the motion. Cue every element on the word that names it (rules below). Run check after every change.
  6. Render. render.py A.js words.json video build/A.mp4 --workers 4 per block (never --draft for anything sent). Every block's build/A.mp4.render.json must say "capture": "fresh" and the same browser as the other blocks; if the browser changed, re-render all blocks.
  7. Fault scan. faultscan.py build/A.mp4 --sheet build/A_contact.png on every block, then on the final reel. Look at every flagged time. A black band or a one-frame flash is a fault: stop and fix it. Dark photo content is fine.
  8. Assemble. assemble.py blocks.json <Reel>_REV01_<date>.mp4 [--music bed.mp3]. It prints the measured loudness; it must be at the target (-14 LUFS default) and the true peak at or below -1.5 dBTP.
  9. ⛔ Review. Send the reel and its contact sheet. Picture-only fixes re-render only the changed blocks and ship as the next _REV. The owner posts it.

Motion rules (why: a calm, premium feel; nothing jitters, nothing is hard to read)

  • Words never move. A word fades in (0.28 s) and then stays still. No slide, scale, bounce or drift on text.
  • Headlines: hl(el, text, cue(...)) fades each word in on its own word in the timeline. When a headline paraphrases a voice, use {stream:true}: words appear left to right, 0.1 s apart, from the cue.
  • Camera: dead still while a shot holds; it only slides (0.8 s, ease-in-out) between shots. In 9:16, stack shots vertically in the world (shot 2 at y 1920, shot 3 at y 3840) and slide down: SHOTS=[[t,540,960,1],[t2,540,2880,1],...].
  • Photos may push in slowly: push:[1,1.04] = constant speed, never more than +6 % (MOTION.PUSH_MAX; the engine refuses more). No push on a photo that carries text.
  • Drawings draw on as they are named, reference first: walls before the window and door in them, the floor line before heights measured from it. at('#walls', cue('Walls'), null, 'draw', 0.9); 0.6–1.0 s per element.
  • Dimensions to one scale. If a plan shows two dimensions, both use the same scale.
  • Label what the words name; every number names its unit ("3.2 m", never a bare "3.2").
  • No bounce, overshoot, elastic or spring easing anywhere. Every timing comes from MOTION in reel.js; change it there, never as a literal in a block.
  • A bare dashed SVG element given to at() shows before its cue: wrap it in a <g>.

Timing tokens (MOTION in reel.js)

TokenValueUse
WORD_FADE / WORD_LEAD0.28 s / 0.08 sword fade-in; starts just before the word
STREAM0.10 sstream-mode headline word spacing
REVEAL / EXIT0.5 s / 0.4 sblocks, chips, swatches, photos in / out
draw (at(...,'draw',dur))0.6–1.0 sper drawn element
CAM_SLIDE / CAM_LEAD0.8 s / 0.35 scamera slide; starts before the shot's cue
PUSH_MAX0.06photo push limit
WIPE1.2 sbefore/after wipe
Easing: ease-out cubic for reveals, ease-in-out cubic for slides, draws and wipes.

Layout and the check

  • Safe zone (SAFE in reel.js): text, labels and captions stay inside x 65–1015, y 269–1248 of the 1080x1920 frame (top 14 %, bottom 35 %, sides 6 %). These figures are widely used third-party guidance for Reels, not a Meta-published rule for organic posts: re-measure from a phone screenshot of a posted reel and set safeZone({...}) if the app changes. Photos and drawings may extend past it; text may not.
  • render.py <block> words.json check build/<block>.check.json is mandatory before every render. It samples every 0.25 s and fails on any overlap held 0.5 s or more between separate elements (text, drawings, chips, swatches, photos, captions) and on any text outside the safe zone while the camera holds. A fail means fix the layout, never render. Prove a new rule with a negative test (re-introduce a known fault; the check must fail).
  • Text that belongs on a photo goes inside the photo's element (parent: photoDiv) with a scrim() behind it, or the photo is a background (bg:true). Otherwise the check reports text over photo, which is what you want.
  • The check does not see things cut off at the frame edge: the self-review does.

Building blocks (reel.js)

boot(()=>{                                   // every block starts with boot(...)
  handle('@yourstudio');                     // small handle at the top of the safe zone
  $('world').insertAdjacentHTML('beforeend', `<div id="h1" class="abs serif" style="left:90px;top:360px;width:900px;font-size:104px"></div>`);
  hl('h1', 'A small room, *re-planned.*', cue('A small'));          // *...* = accent italic; ~word = not in the timeline
  at('#walls', cue('Walls'), null, 'draw', 0.9);                   // fx: 'fade' | 'pop' | 'wipe' | 'draw'
  photo({src:rel('photos/living.jpg'), x:0, y:1920, w:1080, h:1350, a:cue('Light'), push:[1,1.04]});
  beforeAfter({before:rel('photos/before.jpg'), after:rel('photos/after.jpg'), x:90, y:2440, w:900, h:900, a:cue('Before'), t:cue('after')});
  swatch({name:'Oak', note:'floor', color:'#B08A62', x:90, y:4480, size:270, a:cue('Oak')});   // or src: rel('photos/oak.jpg')
  clip({dir:rel('clips/walk'), n:240, x:0, y:5760, w:1080, h:1920, a:cue('Walk'), t0:cue('Walk')}); // frames from frames.py
  captions(1120, 3);                         // burned-in captions from the timeline (voiced reels)
  SHOTS=[[0,540,960,1],[cue('Light'),540,2880,1]];
});

rel(path) resolves a file next to the block. cue('words', after) throws if the words are not in the timeline, so a typo fails loudly. Full example: examples/demo/demo.js.

Arabic and other right-to-left reels

  • Call rtl() first in boot(). Headlines use Noto Naskh Arabic, text IBM Plex Sans Arabic (OFL); Latin falls back to the default fonts. No italic, letter-spacing or upper case in Arabic (letters must join); accent words change colour only.
  • Text boxes keep left + width; text aligns to the right edge. Lay out lists right to left (first named = rightmost).
  • Cues ignore diacritics, tatweel, alef/yaa/taa-marbuta variants and convert Arabic-Indic digits; a cue with no letters throws. ة and ه count as one letter, so cue a longer phrase where two words differ only there. Number words match in English only: in Arabic, cue digits or the word as transcribed (words.py --lang ar picks the multilingual model).
  • Captions break on ، ؛ ؟ and Latin punctuation, and do not display it.
  • Sample: examples/arabic/.

Look

The theme lives only in engine/templates/reel.css (colours as CSS variables, fonts) and the font files. Default "warm minimal": paper #F4EFE8, ink #2B2622, accent #9C6B4E, Cormorant Garamond headlines + Jost text (both SIL OFL 1.1). To change the look: edit the variables, or add OFL fonts to engine/fonts/ with their licence file and list them in fonts.css and window.FONTS (reel.js). Ask the owner for 3–5 screenshots of their grid first and match it.

Self-review (Claude, before sending any sheet or render)

  1. Each element starts on its word; reference lines draw first.
  2. Words still once landed; camera still while holding; no jump at the end of a slide.
  3. Nothing cut off at the frame edge; all text inside the safe zone; nothing important under the bottom 35 %.
  4. Dimensions to one scale; every number has its unit; spelling matches the signed beat sheet.
  5. Check PASS on every block; render.json says capture: fresh, same browser for all blocks.
  6. Fault scan clean, or every flagged time inspected and explained.
  7. Loudness and true peak printed by assemble.py at target.

Rights and privacy

  • Only the owner's photos, or ones they have the rights to. Client homes: no faces, addresses or documents without consent.
  • Music: only tracks the owner has a licence for, or the platform's own library added in the app at posting.
  • No claims the owner has not made (prices, timelines, awards).

Troubleshooting

  • cue not found: "...": the words are not in words.json in that order; fix the beat sheet or the cue.
  • ERR reqfail:file:///...jpg: a photo or clip path is wrong; use rel() and check the file name.
  • Ghost copies of earlier drawings or a black band: you rendered with --draft. Re-render without it.
  • Slow renders: --workers 4 (up to 8) on a machine with that many cores; check and storyboard are fast, video is not.

Related Skills