MohZhuang 2581e2c685 docs: 重构 README 并统一文档语言为中文
README 精简(中文 528 → 364 行,-31%;英文 552 → 390 行,-29%):
- 删除使用者不需要的深层细节,改为指向 references/ 与 SKILL.md:
  横向对比全表、八条契约表、project.json 逐字段说明、仓库目录树、B() 代码详解
- 第一定位改为「Agent Skill」,而不是「一条渲染流水线」
- 新增「安装」章:一句话安装 prompt + 九个智能体的技能目录对照表 + 环境要求
- 新增「参考思路与姊妹项目」:anything2explainer 与 html-video,并明确标注
  两者为不同作者的独立项目,本项目运行时不依赖其代码

文档语言统一为中文(README.en.md 除外):
CHANGELOG.md、CONTRIBUTING.md、THIRD_PARTY_NOTICES.md、PR/issue 模板、CI 注释。
许可证原文(MIT / Apache-2.0)与 GSAP 自带 README 属第三方逐字文本,未翻译。

跨平台可移植性修复(原先隐含 WorkBuddy 专属路径):
- peek_frame.mjs 的技能根兜底改为遍历各智能体技能目录
  (.workbuddy / .claude / .codex / .gemini / .cursor / .agents),已用隔离夹具正反验证
- setup_env.sh 标注哪几个候选是 WorkBuddy 托管路径
- SKILL.md 新增「跨 Agent 安装」章,安装路径不再写死单一平台

修正一处事实错误:THIRD_PARTY_NOTICES 原称两个方法来源项目「均为作者本人作品」,
与事实不符,已改为「他人独立项目」并补充来源链接。
2026-09-21 13:11:22 +08:00

html-explainer

An Agent Skill: give it a topic, get back a narrated explainer video with subtitles and covers.

Write scenes in HTML → deterministic frame-by-frame rendering → a real MP4. Everything runs locally; the core pipeline needs no API key and charges no per-render fee.

License: MIT Version Agent Skill Claude Code Codex WorkBuddy Python Node

简体中文 · English


What it is

html-explainer turns a topic into a narrated explainer video with burned-in subtitles and a progress bar, with the visuals written in HTML/CSS/GSAP.

It is an Agent Skill (SKILL.md + scripts/ + references/), following the Agent Skills convention, so Claude Code, OpenAI Codex, WorkBuddy, Cursor, Gemini CLI and others can load it directly. Underneath it is plain Python and Node scripts, so you can also run it by hand.

You say one sentence:

Make a 3-minute explainer video about why the sky is blue.

The skill then walks the agent through: research → script → voiceover → subtitles and beats → scene authoring in one of 23 styles → render → QC → covers.


Install

Send this to your agent:

Install this Skill into my local environment: https://github.com/OneMoh/html-explainer.git
Put it in your skills directory and check/install the required runtime
(Python 3.9+ / Node 18+ / Chrome or Edge / ffmpeg)

It clones into the right directory, runs the environment check and installs whatever is missing. Then start a new session so the agent rescans its skills directory.

Manual install

The repository root is the skill directory (SKILL.md sits at the root), so clone it straight into your skills directory — there is no subdirectory to copy:

git clone https://github.com/OneMoh/html-explainer.git ~/.workbuddy/skills/html-explainer
bash ~/.workbuddy/skills/html-explainer/setup_env.sh --install

On Windows ~ is C:\Users\<you>.

Skills directory by agent

Agent Personal (global) Project (in-repo)
WorkBuddy ~/.workbuddy/skills/ <workspace>/.workbuddy/skills/
Claude Code ~/.claude/skills/ .claude/skills/
OpenAI Codex ~/.codex/skills/ .codex/skills/ or .agents/skills/
Gemini CLI ~/.gemini/skills/ .gemini/skills/ or .agents/skills/
Cursor ~/.cursor/skills/ .cursor/skills/
GitHub Copilot / VS Code ~/.copilot/skills/ .github/skills/
OpenCode ~/.config/opencode/skills/ .opencode/skills/
Windsurf ~/.windsurf/skills/ .windsurf/skills/
Others (common convention) ~/.agents/skills/ .agents/skills/

If unsure where to put it, prefer .agents/skills/ — most tools read it. (Claude Code is the exception and only reads .claude/skills/.)

Requirements

Item Minimum Notes
Python 3.9+ edge-tts==7.2.8 (pinned deliberately — v7 changed the boundary API), numpy, pillow, imageio-ffmpeg
Node.js 18+ Used by the renderer and the cover builder
Browser Chrome or Edge Auto-detected; playwright chromium (~115MB, one time) only if neither exists
ffmpeg any version Looked up on PATH; falls back to the static binary bundled with imageio-ffmpeg
Disk ~2GB per video Frame PNGs are large; safe to delete after muxing

bash setup_env.sh only reports what is missing; --install installs it. No admin rights needed.


Quick start

A video takes seven commands, all run inside the video project directory:

PY=<path to your venv python>   # expand to an absolute path when briefing a sub-agent
SKILL=<skill install directory>

# 0 · scaffold a project (tree + config + theme + both cover templates)
"$PY" "$SKILL/scripts/new_project.py" ~/videos/sky-blue sky-blue --topic "why the sky is blue"
cd ~/videos/sky-blue
# …fill narration.json, fill project.json's order, author one frames/<id>.html per scene…

"$PY" "$SKILL/scripts/tts_build.py"      --project .   # 1 · voiceover
"$PY" "$SKILL/scripts/timeline_build.py" --project .   # 2 · global timeline
"$PY" "$SKILL/scripts/subs.py"           --project .   # 3 · subtitles + beats
"$PY" "$SKILL/scripts/lint_frames.py"    --project .   # 4 · audit frames before rendering
node    "$SKILL/scripts/render_video.mjs" . --preview 30   # 5 · 30-second draft first
"$PY" "$SKILL/scripts/qc_check.py"       --project .   # 6 · QC
node    "$SKILL/scripts/render_video.mjs" .            # 7 · full render
node    "$SKILL/scripts/cover_build.mjs"  .            #    + both covers

Output lands in out/: slug.mp4, cover_169.png, cover_34.png, slug.srt, slug.vtt, plus qc_report.md and qc_sheet.jpg.

A video project looks like this:

my-video/
├── project.json      # slug, fps, size, voice, rate, scene order, chapters
├── narration.json    # [{ id, text }] — pipes "|" split subtitle blocks
├── theme.css         # every colour, as CSS variables
├── frames/           # one HTML per scene; <id>.beats.js is generated, never hand-edited
├── audio/  render/  research/  script/
└── out/              # MP4, covers, SRT/VTT, QC report

The order matters: beats.js is derived from the TTS output. That is why, after you edit the narration, everything downstream re-times automatically while no scene code changes.

If you plan to reuse the rendered PNGs, always pass --keep-frames. Preview mode clears the frame directory after muxing by design.


Why not screen recording

Most "HTML to video" tools launch a browser, play the animation and record the screen. That breeds a whole class of intermittent bugs — the animation played before recording started, a web font loaded late so half the video used the wrong typeface, the first frames captured the pre-animation state.

html-explainer does not record. It seeks: position the GSAP timeline at that instant (tl.pause(t, false)), sync every CSS animation (document.getAnimations().currentTime), wait two animation frames, screenshot. Frame 1204 of one render is pixel-identical to frame 1204 of the next — so QC can point-sample, a single scene can be re-rendered alone, and a whole class of timing bugs becomes impossible.

The trade-off: every animation must be seekable. Wall-clock animation (setInterval, requestAnimationFrame counters, CSS transition entrances) cannot work and is rejected by lint_frames.py.


Features

Beats anchored to narration Scenes position animation by matching subtitle text (B('block text')), never by hardcoded frame numbers. Edit the script and the whole video re-times with zero scene-code changes.
Word-boundary subtitles Timing comes from edge-tts WordBoundary events, never interpolated by character count — in Chinese two phrases of equal length can differ 3× in duration.
Two-level clock Frame length uses MP3 container duration (keeps A/V in sync); the final subtitle block ends on actual speech end.
23 visual styles 8 categories, each with its canvas, type scale, timeline and colour discipline recorded. No designing from a blank page.
Dual covers Every video ships a 16:9 cover plus an independently re-laid-out 3:4 cover — not a crop. Cropping loses 57.8% of the width.
Pre-render audit + QC lint_frames.py reports contract violations before you spend render time; qc_check.py checks loudness, drift and samples frames.
Structurally offline GSAP vendored; browser auto-detected; ffmpeg falls back to the imageio-ffmpeg static binary; web fonts are banned by contract.

Pipeline

flowchart LR
    A["Topic"] --> B["Research<br/><i>every figure sourced</i>"]
    B --> C["narration.json<br/><i>pipes split subtitle blocks</i>"]
    C --> D["tts_build.py<br/><i>edge-tts to MP3 + word boundaries</i>"]
    D --> E["timeline_build.py<br/><i>global axis + stitched track</i>"]
    E --> F["subs.py<br/><i>subs.json, srt/vtt, beats.js</i>"]
    F --> G["frames/*.html<br/><i>one scene per line, styled from the library</i>"]
    G --> H{"lint_frames.py<br/>eight contract rules"}
    H -->|fail| G
    H -->|pass| I["render_video.mjs<br/><i>seek per frame to PNG</i>"]
    I --> J["ffmpeg<br/><i>H.264 + AAC mux</i>"]
    J --> K[("out/slug.mp4")]
    I --> L["qc_check.py<br/><i>loudness, drift, contact sheet</i>"]
    K --> M["cover_build.mjs<br/><i>cover_169.png, cover_34.png</i>"]

    style K fill:#1f6feb,color:#fff
    style H fill:#8957e5,color:#fff
    style M fill:#238636,color:#fff

Command reference

Command Purpose
new_project.py <dir> <slug> --topic "..." Scaffold a project
tts_build.py --project . Synthesise voiceover (caching, timeouts, backoff retries, silence trim)
timeline_build.py --project . Global timeline + stitched narration-full.mp3
subs.py --project . Three subtitle outputs + one beats.js per scene
lint_frames.py --project . Audit frames against the eight contract rules
render_video.mjs <dir> Render and mux the MP4
qc_check.py --project . Loudness / duration / stream checks + contact sheet
cover_build.mjs <dir> Render both covers, 2× by default

Common optional flags: --preview N renders only the first N seconds, --keep-frames keeps the frame PNGs, --only id,id audits just the named scenes, --scale N sets the output multiplier.

A few handy extras: peek_frame.mjs <dir> <id> (screenshot one scene in seconds — the one you reach for while designing), make_theme.py --topic "medical" --use (derive a palette from a topic word), package_skill.py (build a portable zip).

Full flag reference is in SKILL.md, or pass --help to any script.


The B() beat system

subs.py emits one beats.js per scene, exposing each subtitle block by its source text, so scene animation can be anchored directly to the moment words are spoken:

tl.fromTo('.card',    { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('right-hand card'));
tl.fromTo('.verdict', { scale: 0.8 },          { scale: 1, duration: 0.6 },        B('the verdict') + 0.2);

B('block text') is the moment those words start being spoken; Be('block text') is the moment they finish.

This is this project's own design, and it is the part that changes how the work feels: edit the narration once, re-run three commands, and every scene re-times itself. Compare with hardcoded frame numbers — one edited sentence means re-aligning the entire video by hand.

Fallbacks are forbidden. B('x') || 3.2 hides "beat not found" behind a stale number. A miss should fail the build — a mis-timed video is far worse than a failed build.


Visual style library

Do not design scenes from a blank page. 23 styles across 8 categories, each recording its canvas, type scale, timeline structure and colour discipline — full catalogue in references/style-catalog.md (Chinese).

It covers bold signal cards, luxurious minimal whitespace, NYT-grade data charts, Swiss grid, glitch art, film light leaks, fluid hero backgrounds, brand logo outros, VFX text cursors, 9:16 social formats, product promos and more.

Two classes, with very different adaptation costs:

  • rich (12) — single file, pure CSS @keyframes. The seek renderer drives them with no changes; adaptation is three steps: swap web fonts for a system stack, lift bottom elements out of the subtitle band, drop in real content.
  • gsap (11) — multiple compositions loaded from a CDN. Do not port the code. Take only the visual DNA and re-express it in CSS keyframes.

Rotate 2–4 styles per project. Eight scenes sharing one look reads as monotonous.


Dual covers

Every video ships two covers that are siblings, not crops. Cropping 16:9 to 3:4 leaves 810px of 1920px — 57.8% of the width is gone, and any full-width headline gets sliced in half. So both share one visual DNA, but each is laid out again from scratch:

Element 16:9 (1920×1080) 3:4 (1440×1080)
Paradox visual Right side, side by side Top, stacked vertically
Hook Bottom-left, two lines, ≥96px Bottom, three lines, ≥120px
Max characters per line 7 5

new_project.py generates the templates for both sizes directly; output is 2× by default. Full rules and the pre-upload checklist: references/cover-guide.md (Chinese).


Troubleshooting

Symptom Cause Fix
Every frame is a frozen first frame, no error Timeline not registered on window.__tl; or page.evaluate received a string instead of a function literal (Playwright evaluates it once, so the body never runs) Pass a real function literal
Video comes out 1280×720 viewport was passed to browser.launch() — it is a context-level option Pass it to newPage()
Cover renders at 1× Same trap, deviceScaleFactor twin Pass to newPage() + screenshot({ scale: 'device' })
Numbers render but never move The seek suppressed onUpdate callbacks Renderer fixed (pause(t, false)); use a transform-based number reel in scenes
No such file or directory on Windows Non-ASCII path — Windows ffmpeg reads UTF-8 as ANSI Keep paths ASCII
Preview finished, frame PNGs are gone Preview mode clears the frame directory by design Pass --keep-frames

references/lessons.md is the most valuable file in this repository. 34 numbered entries, each one a bug where "the video looked fine but was wrong" — including how it was misdiagnosed at first. Read it before debugging from scratch.


Who this is for

Good fit: turning articles, docs or a topic into narrated video at scale; already using an AI coding agent and preferring to describe visuals in HTML rather than learn motion software; needing reproducible output; needing to run offline or refusing per-render fees.

Poor fit: live-action editing, talking-head footage, re-creating an existing video; wanting a drag-and-drop editor (scenes are code, deliberately); wanting React component animation as the authoring model — that is the domain of the sibling project anything2explainer.


Prior work and sibling projects

html-explainer did not invent its methodology. It has two clear sources of ideas. Both are independent projects by different authors — not the same author as this one, and this repository neither bundles nor depends on any of their code at runtime. What was carried over is design specification and engineering experience.

  • Vincentwei1021/anything2explainer (TypeScript / Remotion) — topic in, narrated explainer video out. What this project inherited from that line is the narration and A/V-sync methodology: word-boundary subtitles, the two-level clock, speech-rate calibration, and the QC criteria. Difference: it is Remotion plus React component animation; this project is HTML/CSS/GSAP plus deterministic seeking.

  • nexu-io/html-video (Apache-2.0, by the Open Design team) — HTML to real MP4 on your own machine. What this project inherited from that line is the 23-style visual catalogue, transcribed from its template design specifications. Seven of those styles trace further back to MIT-licensed design work. Difference: it relies on real-time recording; this project seeks frame by frame, so output is reproducible.

This project's own parts: the deterministic seek renderer, B() beat anchoring, the dual-cover system, and the criteria in lint_frames.py / qc_check.py. Full attribution and the per-style mapping are in THIRD_PARTY_NOTICES.md (Chinese).

If you want React component animation or Studio-style visual collaboration, go straight to the two projects above. If you want to edit one sentence without re-aligning the whole video by hand, and you want frame-level reproducibility, use this one.


Roadmap

  • Per-scene render switch (--only <id>) — today this needs the temporary-project workaround
  • Cover layout measurement tool (line count / margins / overlap / hook font size in one pass)
  • More cover aspect ratios
  • English descriptions in the style catalogue
  • Optional music bed with automatic ducking under narration

Contributing

Issues and PRs are welcome. The most valuable contribution is a diagnosed silent failure added to references/lessons.md — write down how it first misled you, the evidence that proved the cause, the fix, and the general rule that follows. Please read CONTRIBUTING.md first (Chinese).


Licence and acknowledgements

MIT © 2026 Moh

MIT covers the original code and documentation. Bundled third-party components and the derived style specifications remain under their own terms; GSAP is bundled under GreenSock's standard "no charge" licence. Full notices are in THIRD_PARTY_NOTICES.md (Chinese).


Author: Moh

Built because "edit one sentence, then re-align the entire video by hand" is not a good way to spend an afternoon.

S
Description
把一个主题做成有配音、字幕、封面的讲解分析类视频 —— 全本地、用 HTML 写画面,稳定音画同步。 面向 AI 编程智能体的确定性 HTML → MP4 渲染流水线。
Readme MIT
349 KiB
0 Stars 1 Watchers 0 Forks
Languages
Python 69.6%
JavaScript 20.5%
HTML 7.2%
Shell 2.7%