EffectCraft motion kit
A tested workflow for making 60-second corporate motion graphics in EffectCraft that look designed, are in sync, and are free of the classic bugs: empty frames between scenes, overlapping voice lines, text covered by decorations, numbers that vanish, and SFX off the beat.
Design comes from looks and layouts, not from scratch. Pick one of five tested looks, write each
scene as one scene() call with your content and word timings, and the layout handles size, grid,
colour and motion. Agents that invent a style end up with dark gradients, glow blobs and small
centred text; that is the single biggest reason a generated promo looks cheap.
Any agent can use this. Everything runs from a shell (effectcraft-cli through scripts/ec.py).
MCP is optional. Kit location: the folder containing this file (call it $KIT).
Not this kit: stick-figure / character story videos (split-screen "wrong way vs right way") use the
stick-figure-story skill instead.
Golden rules (the reasons are in references/)
- Times come from data, not guesses. The scene plan lives in
timeline.json. Scene scripts useSC("s3")andW("s3", "word")(the absolute time a word is spoken). Never type a time like23.6. - Look at every scene you build. Render preview frames (
ec.py frames) and open the PNGs. Code that runs without errors can still render wrong. - "Done" means the gates passed.
timeline.py checkhas 0 FAIL,ec.py qahas 0 FAIL,ec.py qa-videohas 0 FAIL, and you have looked at the contact sheet. Report the results honestly, warnings included. - Use a look and the layouts (
references/looks.md). Compare two looks before choosing. At least a third of the scenes must be visual (phone / icons / flow / bars, or a custom scene with an object). Custom scene code is for what no layout covers (a map, a product mock-up), and it still uses the look's paletteP(S.bg)and type sizesLOOK.type. - Read
references/pitfalls.mdbefore writing any EffectCraft script. Readreferences/motion-design.md,references/looks.mdandreferences/techniques.md(beat grid, peak-aligned SFX, continuous morphs, ban list) before designing scenes. - The brand is not yours to edit (
references/brand.md). Show the real logo file, never the name typed as a wordmark. Write the tagline character for character. Invent no product names, button labels or stats. Fill in the"brand"block intimeline.jsonsoec.py qachecks this. - Passing QA is not the same as good design. Plan the colour script, the visuals and the
signature moment before writing scenes, and score the film in a creative-director review before
rendering (
references/design-review.md, compared withreferences/benchmark_sheet.png). 7b. Real product UI (SaaS, app films): followreferences/ui-footage.md: capture sample data only, crop to one panel, show it readable (55%+ width) or rebuild it as animated UI, one screen per shot, no tinted walls of thumbnails, no invented numbers, and build here so the QA gates run. - High-energy / hyper-kinetic ads (TikTok-style, a slam on every spoken word): follow
references/high-energy.mdandtemplates/hyper.jsxinstead of the calm layouts. - Report what the render shows, not what you planned. Before writing the report or handoff, pull frames
from the final MP4 (not the preview) at each scene's middle and last half-second, and check every claim:
is each line on screen long enough to read, is every drawn step really visible, is the screenshot legible,
does the length match the brief and the file name? Read every voice transcript against the script, and
listen to the brand name. In testing, an agent's handoff described four diagram steps, three promises and
"BridgeAxis Technologies"; the film showed the last step 0.3 s before the cut, the promises for half a
second, unreadable screenshots, and a voice saying "Bridge Access".
timeline.py checkandec.py qanow warn about unread voice mistakes and text readable for undermin_readseconds: fix those warnings, don't explain them away. Run on the highest reasoning setting your agent has: the sloppy run was on a "Low" one.
Project layout
<project>/
timeline.json scene plan + sfx cues + music (start from $KIT/templates/timeline.example.json)
audio_meta.json voice files + word timestamps (from the audio step)
soundtrack.wav generated by timeline.py mix
assets/voice|sfx|bgm|icons|fonts
scripts/lib.jsx copy of $KIT/templates/lib.jsx (set COMP; the look supplies colours and fonts)
scripts/look.jsx written by `ec.py look NAME --accent HEX`
scripts/timing.jsx generated (timeline.py emit) - never edit
scripts/wipes.jsx generated (timeline.py emit) - run LAST
scripts/reset.jsx, s1.jsx ... s8.jsx
qa/ previews and contact sheets
Run every command from <project>/. ec.py reads the project path and comp name from timeline.json.
Workflow A: new video
0. Brief. Ask before building: brand/website to match, mood (loud / calm / technical / luxurious), length and aspect ratio, voice (gender/tone), and whether music/SFX are wanted. Guessing the creative direction is the most common reason a first cut gets rejected.
1. Setup
mkdir -p <project>/{scripts,assets/{voice,sfx,bgm,icons},qa} && cd <project>
cp $KIT/templates/lib.jsx $KIT/templates/reset.jsx $KIT/templates/s1.jsx scripts/
cp $KIT/templates/timeline.example.json timeline.json # edit project/comp/output names
python3 $KIT/scripts/ec.py fonts # installs the looks' fonts (once per machine)
Set COMP in scripts/lib.jsx to the timeline's comp name. Note the brand's accent colour (logo,
website CSS, product). If the brand has a website, read its CSS for the accent and fonts.
Put the logo files in assets/logo/ and fill in the "brand" block in timeline.json: name,
exact tagline, logo files, and any approved product names or URLs (references/brand.md).
2. Script and voice. Write 8 lines (hook, problem, brand, 2–3 benefits, proof, end card); 60 s
holds about 110–130 spoken words. Generate one audio file per line and get word timestamps into
audio_meta.json in this shape:
{"voices": [{"id": "s1", "path": "assets/voice/s1.wav", "duration_s": 6.45,
"words": [{"word": "Every", "start": 0.30, "end": 0.59}, ...]}]}
First run python3 $KIT/scripts/ec.py doctor. It lists what this machine has and how to install
anything missing. When it says ready, don't research or install alternatives (edge-tts, Whisper,
piper, Playwright): the tools below are the tested ones. With no voice at all, time scenes with
"in+0.5" offsets instead of words; $KIT/demo/ is a complete silent example (ec.py demo renders it).
- Voice (the default): local Kokoro, female voice
af_heart. Free, no account. The model is cached in~/.cache/hyperframes/tts/(downloaded once on the first run):Withoutexport HYPERFRAMES_PYTHON=~/.venvs/kokoro/bin/python # REQUIRED: the venv with kokoro-onnx npx hyperframes tts "Line one text." -v af_heart -o assets/voice/s1.wav # one file per lineHYPERFRAMES_PYTHONit fails with "The kokoro-onnx package is not installed". Don't pip-install anything: set the variable. - Word timestamps:
npx hyperframes transcribe assets/voice/s1.wav --json. It uses the Parakeet model, cached in~/.cache/hyperframes/parakeetafter the first run (engineautopicks it). Parakeet is right for clean TTS voices like Kokoro. Don't pass-e whisperhere: hyperframes' Whisper engine (whisper.cpp) is not set up. For real recorded voices with accents, the footage-edit skill uses faster-whisper large-v3-turbo instead (~/.venvs/asr). Copy the words and their start and end times intoaudio_meta.json. - Music and SFX: the media-use audio engine (
node <skills>/media-use/audio/scripts/audio.mjs --request audio_request.json --out audio_meta.json --only bgm,sfx). It retrieves from the HeyGen catalog by description, for example"bgm": {"mode": "retrieve", "query": "warm calm piano"}.<skills>is~/.claude/skillsfor Claude Code,~/.agents/skillsfor Codex/OpenCode/DeepSeek. - HeyGen text-to-speech (through the engine or the
heygenCLI) is the alternative voice, but the free allowance runs out. When it does, use Kokoro. Music and SFX retrieval also need theheygenCLI signed in (heygen auth login --oauth) unlessHEYGEN_API_BASEis set; without it,sfxstill answers from media-use's bundled library, and the user can supply their own music file.
Write down which provider and voice you actually used.
3. Timeline
python3 $KIT/scripts/timeline.py beats # music tempo, beats, bar starts, drop -> beats.json (needs "music")
python3 $KIT/scripts/timeline.py suggest # proposes scene in/out + voice_at; with beats.json, scenes end on beats
# paste into timeline.json, set each scene's "bg" (light/dark/accent), add sfx cues anchored to words
python3 $KIT/scripts/timeline.py words # every word with its absolute time
python3 $KIT/scripts/timeline.py check # MUST be 0 FAIL
python3 $KIT/scripts/timeline.py emit # writes scripts/timing.jsx + scripts/wipes.jsx
python3 $KIT/scripts/timeline.py mix # writes soundtrack.wav (-14 LUFS)
Cut on the beat: timeline.py snap moves existing cuts onto beats where the voice still fits; in scenes,
BEAT(n), DOWNBEAT(n), nearBeat(t) and T.drop time slams and pops to the music (references/techniques.md).
SFX "at" can be a number, "beat:12", "downbeat:3", "drop", "s2:late" (the word "late" in s2's voice; "s2:the#2" = 2nd "the"),
"wipe:s3" (the transition into s3), "in:s3" or "out:s3". Add "offset", "vol", "max"
(trim length) and "align": "peak" (the sound's loudest moment lands on the anchor: use it for impacts and pops).
4. Plan, then write the scenes. First write the plan in qa/design_review.md
(references/design-review.md, part 1-6): the colour script with one line per scene, the visual
that shows what each line says, each scene's hero, and the one signature moment.
Give each scene a background in timeline.json ("bg": "light" | "dark" | "accent") and re-run
timeline.py emit. Alternate them so each cut reads as a new chapter. Use "accent" for the 1-2
loudest moments, usually the emotional peak. Put the brand reveal and end card on the background
where the real logo reads best (usually light), not on the accent. Then write scripts/s1.jsx ... sN.jsx,
each one scene() call with a layout (specs: top of templates/layouts.jsx; guide: references/looks.md):
// scripts/s2.jsx
scene("s2", "list", { label: "The problem",
items: [["Late arrivals.", "late"], ["Customs surprises.", "customs"], ["Tracking that tells you *nothing.*", "tracking", "nothing"]],
punch: ["Too late.", "too"] });
*word* marks the accent part of a line. Times are words from timeline.py words ("word",
"word#2", "word+0.3", "in+0.2"), written without punctuation. Layouts grow or shrink text to
fill the frame, so write the copy you want and let the layout size it. Keep on-screen lines short:
they are headlines, not the voiceover. Only for something no layout covers (a map, a product
mock-up), copy $KIT/templates/scene_template.jsx instead.
Make at least a third of the scenes visual. A film that is only words on backgrounds reads as a
slide deck, however good the type. Visual layouts put an object on screen that the voice is about:
phone (the product on a phone screen: a payment landing, an order, a tracking update), icons
(2-4 drawn pictograms with labels), flow (stops joined by a line that fills as each is spoken:
customer → phone → bank, warehouse → port → door) and bars (2-4 bars that grow: fees, waiting
times, before/after). Pick the scenes where the voice describes a thing or a process, not a claim.
scene("s4", "flow", { label: "How it works", title: ["Banked the *same day.*", "money", "same"],
nodes: [{ icon: "shop", label: "You sell", at: "sell" }, { icon: "phone", label: "Tapnest", at: "app" },
{ icon: "bank", label: "Your account", at: "account", note: "Same day" }] });
Icons are drawn by the kit (no files): phone card bank cash clock check tick shop bolt chart truck
box pin lock user people chat globe send star plane doc. A custom visual scene counts too when its
picture/object layers have "VIS" in their names (e.g. "s5 VIS map"); ec.py qa warns when fewer
than a third of the scenes are visual.
Don't copy layouts.jsx into the project: ec.py loads it from the kit, and a copy freezes the old version.
5. Pick the look (read references/looks.md first). Shortlist two looks that fit the brand in
different ways, take the accent from the brand, and compare them on YOUR scenes:
python3 $KIT/scripts/ec.py new
python3 $KIT/scripts/ec.py import soundtrack.wav assets/icons/*.png
python3 $KIT/scripts/ec.py compare editorial swiss --accent FF5B1F # -> qa/look_compare.png: OPEN it
python3 $KIT/scripts/ec.py look editorial --accent FF5B1F # apply the winner
6. Build and preview
python3 $KIT/scripts/ec.py run s1 s2 s3 s4 s5 s6 s7 s8 wipes # s1 creates the comp + soundtrack
python3 $KIT/scripts/ec.py frames 2 4 6.5 # OPEN qa/frames.png and look at it
Preview two or three moments of every scene. To fix one scene, edit it and run ec.py rebuild sN
(clears only that scene, rebuilds it, re-runs the wipes). To rebuild everything (e.g. after changing
the look): ec.py run reset s1 ... sN wipes. The look supplies the transition; to replace it, write
your own scripts/wipes.jsx without the "GENERATED" header line (emit then leaves it alone).
Custom-scene checklist (scene_template.jsx shows each pattern):
- background first; every layer pushed into
L;lifeAll(L, S.in, S.out)at the end - (custom scenes) colours from
P(S.bg), sizes fromLOOK.type - reveals timed with
W(); nothing fades out beforeS.out - children
parent()ed to their card BEFOREpopIn(card) - counters use
countUp()on ONE text layer - decorative ghost text and glows moved under the content with
placeBelow() - text sizes follow motion-design.md (labels >= 18 px, body >= 26 px)
7. QA and render
python3 $KIT/scripts/ec.py qa # fix every FAIL; fix each design WARN or say in the report why it is right
python3 $KIT/scripts/ec.py sheet # OPEN qa/sheet_1.png + qa/sheet_2.png, look at every frame
# creative-director pass: score the 8 review questions in qa/design_review.md
# (references/design-review.md); redesign any scene scoring below 4, then sheet again
python3 $KIT/scripts/ec.py render # ~5-10 min on CPU; runs qa-video automatically
QA passing means the film works, not that it is good. In testing, a generic film and a mature one
both passed QA cleanly; only the design review separates them.
Open qa/render_sheet.png. Check the brand reveal and the end card against references/brand.md:
real logo, exact tagline, nothing invented. Then report: the file path, the qa and qa-video
results, and anything you chose not to fix and why.
8. Deliver. Put every finished video (wide AND vertical) in ONE folder and give the user the
full path of each file. In testing, a user could not find a vertical cut that was rendered into its
own project folder. Write AGENT_HANDOFF.md in the project: scene table, brand choices, voice
provider and voice, music and SFX sources, and the rebuild commands, so the next agent (or you,
later) can pick up without re-researching.
Brand assets: getting a usable logo
- Take the logo from the brand's website or code (
<img>sources,public/,assets/). - If it has a baked-in white background, key it out to a transparent PNG. If it has an old tagline or other text baked in, crop it off: never show outdated copy.
- Check the wordmark against every background you plan to put it on. A navy wordmark disappears on a dark-green scene. Then either keep logo scenes on light backgrounds, or use a light version of the logo that the brand already publishes. Don't recolour the logo yourself.
assets/logomust be a real folder, not a symlink:ec.py importcan't read files through a symlinked folder.
Vertical 9:16 cut (Reels, TikTok, WhatsApp status)
Make it as a copy of the finished wide project, so the voice, timing and soundtrack are reused:
cp -r <project> <project>-vertical && cd <project>-vertical
# rename project/comp/output in timeline.json (e.g. promo-60s-vertical.mp4)
scripts/lib.jsx:W_ = 1080, H_ = 1920. Inscripts/look.jsx: side margins about 90 px and a content area about y = 440-1640 (keep clear of the app UI at the top and bottom).- Re-flow every scene: shorter lines (3-4 words), logo stacked ABOVE the text instead of beside it,
rows of icons become columns or 2×2 grids. Layouts built for landscape (e.g.
phone) may need the phone centred and scaled up, with the headline under it. - Rebuild everything:
ec.py run reset s1 ... sN wipes, then QA, sheet and render as usual. - Then copy the vertical mp4 next to the wide one (step 8).
Workflow B: fix or audit an existing project
- Copy the project before touching it, unless the user said to edit in place.
- Write a
timeline.jsonwithproject,comp,durationand thescenes, including each scene'svoiceandvoice_atcopied from wherever the project really places its voice lines (its mix script, or audio_meta.json). Without those,timeline.py checkrefuses to pass, because it would be checking nothing. Then runtimeline.py check,ec.py qa,ec.py qa-video <mp4>, andec.py sheet. Keeptimeline.jsonin sync with every re-timing you make. - Find the voice offsets (look for a mix script or audio_meta.json) and check them against the word timestamps. Look for overlapping lines, words that spill past a scene cut, and SFX more than 0.3 s away from their word.
- Fix the causes in the project's own scene scripts, rebuild the affected scenes, and re-run QA until there are 0 FAIL. If the project has its own build script, keep using it, so you don't end up with two build paths.
- Report before/after QA output and what changed. Mention anything left over (e.g. off-brand fonts). Only claim a fix that a QA command or a frame you looked at confirms. In testing, an agent without these checks reported a collision as fixed while it was still on screen.
Using MCP instead of the CLI (optional)
If the EffectCraft GUI is running with --control 9877 and you have the effectcraft-app MCP server,
run_script works the same way. Prepend lib.jsx and timing.jsx yourself, and use render_preview
for frames. Don't mix the two on one project: the GUI holds the project in memory and overwrites
CLI edits when it saves (pitfall #19).
When things go wrong
| Symptom | Likely cause | Fix |
|---|---|---|
ERR ... not found in voice line | word typo or different wording in the TTS | timeline.py words and use the exact word |
| scene builds twice / layer count jumped | re-ran after a "timeout" | ec.py rebuild sN |
| text invisible | parented after popIn, or created under its box | pitfalls #6 / #9 |
| wrong-looking font | variable font or fallback | pitfalls #16–17 |
| wipes hidden behind a scene | scene rebuilt after the wipes | ec.py run wipes (rebuild does this) |
qa dead frames at a cut | content faded out early / next scene starts late | keep content until S.out; first reveal within 0.3 s |