Motion Ad Studio (self-improving)
Make a 20-40 s vertical motion-graphics ad that a senior motion designer would sign: frame 0 already moving, one carrier object through the film, transitions cut mid-motion and velocity-matched, and every click, landing, drop and whoosh locked to a spoken word or a music beat. The film is a single HTML page (GSAP timeline + pure functions of time). It renders frame-accurately to MP4, and scripts check it before anyone watches it. Sound effects are placed from the film's own event list, so audio sync holds by construction rather than by ear.
Every run: read references/lessons.md and references/runs.md FIRST. Finish with the Retrospective (step 13).
The skill improves only if you write the lessons back.
0. Preflight (always)
python scripts/preflight.py [--elevenlabs-mcp yes] # pass yes if YOU have ElevenLabs tools connected
Paths used below: scripts/..., references/... and templates/... are relative to this skill's folder
($SKILLS/motion-ad-studio). $MOTIONMAXXING is the engine folder ($SKILLS/motionmaxxing; preflight prints it), and
$SEAM_GATE is the optional seam-gate script that preflight finds (or pass --seam-gate FILE).
| Need | Why | If missing |
|---|---|---|
| motionmaxxing engine (Apache-2.0) | render.mjs (frames → MP4), look.py gates, lint.mjs design lint, runtime/vendor/gsap.min.js | git clone https://github.com/Tejashmakwana/motionmaxxing next to this skill, or set MOTIONMAXXING |
| ffmpeg + ffprobe, Node 18+, Chrome/Chromium/Edge | render, probe, mix | install; set CHROME_PATH if Chrome is not auto-found |
| Python 3.9+ with numpy + Pillow | beats, synth bed, mix, sheets | pip install numpy pillow |
| faster-whisper (optional) | word timestamps for word-locked timing | pip install faster-whisper, or any aligner that gives word start/end times |
| seam gate (optional) | proves each transition is velocity-matched | HyperFrames motion-doctrine + patches/seam-gate-camera-overlap.patch |
| ElevenLabs (optional, recommended) | expressive VO, composed music, custom SFX | see the next section |
ElevenLabs is optional: notify the user once, never block
If preflight prints the NOTICE (no API key and you have no ElevenLabs tools), relay it to the user in their language before the audio step, in one short message, and let them choose:
- connect: they add an
ELEVENLABS_API_KEYor connect the ElevenLabs MCP server. Re-run preflight. - continue: build the full film with the no-key path below. Say once in the delivery note that ElevenLabs would lift VO and music quality. Do not ask again in the same project. If you cannot ask (batch or non-interactive run), continue without it and put the recommendation in NOTE.md.
| Layer | With ElevenLabs | Without it (still ships) |
|---|---|---|
| Voice-over | TTS with emotion tags, 2 takes, pick one | the user's own recording (best), or a local/OS TTS guide voice; or a music-led film with no VO |
| Music | music generated to the film length, drop on the payoff | scripts/synth_bed.py (procedural, exact beat grid), or a track the user has the rights to |
| SFX | 1-3 hero samples (whip, whoosh, typing) | scripts/mix_events.py synthesizes every SFX locally |
| Word times | speech-to-text with timestamps (or TTS timestamps) | scripts/words.py (local faster-whisper) |
Details, prompts, credit costs and licence rules: references/audio-providers.md.
Pipeline
-
Workspace.
python scripts/new_film.py <project>copies the one-take kit, the GSAP runtime and local fonts. Render one still of the untouched demo to prove the toolchain:node $MOTIONMAXXING/scripts/render.mjs <project>/film/index.html --still 1.2,16.4 <project>/stills. -
Brand kit. Get the official logo files from the user and crop each to its alpha bbox. If a carrier will dock into the logo, measure the sub-glyph rects. Take colours by counting
#hexin the site CSS and fonts as local woff2 (scripts/fetch_fonts.py). Use the CTA channel the brand actually uses. Never redraw or approximate a logo: ask for it. -
One idea + a new style. Write one idea only this brand could film. The cover-the-logo test: hide the logo and the film should still point to this brand. Write three directions, kill two, and pick a style DIFFERENT from the last row of
references/runs.md. -
Hook. Write 3 variants under 12 words (question, negation, specificity, story) and pick one. Frame 0 is already busy and moving, the hook is on screen by frame 1, and nothing fades up from black.
-
Voice-over. Write about 2.5 words/s (30 s ≈ 70-75 words). If the TTS supports emotion tags, mark each line ([curious] [confident] [energetic] [playfully] [warmly]). Generate or record it, trim long pauses, fit the length with
atempo≤ 1.1, then get word times:python scripts/words.py audio audio/vo.wav --lang <code>. Whisper gives you TIMES only. Caption text always comes from the approved script. -
Music. Generate (length + BPM + "drop at "), license a track, or run
python scripts/synth_bed.py --bpm 120 --dur 32 --drop 15.8 --out audio/bed.wav. Get the grid withpython scripts/beats.py audio/music.mp3 audio/beats.json(synth_bed writes an exact.beats.json). Trim the music head so the drop lands on the payoff VO word: film beat k = first_beat - trim + k·period. -
STORYBOARD.md + ledger.json (
templates/one-take/has a filled example). Include:- the page spec, the idea and the three directions;
- a rhythm sheet (stretch / t / energy 0-10 / route / music);
- a timing map: every event with the word
w(i)or beatbeat(k)it is locked to; - the VO and the list of illustrative data;
- the ledger: one row per seam (exit/entry axis + direction, cut time).
-
Build. Copy patterns from the kit (
references/one-take-kit.md):- clocks
w(i)/we(i)/beat(k); - per-frame order
tl.seek → PRE (cameras, moving anchors) → cursors (read live rects) → UP (UI that follows); - keyframed 3D camera and a cursor engine that targets live DOM rects;
- typing/text/class updaters as pure functions of t;
sfx()events and camera-derived whooshes.
Contract the renderer needs:
#maincarriesdata-composition-id="main" data-width data-height data-duration.window.__seek / __duration / __timelines / __events / __player / __ready / __playerReady / __renderReady. Montage-style films (layered scenes, flat UI, logo carrier):references/montage-kit.md. - clocks
-
Look at it. Render 15-20 stills (
node $MOTIONMAXXING/scripts/render.mjs ... --still t1,t2,... stills), tile them (python scripts/sheet.py stills sheet.jpg 6 220) and run one vision pass: one line per frame, then a defect list. Fix and repeat 2-3 passes.- Geometry truth (what is where, visible, its opacity) comes from
node scripts/probe.mjs index.html "t1,t2" "#a,#b". - Text truth comes from a full-res crop: vision misreads downscaled Arabic and CJK as "typos" that are not there.
- Geometry truth (what is where, visible, its opacity) comes from
-
Seam gate (recommended). Serve the film folder on localhost, then run
node "$SEAM_GATE" verify --ledger ledger.json --url http://127.0.0.1:<port> --comp-url http://127.0.0.1:<port>/index.html. The target is 0 FAIL. Rows where a one-take camera whip keeps both windows on screen need"overlap": "camera"plus the patch inpatches/. Stop the server afterwards. Seereferences/qa-gates.md. -
Mix. Every SFX lives in
window.__events({t, type:'sfx', name, gain, dur?, speed?}).- Dump them with
node $MOTIONMAXXING/scripts/render.mjs index.html --still <t> ev, which writesev/events.json. - Mix:
python scripts/mix_events.py --events ev/events.json [--vo vo.wav --vo-at 0.10] [--music bed.wav --music-trim s] --dur <s> --out audio/mix.wav [--music-db -24] [--tp -2.5] [--whip/--whoosh/--typing samples] [--lift t0:t1:dB] [--fade t]. - Levels: VO at -18 dBFS RMS, music at -30 with ducking under the VO, whooshes peak-aligned to the camera's fastest frame, two-pass loudnorm to -14 LUFS / TP ≤ -1.
- Dump them with
-
Render + gates + deliver.
- Render:
node $MOTIONMAXXING/scripts/render.mjs index.html <NEW-name>.mp4 --audio ../audio/mix.wav --crf 17 --grain 0.03. Use a new output name for every attempt: two renders into one file corrupt it. ffmpeg -v error -i x.mp4 -f null -must print nothing. Then runpython $MOTIONMAXXING/scripts/look.py x.mp4 --out look --expect <dur> --expect-audio(G0/G2/G3/G5 PASS) andnode $MOTIONMAXXING/scripts/lint.mjs index.html.- Deliver: the 9:16 master, a 4:5 crop (
crop=1080:1350:0:200once the layout is checked), 2-3 cover PNGs, NOTE.md (idea, timing map, real vs illustrative data, audio sources + licences, gate numbers, what to improve) and aproject/folder (film + audio + scripts). - Clean up: stop servers and orphan headless Chrome.
- Render:
-
Retrospective (MANDATORY, before your final reply). Append dated rule + why lessons to
references/lessons.md. Add a row toreferences/runs.md(date, product type, style, length, gate numbers, verdict when known). Patch this file, a script or the kit if a step changed. When the user reacts later, record the verdict. Lessons hold method only, never client names or private data.
Quality bar
- Hook: real customer phrasing or a curiosity gap. Frame 0 moving, the hook readable by frame 1, no fade from black.
- Continuity: one carrier through the film (cursor, chip, logo mark), no crossfades, and 2-3 transition types repeated. Cuts happen mid-motion and velocity-matched. Keep one current direction: for "next", RTL films travel content left→right and LTR films right→left. Reserved vectors are spent on meaning.
- Timing lock: every click, drop and landing sits on
w(i)orbeat(k), and the music drop sits on a VO word. Whooshes derive from camera velocity. The timing map lists each event and what it is locked to. - No idle wobble: every hold has a camera move with intent, a staged reveal or live UI. Stillness belongs only right before a payoff.
- Safe zones (1080×1920):
- paid Meta placements: readable text in y 270-1250 and x 60-960 (the right 120 px is buttons);
- organic Reels/TikTok: keep the bottom 320-370 px clear;
- put the caption rail under the UI panels, never over a composer;
- drop rail words the picture already shows.
- End: wordmark plus one CTA in the brand's real channel. Hold the end 0.6-1.4 s and keep the final shot ≤ 25 % of the film.
- Honesty: declare illustrative data in NOTE.md. Never invent performance claims, prices or testimonials.
- Targets: look.py G0/G2/G3/G5 PASS, motion energy ≥ 5/255 mean luma change (dark UI films sit near 2), cut-sync ≥ 35 % (the measured pro benchmark), -14 LUFS ± 0.5 with true peak ≤ -1 dBTP, 0 decode errors.
Platform notes
- Background jobs on Windows git-bash: call
node.exe, not thenodewinpty alias ("stdin is not a tty"). - An ESM import of an absolute Windows path needs
file:///C:/.... - Headless Chrome leaks from interrupted runs. At the end, list browsers started with
--headless=newand--remote-debugging-port=0whose parent is gone, and kill the root with its tree. - Render speed: flat UI ≈ 4 min per 35 s at 1080×1920. A 3D world ≈ 4-5 min at ~4 fps, but only if the 3D layer count stays small.
Files
scripts/:preflight.py: dependencies and ElevenLabs notice;new_film.py: scaffold a project;fetch_fonts.py: local woff2;words.py: word times;beats.py: beat grid and drop;synth_bed.py: no-key music;mix_events.py: events-driven mix;sheet.py: contact sheet;probe.mjs: live geometry.
templates/one-take/: the full one-take 3D camera film as a runnable demo (fictional product, Arabic RTL copy), with STORYBOARD.md and ledger.json.references/:research.md: landscape survey, upstream skills, security notes, platform specs;lessons.md,runs.md: the self-learning memory;audio-providers.md,one-take-kit.md,montage-kit.md,qa-gates.md.
patches/seam-gate-camera-overlap.patch: opt-in seam-gate rule for one-take camera films.