agentleFS
eisbaweisbaw/mped-skills/skills/explainer-video/SKILL.md

explainer-video

Skill11 starsChanged 3 months agoMITView on GitHub

Produce a narrated, animated explainer video (YouTube-quality, 1080p) from a topic, the HTML way — manuscript → local TTS speech → seekable-HTML graphics → combined mp4. Use when asked to "make an explainer/demo video", "animate this concept", "turn X into a narrated video", or to add a scene to this repo. Reproducible on x86_64-linux (Nix flake + virtual-clock frame capture, no cloud/API keys).

19 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

5 rules. 0 are common, 1 is distinctive, and 4 are very specific to this project.

  • 0

    Common

    Most of the 17 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every scene page is a pure function of ?t=<ms> — it reads window.T (milliseconds, set by shared/clock.js from the URL) and renders the …

    line 89
  2. Distinctive:

    Keep the light default unless the user specified dark, then provision the toolchain.

    line 120
  3. Very specific:

    Keep the tested af_heart voice unless choosing another intentionally.

    line 133
  4. Very specific:

    Use the theme variables (--accent, --accent2, --panel, --ink), the .term/.codefile/.chip components, and <svg class="wires"> with <path …

    line 182
  5. Very specific:

    Read the PNGs. Fix overlaps, wire anchor positions, off-screen elements, mistimed beats.

    line 206

“Common” is measured: kept by at least 6 in 10 of the 17 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

explainer-video — narrated animated explainers, the HTML way

Turns a topic into a finished narrated video (1920×1080 by default) by rendering HTML/CSS/JS pages as deterministic frames at the manuscript resolution and muxing them with a local TTS voice track. (Manim and animated-SVG/marp are alternative ways to render the same manuscript; this skill is the HTML one — the most polished — and ships a self-contained HTML-only scaffold.)

Everything runs inside a pinned Nix flake on x86_64-linux: no screen-recording, wall-clock animation, cloud APIs, or API keys. Work inside nix develop path:. so a newly copied, untracked flake is addressed explicitly.

The pipeline ships inside this skill. bootstrap.sh and the scaffold/ directory beside this SKILL.md is a complete, self-contained project — Nix flake (+ flake.lock), Justfile, the shared infra (shared/clock.js, brand.css, tts.sh (Kokoro), kokoro_synth.py, tts_manifest.py, capture-html.sh, assemble.sh, assembly_fingerprint.py, audio_pcm_sha256.sh, copy_clean.sh, manuscript.py, narration_duration.py, scene_selection.py, scene_media.py, scene_len.sh, check.sh, audition.sh, review.sh, validate.sh, list_voices.py), the HTML track (tracks/html/render.sh, capture-cdp.mjs, verify.sh, verify-seek.sh, scenes/common.js+common.css), tracks/html/templates/scene.html, a template script/script.json, and one example scene. All paths below are relative to a project of that shape.

To start a new video, bootstrap a clean scaffold out of this skill:

bash "$(dirname "$(realpath SKILL.md)")/bootstrap.sh" ~/my-video # from the skill dir
cd ~/my-video && nix develop path:. # chromium, ffmpeg, node, Kokoro deps
just fetch-kokoro                  # one-time: pinned Kokoro-82M model + venv (offline after)

Then replace script/script.json (the manuscript) and add scene pages under tracks/html/scenes/ (copy tracks/html/templates/scene.html). Everything is reproducible from the clean scaffold alone — the skill depends on nothing outside itself. The bootstrap deliberately excludes live generated state. (out/, assets/kokoro/, .venv-kokoro/, node_modules/ are gitignored and regenerated by just fetch-kokoro / the render.)

The four stages (and where each lives)

StageWhatFiles
1. Manuscriptthe canonical scene list: id, title, seconds, narration, visualscript/script.json
2. SpeechWAV + duration + provenance receipt per scene, via local TTSshared/tts.sh (Kokoro-82M) → out/audio/
3. Graphicsone seekable HTML page per scene, animated on a virtual clocktracks/html/scenes/<id>.html
4. Combineframe-capture each page → mp4, then mux with narrationtracks/html/render.sh, shared/assemble.sh → out/html.mp4

The single source of truth is script/script.json: it owns voice, FPS, resolution, scene order, and speech. Receipts bind each WAV to those synthesis inputs, so changed text or a replaced WAV fails closed until narration is rerun. Assembly embeds a canonical input fingerprint and the decoded final-audio hash in the MP4; changed scene videos, narration artifacts, order, media settings, assembly settings, or played narration make validation fail until rebuilding.

Themes

The palette + fonts are a swappable theme. Use the light default unless the user requests another look. Bundled themes in themes/<name>/:

themelookfonts
lightwarm cream/paper, blue accent (default; terminals stay dark)JetBrains Mono + Inter
darkmuted neutral dark, lifted blue accentJetBrains Mono + Inter

Activate one with just theme <name> — it copies themes/<name>/vars.css to shared/theme.css, which shared/brand.css @imports, so every scene re-skins with no page edits. All theme fonts are nix-provided (pinned). just themes lists them; add a theme by dropping a new themes/<name>/vars.css (same variable set). Only the palette/fonts change — scene layout is theme-agnostic.

TTS engine. Stage 2 is Kokoro-82M (shared/tts.sh, provisioned by just fetch-kokoro) — fully local, offline, 24 kHz, deterministic (CPU inference, fixed speed/lang, hash-checked model). The voice is .meta.voice. It emits out/audio/<id>.wav, .dur, and .tts.json; downstream work verifies the receipt before using an existing narration artifact.

The one non-negotiable rule: the virtual-clock contract

Every scene page is a pure function of ?t=<ms> — it reads window.T (milliseconds, set by shared/clock.js from the URL) and renders the frame for that instant. Never use Date.now(), setInterval, or requestAnimationFrame for animation state. Break this and the frame grabber desyncs and the render stops being reproducible.

shared/clock.js gives you: window.T, seg(t0,t1) (eased 0→1 progress of a time window), fadeWin(t0,t1), lerp, easeInOut, clamp. tracks/html/scenes/common.js builds the animation vocabulary on top:

  • enter(el,t0) / pop(el,t0) — fade+rise / scale-pop entrance
  • fadeIn / fadeOut / showWin(el,t0,t1) — visible only inside a window
  • pulse(el,t0) — one-shot glow ring
  • draw(pathEl,t0,dur) — SVG stroke draw-on (stroke-dashoffset)
  • typeCmd(el, cmd, t0) — deterministic typewriter (chars = f(T)); cmd is a string or [{t:"text", c:"cssClass"}] spans for syntax colour; returns the ms it finishes typing so you can chain the next beat
  • revealLines(el,t0) — stagger-reveal child lines (terminal output)
  • caretOn() — 2 Hz blink from T only

A scene constructs or queries its DOM once, then exposes window.render(). Put every T-dependent mutation inside that function and set complete state on each call; do not append, latch classes, or freeze const T outside it. The capturer can then seek forward, backward, or repeatedly without carrying state. See tracks/html/templates/scene.html for the copy-paste skeleton, and run just verify-html <id> after changing a scene.

Procedure

0. Preflight and optional theme

Keep the light default unless the user specified dark, then provision the toolchain.

nix develop path:. --command just themes       # light, dark
nix develop path:. --command just theme dark   # only when requested
nix develop path:. --command just check
nix develop path:. --command just fetch-kokoro # pinned model + locked venv

1. Write the manuscript (script/script.json)

For a new video, replace .scenes and set .meta.title, .meta.voice, .meta.fps, and .meta.resolution (positive even 16:9 dimensions). Keep the tested af_heart voice unless choosing another intentionally; run nix develop path:. --command just voices to list every valid pinned voice ID. Each scene:

{ "id": "s1_slug", "title": "On-screen title", "seconds": 30,
  "narration": "What the voice says. Natural, tight, ~2.5 words/sec.",
  "tts_text": "Optional speech-only spelling for pronunciation.",
  "visual": "What animates, beat by beat — this is your storyboard." }

Rules: ids are s<n>_slug; narration is commonly ~15–25s per scene (Kokoro reads ~2.5 w/s); optional tts_text overrides only spoken text while narration remains reader-facing. The visual field is the stage-3 brief. Review and freeze the manuscript before rendering. Commit only when the user or repository workflow authorizes it.

2. Render speech (drives every scene's length)

nix develop path:. --command just audio              # all scenes
nix develop path:. --command just audio s1_slug      # selected scenes

This writes out/audio/<id>.wav, <id>.dur, and <id>.tts.json. From now on shared/scene_len.sh <id> returns the exact clip length (narration + 0.8s) — render each scene page to that length, so visuals match speech with no dead air or clipped narration. If text, voice, settings, or WAV content changes, scene rendering and assembly reject the stale receipt and require fresh audio.

Now listen to the exact synthesized clips while reading both the reader-facing narration and the effective spoken text:

nix develop path:. --command just audition             # all scenes, manuscript order
nix develop path:. --command just audition s1_slug     # selected scenes, CLI order

Pause here for explicit human approval of pronunciation and pacing. A successful player exit is not approval. If a word sounds wrong, edit tts_text, rerun just audio <id>, and audition it again. The command writes no approval file and refuses to play anything if any selected WAV or receipt is stale.

3. Build the graphics (one page per scene)

Copy tracks/html/templates/scene.html to tracks/html/scenes/<id>.html and realize that scene's visual. The skeleton is fixed:

  • First two <head> tags must be shared/brand.css then shared/clock.js (relative ../../../shared/… — the pages live two dirs deep). Then common.css + common.js.
  • <body><div class="stage"> is a 1920×1080 design canvas that scales uniformly to .meta.resolution. Place elements with left/top px; start animated ones with class hidden0 (opacity 0) and reveal them from the script.
  • Use the theme variables (--accent, --accent2, --panel, --ink), the .term/.codefile/.chip components, and <svg class="wires"> with <path class="wire"> for connectors you draw() on.
  • The closing script assigns an idempotent window.render, then calls it once for browser preview. Helpers take bare element IDs: window.render=()=>{ enter('hdr',300); typeCmd('cmd',CMD,900); draw('wire',1600); }; window.render();.

Verify while you build — do not wait for the full render:

# grab three representative instants of ONE scene as PNGs and LOOK at them
read width height fps < <(
  nix develop path:. --command jq -r \
    '[.meta.resolution[0], .meta.resolution[1], .meta.fps] | @tsv' \
    script/script.json
)
for t in 0 8000 16000; do
  nix develop path:. --command chromium --headless=new --no-sandbox --disable-gpu \
    --window-size="$width,$height" --screenshot=/tmp/v_$t.png \
    "file://$PWD/tracks/html/scenes/s1_slug.html?t=$t&fps=$fps&w=$width&h=$height" \
    2>/dev/null
done
nix develop path:. --command just check              # manuscript + scene files
nix develop path:. --command just verify-html s1_slug

Read the PNGs. Fix overlaps, wire anchor positions, off-screen elements, mistimed beats. This screenshot-per-beat loop is how the good scenes were made. With no scene IDs, just verify-html checks every scene sequentially; explicit IDs preserve CLI order and all requested scenes are attempted before failure is reported.

4. Render + combine

nix develop path:. --command just html                # every scene
nix develop path:. --command just html s1_slug        # selected scene iteration
nix develop path:. --command just review              # actual rendered clips, all scenes
nix develop path:. --command just review s1_slug      # replace review set with this subset
nix develop path:. --command just assemble
nix develop path:. --command just validate
#   -> out/html.mp4  (muxed with narration; audio padded to video length)

render.sh uses tracks/html/capture-cdp.mjs — one persistent headless chromium per scene driven over the DevTools protocol (load once, call window.hfSeek, wait for fonts.ready + double-rAF, then capture). This is much cheaper than launching a chromium per frame (shared/capture-html.sh), which matters under load. Each scene's H.264 stream is copied without another lossy encode; only padded AAC narration is encoded, with 2 dB of headroom. Assembly fully decodes a temporary, applies MP4 fast-start layout, then atomically publishes it. Embedded identities bind the finished cut to the ordered scene MP4s, verified narration inputs, manuscript media settings, assembly contract, and the exact decoded PCM that the final audio stream plays.

just review samples six exact decoded frame indices per selected scene, including the first and last frames. It writes per-scene sheets, out/review/contact-sheet.png, and out/review/layout.json; the manifest binds the displayed frame indices and sheets to source MP4 hashes. Look at every sheet. Successful generation proves only that frames were extracted. It does not prove visual clarity, narrative correctness, synchronization, or that a human reviewed them. Uniform samples can also miss short-lived defects, so play the complete final cut before delivery.

5. Verify the final cut

nix develop path:. --command just validate

The validator checks the embedded assembly fingerprint and final decoded-audio hash, fresh narration receipts and durations, exactly one H.264/yuv420p stream plus one AAC stream, resolution/FPS, total duration, audio headroom, full error-sensitive decode, MP4 fast-start, and exact ordered decoded-frame parity with the source scene clips. It is a technical gate, not a semantic one: watch the complete video and obtain the user's approval. Commit sources only when authorized (out/ is gitignored).

nix develop path:. --command ffplay -autoexit out/html.mp4

One freshness limit is explicit: scene MP4s do not yet have render receipts for arbitrary browser dependencies. The validator can prove which scene MP4s were assembled, but cannot prove they postdate changes to HTML, CSS, the theme, or local assets. After any visual-source change, rerun just html <ids> immediately before just review <ids> and assembly.

Gotchas (learned the hard way — heed them)

  • One chromium per frame melts under parallel load. With multiple renders going the box hit load ~77 and throughput collapsed to ~7s/frame. The CDP capturer fixes it; still, render one video at a time for best throughput.
  • Flush before capture. Screenshot only after document.fonts.ready and a double requestAnimationFrame — otherwise text/layout isn't painted yet. (Already handled in capture-cdp.mjs; replicate if you write a new capturer.)
  • white-space: pre can double-render whitespace runs — prefer explicit spans/margins for terminal alignment.
  • Absolute positioning drifts — a wire drawn to M520 528 must match the element's real px box; when you move a panel, move its wire anchors. The screenshot loop catches this immediately.
  • Narration ≠ your seconds estimate. Kokoro is ~2.5 w/s; always size clips from shared/scene_len.sh (real .dur), not the script's seconds guess.

Adapting to a brand-new topic

  1. Rewrite script/script.json .scenes + .meta.title for the topic.
  2. Optionally add or activate a palette under themes/; do not duplicate theme data in the manuscript.
  3. One scene page per scene; reuse common.js verbatim — its vocabulary is topic-agnostic (terminals, chips, wires, cards, reveals).
  4. Stages 2/4/5 are unchanged — they read the script.

The infrastructure is generic; only script/script.json and the per-scene HTML are topic-specific.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.