How to use
- Copy the installation command and review the source package before installing it in your own agent environment.
- Check the source document for required tools, credentials and supported hosts.
- Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/whaleyxbt/claude-motion --skill sound-designBefore you start
- Installation does not include a model subscription, API allowance or permission to access your files.
- Repository stars and installation counts describe activity, not an independent quality or security test.
Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
Sound design
Most AI-made motion graphics are silent. Sound is what makes a piece feel produced.
The sfx/ engine synthesizes every sound in pure Python stdlib: no samples, no licenses, no installs, same output on every run. It reads the same timeline JSON as the picture, so sound can't drift out of sync.
How it fits together
| File | Role |
|---|---|
sfx/synth.py | generators: each returns mono samples at 48 kHz |
sfx/mix.py | Mix: place sounds in time, master to a loudness target, write WAV |
sfx/loudness.py | BS.1770 integrated loudness (matches ffmpeg's ebur128) |
sfx/cues/<video>.py | a cue sheet: one video's sound design |
python3 -m sfx --list # generators with what each is for
python3 -m sfx sfx/cues/<video>.py # render → mastered WAVWriting a cue sheet for a new video
Create sfx/cues/<video>.py:
from sfx.synth import click, pop, whoosh, chime, riser, impact
TIMELINE = "timeline.json" # the same file the composition reads
OUT = "public/sfx.wav"
TARGET_LUFS = -14.0 # optional, this is the default
def score(mix, tl):
s1 = tl["s1"]
mix.put(s1["title"], whoosh(0.4, 500, 2400, peak=0.45), 0.12)
for i, t in enumerate(s1["items"]):
mix.put(t, pop(900 + i * 150, 400), 0.3, pan=-0.4 + i * 0.3)mix.put(t, samples, gain, pan):talways comes fromtl, never a typed number. Offsets from a beat (s1["exit"] + 0.05) are fine.mix.typing(start, end, chars): one humanized keystroke per character.mix.put_stereo(t, left, right, gain): for stereo sources likepad().- Mastering is automatic: the engine soft-clips and sets gain to hit
TARGET_LUFS, raising drive only as far as needed to keep the true peak ≤ −2 dBFS (room for AAC overshoot, so the MP4 stays under −1 dBTP). A mix that's mostly short hits with silence between can't reach −14 without crushing; the engine then comes out quieter and says so. Fix it with sustained sound (a pad, longer tails), not by forcing it. - Add an
"sfx"script inpackage.jsonpointing at the new cue sheet, or run it directly.
sfx/cues/effort.py is a complete, real example: 128 sounds plus a pad bed.
Visual event → sound
| On screen | Generator | Notes |
|---|---|---|
| Typing | mix.typing() | jitter ±4–6 ms, random gain |
| Word / line reveal | whoosh() + blip() on the landing | short, airy |
| Badge, chip, callout pops in | pop() | raise pitch across a sequence |
| Control snaps to a step | click() + blip() in key | |
| Sketch / draw-on | scratch() | |
| Build-up | riser() | end exactly on the hit; one big riser per video |
| Success / arrival | chime() | chord in key |
| Hero transition | whoosh() in, impact() on the hit | one impact per video |
| Under everything | pad() with an envelope() | duck it under the hero transition |
Accent, don't narrate: not every motion needs a sound. Continuous motion (drift, grain, rotation) stays silent.
Levels and space
- Hero hits: gain 0.25–0.35. Supporting sounds: 0.08–0.2. Pad: ~0.03, ducked ~65% under the hero transition.
- Pan follows on-screen position (−1 left … +1 right). Sequences sweep across the stereo field.
- Pick one key and keep every tonal sound in it; pitch rises with progress and resolves on the payoff. (In the effort example the pad and slider notes are D major, but the scene 3 pops and chime are still in C major. Don't copy that.)
Adding a generator
Write a function in sfx/synth.py that returns a list of floats at SR. Put what it's for in the first docstring line; --list shows it. Use the module-level random for noise so renders stay reproducible. Build on bandnoise() for anything airy.
Verify
npm run sfx # or python3 -m sfx sfx/cues/<video>.py
npm run wave # out/wave.png + loudness of the WAV
npm run build
node scripts/wave.mjs out/effort.mp4 # loudness of what you'll actually postIn out/wave.png every coral line is a timeline beat. Each transient should sit on a line or a hair after it. A hit between lines means a hardcoded time or a stale WAV. Pass --timeline for a video with its own timeline file.
Optional: generated audio
For voiceover or sounds you can't synthesize, the ElevenLabs MCP (see .mcp.json.example) can generate files into public/. Place them with <Audio src={staticFile(...)} /> inside a <Sequence from={beat * FPS}>.




