Under the hood
Turns a topic (or the user's code) into a narrated, animated explainer. The look is warm and hand-drawn:
- a cream dot-grid "paper" background with ink line art
- blue for requests, orange for responses, green for encrypted or success
- a step tracker at the top and big captions at the bottom
- packets that fly between boxes, and camera pans between "worlds"
The reference result is ${CLAUDE_SKILL_DIR}/assets/example-cookie/, which shows what happens when you type "cookie" into Google. Open its scenes.js whenever you need a pattern for something complex.
Everything is code driven. A canvas page renders frames deterministically, narration clips set each scene's length, and the music and sound effects are synthesized from the same timeline. A script change re-syncs everything, and a 60-second video renders in about 30 seconds.
Topic requested: $ARGUMENTS
Where things live
- Scripts:
${CLAUDE_SKILL_DIR}/scripts/(call itSCRIPTS). - Runtime:
UTH_HOME(default~/.local/share/under-the-hood) holds the node module, the TTS venv and saved voices.doctor.shprints the exact paths, includingPY(the TTS Python). - Projects:
./explainers/<slug>/in the current working directory, unless the user names another place. If the working directory is the home folder, ask where to put it instead of cluttering~.
A project folder looks like this:
index.html engine.js scenes.js script.json timeline.js
narration/ (voice clips: <id>.wav, <id>_take2.wav, plus preview files)
audio/ (stems and mix.wav)
.shots/ (screenshots, one new folder per run)
<slug>.mp4
Workflow
0. Check the setup (one line when ready)
bash ${CLAUDE_SKILL_DIR}/scripts/doctor.sh # add --no-tts if the video has no narration
If it prints READY, note PY and VOICES and move on. If it prints MISSING lines, load the under-the-hood-setup skill and follow it, then run the doctor again.
1. Short intake
If $ARGUMENTS is empty, ask for the topic. Then ask only what you can't reasonably decide, in one message, with sensible defaults:
-
Angle, only when the topic is broad. "How the internet works" needs a focus, "what happens when you git push" doesn't.
-
Voice: always offer this, because users don't know their own voice is an option. Present it like:
- built-in voice (default, works right away)
- a saved voice, if
VOICESlists one - your own voice: record about 20 to 30 seconds on your phone and send the file. Your voice stays on your machine.
- no narration (music and sound effects only)
If they pick their own voice, read
${CLAUDE_SKILL_DIR}/references/voice.mdand give them its recording tips and sample paragraph before anything else. They can record while you write the storyboard. -
Length: default 30 to 60 seconds, 16:9.
Don't turn this into a questionnaire. Two or three short questions at most.
2. Understand the topic and get the facts right
If the user gave code, read it and trace the real flow. Tech viewers will call out mistakes. So:
- Get the order of events right. Example: on a page already loaded, DNS and the TLS handshake already happened, so place them on first contact or label them "first visit".
- Prefer what modern systems actually do. Example: Chrome talks to Google over QUIC and TLS 1.3, not a textbook TCP three-way handshake.
- Say "simplified" in a subcaption when you simplify.
- Use illustrative values (example IPs,
.exampledomains, made-up scores) and avoid fake precision like exact millisecond counts. - Don't recreate logos or pixel-exact UIs of real products. Line-art stand-ins fit the style better anyway.
3. Storyboard and script, then get approval before building
Plan 4 to 8 numbered steps, roughly 30 to 60 seconds, plus a title scene first and an end scene last. For each scene write:
- Narration (
script.json):{"id", "text", "gap"}.gapis the pause after the line, 0.25 to 0.45 seconds. - Caption (
cap): 8 words or fewer, readable when muted. Subcaption (sub): one short line. - Visual beat: what appears and moves, and what each sound is.
Read ${CLAUDE_SKILL_DIR}/references/narration.md before writing narration. Article-style narration sounds like AI reading an article. Write the way a person explains to a friend.
Show the user the script and storyboard as a short table and wait for a go-ahead. Changing words now is free. After voice generation it costs a re-run.
No em dashes and no semicolons in narration or on-screen text. The TTS reads them oddly, and short sentences sound more natural anyway.
4. Scaffold
bash SCRIPTS/new_project.sh ./explainers/<slug> "<Page title>"
This copies engine.js, a starter scenes.js (a small working "hit send" example) and script.json. It refuses to overwrite a non-empty folder. Replace the starter content with the real storyboard.
5. Build scenes.js
Read ${CLAUDE_SKILL_DIR}/references/engine-api.md first. It covers the time model, every helper, the layout zones, the sound names and the boot config. Key ideas:
defineScenes([...])with design durations. Animate in design time withprog(t, S.id + a, S.id + b). The engine stretches each scene to fit its voice clip, so never hand-tune to clip lengths.- Keep the world inside y 150 to 800. The tracker sits above that and the captions below.
- Things that should persist (a browser, a server) appear in one scene and stay drawn after it.
- For a second "place", like inside a data center, draw at x + 1920 and pan with
camera(t). Start revealing its contents during the pan so the screen is never empty mid-move. - Every visible event gets a sound in
sfx(add). Keys getkey, flying packets getwhoosh*, arrivals getthunkorpop, success getschimeorsuccess. This is a big part of why it feels finished.
6. Preview without voice, check frames
node SCRIPTS/shots.js <project> # middle of every scene, saved to a fresh .shots/<time>/ folder
Look at every screenshot. Downscale before reading (sips -Z 960 on macOS, ffmpeg -i in.png -vf scale=960:-1 out.png elsewhere). Check for:
- text that overlaps or runs off the edges
- empty frames during pans
- elements drawn on top of things they should sit behind
Fix and re-shoot. If the page fails to boot, the script prints the page errors.
7. Voice
Built-in voice (no --voice):
$PY SCRIPTS/narrate.py --script <project>/script.json --out <project>/narration --takes 2
A saved voice: add --voice <UTH_HOME>/voices/<name>.wav.
A new voice from a recording: follow ${CLAUDE_SKILL_DIR}/references/voice.md. It covers prep_voice.sh, a quick test line, and consent. Only clone the user's own voice, or a voice they confirm they have consent to use.
Then have the user listen: <project>/narration/narration-preview_take1.wav (use open on macOS, xdg-open on Linux).
- The first run downloads about 1 GB of model weights. After that it runs around realtime on Apple Silicon or a GPU, slower on CPU.
- You cannot hear audio. Tell the user that, and ask them to judge the takes by ear. They can mix takes line by line. Redo a single line with
--only <id>. - Turbo supports tags like
[chuckle],[laugh],[sigh],[gasp]and[surprised]. Use at most one or two per video.
No narration: skip this step and build_timeline.py. Scenes keep their design durations, and the video gets music and sound effects only.
8. Timeline, render, verify
python3 SCRIPTS/build_timeline.py <project> [--pick dns=2,enter=2]
node SCRIPTS/render.js <project> [--music-db -12] [--sfx-db 1] [--silent] [--audio-only]
render.js builds the voice, music and sfx stems and mixes them, ducking the music under the voice and normalizing to -16 LUFS. It renders to <slug>.partial.mp4 and only renames that to <slug>.mp4 after ffprobe confirms the full length.
Before telling the user it's done, verify the actual file:
- ffprobe shows the expected duration plus video and audio streams
ffmpeg -ss <late time> -frames:v 1shows the right scene- the loudness is about -16 LUFS
Then open the MP4 and the index.html preview. The preview plays the mix in sync, with chapters and scrubbing.
Report plainly what was verified, what you couldn't check (how the audio sounds), and which parts are simplified.
Working rules
- No glob deletes. Don't run
rmwith wildcards orrm -rfon project folders. New outputs go to fresh folders, and files get overwritten by name. - A stopped command may have partly run. If the user stops a multi-step command, check what changed on disk before going on, and tell them about any partial effects. A half-swapped set of clips can leave a short partial video that looks finished.
- Don't auto-switch on preference hints. If the user says "I like take 2" while still deciding, confirm before re-rendering.
- Offer, don't build: a 9:16 vertical version, trimming to 30 seconds, or more polish. Mention them in one line each.
Troubleshooting
'NoneType' object is not callableatPerthImplicitWatermarker: runuv pip install --python $PY "setuptools<81". Chatterbox's watermark library needspkg_resources.- The first TTS run is slow: that's warmup and the model download. Later runs are faster.
- Music too loud or quiet:
--music-db(default -12). Effects:--sfx-db(default 1). Use--audio-onlyto iterate on the mix quickly. - Chrome not found: set
CHROME_PATH, or load theunder-the-hood-setupskill. - Chrome's offline audio is not bit-identical from run to run, so stems differ slightly between renders. Timing is exact and frames are pixel-identical.