by OneMoh
把一个主题做成有配音、字幕、封面的讲解分析类视频 —— 全本地、用 HTML 写画面,稳定音画同步。 面向 AI 编程智能体的确定性 HTML → MP4 渲染流水线。
# Add to your Claude Code skills
git clone https://github.com/OneMoh/html-explainerGuides for using ai agents skills like html-explainer.
See how html-explainer compares with popular alternatives.
html-explainer is an open-source ai agents skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by OneMoh. 把一个主题做成有配音、字幕、封面的讲解分析类视频 —— 全本地、用 HTML 写画面,稳定音画同步。 面向 AI 编程智能体的确定性 HTML → MP4 渲染流水线。. It has 51 GitHub stars.
html-explainer's catalog security scan is still queued. You can run an instant dependency and prompt-injection check now with the "Scan for vulnerabilities" button above.
Clone the repository with "git clone https://github.com/OneMoh/html-explainer" and add it to your Claude Code skills directory (see the Installation section above). html-explainer ships a SKILL.md manifest, so compatible agents can discover and load it automatically.
html-explainer is primarily written in Python. It is open-source under OneMoh on GitHub, so you can review or fork the full source.
Yes. SkillsLLM lists many other AI Agents skills you can browse and compare side by side. Open the AI Agents category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh html-explainer against similar tools.
No comments yet. Be the first to share your thoughts!
⚠️ Third-Party Software Notice
This skill is third-party open-source software developed and hosted independently on GitHub. SkillsLLM is an informational directory and does not control or maintain the underlying repository.
Any security checks, ratings, or warnings displayed by SkillsLLM are automated and limited in scope. They do not constitute a security certification or guarantee that the software is safe, error-free, or free from malicious code, vulnerabilities, compromised dependencies, or prompt-injection risks.
Review the source code, permissions, dependencies, and configuration before installing or running any third-party skill. Use is at your own risk. To the maximum extent permitted by applicable law, SkillsLLM is not liable for losses arising from third-party software.
The deep catalog scan for this skill is still queued. Run an instant dependency check now instead.
定位:
anything2explainer的流程与音画同步 +html-video的 23 个模板风格库, 渲染层独立实现。作者 Moh,MIT 许可(第三方声明见THIRD_PARTY_NOTICES.md)。零外部依赖:不需要 Remotion/npm 工程,不需要 html-video 仓库/Studio/pnpm/agent 后端。 技能自带:GSAP(离线)、playwright-core、渲染器、TTS/字幕/时间轴/主题/QC/封面全套脚本。 对两个来源项目的引用只存在于注释署名里,运行时不读它们的任何文件。 风格库是抄下来的设计规范文本(
references/style-catalog.md),来源html-video(Apache-2.0), 类比:把菜谱抄回家,之后做菜不需要原餐厅营业。
把一个主题做成原创讲解视频:任意风格的 MG 画面(HTML/CSS/GSAP,1920×1080 或竖版)、
配音(edge-tts)、词级对齐硬字幕、全局进度条。一句话一个场景,画面节拍直接锚在
吐字时刻上(B('块文本') 节拍器)。
不要自己从零想画面 —— 先从风格库里挑;挑出候选后交用户拍板(见确认点 0**),不许静默自选。**
完整目录(含每种的画布/字体/时间轴/配色纪律)
见 references/style-catalog.md;怎么改编成合规帧见 references/template-guide.md。
| 类别 | 可用风格 |
|---|---|
| 演示 / 标题卡 | 大胆海报帧、大胆信号卡帧、奢华极简留白帧、创意电压分屏帧、电光工作室分屏帧、故障艺术标题帧、Kinetic Type、Swiss Grid、Warm Grain |
| 数据可视化 | NYT 风数据图表帧、数据滚动帧、NYT Graph、瑞士网格数据帧 |
| 图解 / 流程 | 东方柔和有机帧、Decision Tree |
| 氛围 / 空镜 | 胶片漏光电影帧 |
| 营销 / Hero | 流体背景 Hero 帧 |
| 片头片尾 | 品牌 Logo 收尾帧 |
| 社媒竖版 | Play Mode、Vignelli(9:16) |
| 产品演示 | Product Promo、Product Promo · 30s |
| 特效 | VFX 文字光标 |
两类模板、两种改编成本(速查表「类型」列):
| 类型 | 数量 | 处理 |
|---|---|---|
| ★ rich | 12 | 单文件 + 纯 CSS @keyframes。零改动可渲染 —— 只需①换系统字体栈(删 Google Fonts)②让出底部字幕带③填真实内容 |
| gsap | 11 | 多 composition + CDN GSAP(或 Remotion)。不要搬代码,只取视觉 DNA 用 CSS keyframes 重写(搬进来会得到静止首帧且零报错,见 lessons.md #9) |
时长档要匹配内容:
frame-bold-signal是 3–6s 的短片花,拉长到 20s 会空。 长段(>10s)优先选 3–30s 档的(Swiss Grid / Kinetic Type / NYT Graph / Warm Grain)。
bash <skill>/setup_env.sh # 自检;--install 联网补装
依赖:Python≥3.9(edge-tts==7.2.8 钉死 / numpy / pillow / imageio-ffmpeg)、Node≥18、
火山引擎零额外依赖(tts_volcano.py 只用标准库 urllib,不需要装任何 SDK)。
Chrome 或 Edge(几乎必有;都没有才下载 playwright chromium ~115MB)、ffmpeg(PATH 或
imageio-ffmpeg 静态二进制自动回退)。Windows 注意:项目路径全 ASCII;给 Node/Python
传 C:/... 正斜杠路径;别用 heredoc 给 Python 传正则。
PY=<venv python 绝对路径> # 派子 agent 时必须展开成绝对路径写进 prompt
"$PY" <skill>/scripts/new_project.py <dir> <slug> --topic "主题" # 阶段 0 建项目
"$PY" <skill>/scripts/tts_setup.py --project . # ★ 先定配音方案 + 音色(问用户:edge / 火山)
"$PY" <skill>/scripts/tts_build.py --project . # 配音:audio/*.mp3 + manifest
"$PY" <skill>/scripts/timeline_build.py --project . # 全局时间轴:layout.json + narration-full.mp3
"$PY" <skill>/scripts/subs.py --project . # 字幕:subs.json + beats.js + srt/vtt
"$PY" <skill>/scripts/lint_frames.py --project . # 静态体检:八条契约违规(渲染前一秒出结果,比渲完再发现便宜得多)
node <skill>/scripts/check_layout.mjs . # ★ 几何体检:越界 / 侵入字幕带 / 元素互相遮挡(lint 看不见几何)
node <skill>/scripts/render_video.mjs . [--preview 30] [--keep-frames] [--only <场景id>] [--mux-only] [--png-fast|--jpeg] [--concurrency N] # 渲染:out/<slug>.mp4
"$PY" <skill>/scripts/qc_check.py --project . # 体检 + 抽帧速览图
node <skill>/scripts/cover_build.mjs . # 封面双方案:out/cover_169.png + cover_34.png
配音引擎(跑之前必须先问用户,见确认点 3):edge(默认,免费免密钥)或
volcano(火山引擎语音合成 2.0,音质更好,需 API Key)。两者产出的 manifest 结构一致,
下游零改动。切换:--provider edge|volcano 或 TTS_PROVIDER 环境变量。
火山密钥只存 tts.env(已 gitignore)—— agent 只调 tts_volcano.py,不读该文件。
详见 references/volcano-tts.md。
渲染截图模式(画面里有满幅照片时务必换掉默认 PNG,见 lessons.md 第 69 条):
默认 PNG 对纯 CSS 图形帧很快(45ms/帧),但对照片满幅帧是 582ms/帧(13×),
且编码在浏览器进程内串行 —— 加 --concurrency 完全无效(实测并发 1/3/6 路的总吞吐
1.80 / 1.86 / 1.87 帧/秒)。照片类片子选:
--png-fast:CDP optimizeForSpeed,逐像素无损、4.4× 加速(132ms/帧,体积 +22%)。--jpeg --jpeg-quality 95 --crf 18 --preset medium:13× 加速、体积 1/5;
q95 = PSNR 41.65dB,已低于 x264 crf18 自身的失真,成片看不出。辅助工具(随时可用,不进主流水线):
"$PY" <skill>/scripts/make_theme.py --topic "医疗" --use # 主题换色(只重写 theme.css,帧零改动)
node <skill>/scripts/peek_frame.mjs . <帧id> --at 40,80 # 单帧速览:秒级出图,先看设计对不对
# ★ peek_frame 的 --at 是**百分比**不是帧号;查冷开场空屏要传 --at 1,3,6 这种小百分数
node <skill>/scripts/peek_frame.mjs . <帧id> --at 100 --guides # 叠十字中线 + 字幕禁区线(判对齐必开)
"$PY" <skill>/scripts/frame_at.py --project . --at 1:23 # 时间点 → 场景/帧号/源文件/终态帧图
"$PY" <skill>/scripts/frame_at.py --project . --list # 全片场景时间表
画面排障(用户报「几分几秒」→ 定位到帧 → 视觉模型看图 → 改 → 局部重渲)的完整四步见 上文「★ 画面出错怎么定位」。
顺序不能乱:tts → timeline → subs(beats 依赖前两者)→ lint → 渲染 → QC → 封面。改解说词 → 重跑前三条,
frames/<id>.beats.js 自动刷新,场景 HTML 一行不用改(这是对 anything2explainer
「帧号硬编码、改一个字全片重对位」的结构性改进)。
--preview会删掉渲出来的帧(设计上是「预览模式顺手清草稿」)。凡是要拿 PNG 做 拼接 / 复核 / 只重渲单场,一律加--keep-frames,否则帧目录会在合成后被清空(lessons #32)。 只改了一两个场景的画面时,不要全片重渲:用「临时项目法」按全局帧号补渲再贴回, 本片 3802 帧全重渲 924s,补渲 hook+outro 两段只花 220s(lessons #33)。
封面决定点击率,成片决定完播率 —— 一张被切掉半个钩子的封面会让整片白做。
| 规格 | 画布 | 输出 | 用途 |
|---|---|---|---|
| A. 抖音主封面 | 1920×1080 | out/cover_169.png |
信息流 / 播放页 |
| B. 兼容 3:4 | 1440×1080 | out/cover_34.png |
主页栅格(防切字) |
核心纪律:两张是独立排版,不是裁切关系。 从 16:9 居中裁 3:4 只剩 810px 宽,丢掉 57.8% 画面,大字钩子必被切。 所以两份共享同一套视觉基因(配色/幕底/主视觉/钩子文案),各自重排一次版:
| 元素 | 16:9 版 | 3:4 版 |
|---|---|---|
| 悖论视觉 | 右侧,左右并置 | 上方,竖排堆叠 |
| 钩子 | 左下,两行 132px | 下方,三行 118px |
| 角标 | 左上 | 顶部居中 |
| 每行字数 | ≤7 字(防孤字断行) | ≤5 字 |
封面三要素(缺一返工):① 大字钩子(≥96px/≥120px,含反差悬念) ② 核心悖论视觉(两个对数 / 一升一降 / 分裂线 / 一明一暗,不是装饰图形) ③ 信息余量(四边 ≥96px/≥110px,无贴边文字)。
做法:复制 assets/cover-template.html 为 frames/cover_169.html 与 frames/cover_34.html,
各自排版 → node scripts/cover_build.mjs .(默认 seek 到时间轴末尾取完整态,--at 0.8 可取入场中间态)。
详规见 references/cover-guide.md。
| 机制 | 口径 |
|---|---|
| 帧时长 | MP3 容器时长(tts_build 裁首尾静音后回填)。词边界时长每段少 ~0.86s,用它必错位 |
| 末块字幕收尾 | 语音真实结束(speech_end_sec)—— 两级时钟,混用则「字幕过了语音还没过」 |
| 字幕节拍 | 词级时间戳首字对帧号,绝不按字数插值(中文同字数时长差 3 倍)。edge 取 WordBoundary,火山取 sentence.words[](需显式开 audio_params.enable_subtitle,本包默认开;不开会静默退回插值) |
| 画面节拍 | 场景 HTML 里 B('块文本') 取该词起播秒排 GSAP —— 画面与吐字同源 |
| 渲染 | 确定性 seek:tl.pause(t, false) + CSS 动画 currentTime=t×1000 → 截图 → ffmpeg 合成。无实时录制,html-video 的引导期/起播/字体坑整类不存在。第二参必须传 false,少了它 onUpdate 类回调被静默抑制(数字滚动恒为初值,见 lessons #27) |
| 字幕层/进度条 | 渲染器注入并逐帧驱动(#mg-subs 44px 白字黑边 bottom 96px;#mg-progress accent 填充),帧作者零负担 |
完整版见 references/workflow-guide.md(含研究员/构建/QC agent 的 prompt 模板与时长档位表)。
建项目(5 分钟):new_project.py + 定主题(make_theme.py --topic/--preset … --use,颜色全在 theme.css 的 CSS 变量里,画面代码禁止色值字面量)
这是硬规则,每一期都要走一遍,不得沿用上一期的选择。
references/style-catalog.md 选出 2–4 个候选列出来,
不要静默从「上次挺好用」的记忆里定 2–4 种。--preset amber、看到数据题就默认瑞士网格。
记忆里"好用"的东西不构成用户的选择。调研(20 分钟,1 agent):research/调研.md,每个数字带 URL;确认点 1(时长/语言)并行问
解说词(30 分钟):narration.json(| 切字幕块,中文 ≤16 字/块)→ 填 order → 确认点 2(文案定稿)→ 确认点 3(配音方案 + 音色)→ tts/timeline/subs 三连 → 核对时长区间(差 >15% 改句子,别改语速硬凑)→ 定稿后不改词
确认点 3 必须问两件事(用 tts_setup.py 落实):
① 方案 —— 「配音用 edge-tts(免费、免密钥、开箱可用)还是火山引擎语音合成 2.0
(音质更好,需要 API Key,约 1 分钟配置)」;
② 音色 —— 选定方案后列候选让用户挑,也可自定义 ID。
选火山时:缺 tts.env → tts_setup.py 生成空模板并停下来,让用户手工填密钥
(agent 不读该文件、不参与填值);用户说填好了 → --check 测连接 → 再选音色。
★ 提醒用户用 cp tts.env.example tts.env 复制,别把 tts.env.example 改名成
tts.env —— 那是要留在仓库里的模板(改名会让 check_integrity.py 报错)。
详见 references/volcano-tts.md。
分镜(含选风格,20 分钟):script/storyboard.md,每场景一行 ——
先从风格库挑出 2–4 个候选交用户选(受确认点 0 约束,不可静默自选),
再照 references/style-catalog.md 的「挑风格的实用建议」表核对内容类型与时长档,
最后写画面/主角·尺寸/光/B() 锚点;末尾全局约束
(贯穿示例、事实清单、每章 1–2 高光时刻、每章 ≥3 运镜)。
全片建议 2–4 种风格轮换,避免 8 个场景全用同一个模板。
场景构建(并行):每组 4–8 场景一个 agent(一波 ≤3–4 个)。
source/index.html 改写 —— ①删 Google Fonts 换系统栈
②底部元素抬到 ≥176px ③填真实内容(照 example.md 的字段)frames/_template.html 复制,契约见 references/frame-contract.mdreferences/template-guide.md;边做边写盘node scripts/peek_frame.mjs <项目> <id> --at 40,80 秒级出图静态体检:lint_frames.py --project . 把八条契约违规钉在渲染之前(外链字体、色值字面量、
墙钟逻辑、B()|| N 兜底、字幕带压内容、缺中文字体族…);FAIL 清零再进渲染。
再跑 check_layout.mjs . —— lint 看不见几何,元素互相遮挡 / 侵入字幕带 / 出画只有它管;
ERROR 清零再进渲染(两条命令都是一秒级,比渲完几千帧再回来看便宜得多)。
打样:render_video.mjs . --preview 30 → 确认点 4(风格/字号/语速/节奏一次定稿)→ 全片渲染 + qc_check
QC:qc_report.md 的 FAIL 清零 + qc_sheet.jpg 肉眼过(字幕带 80–170px 无内容、一焦点、光跟主角)→ 按组修复 → 重渲
封面双方案:做 frames/cover_169.html + cover_34.html(独立排版)→ node scripts/cover_build.mjs .
→ 核对 out/cover_report.md + references/cover-guide.md 的七条自检清单
交付:mp4 + 两张封面 + srt/vtt(上传平台=可检索文本)+ 发布说明(硬字幕→关平台自动字幕; AI 配音→勾 AIGC;封面文字须与视频首帧钩子同义;受监管题材过合规)
成片里发现画面问题(元素错位、被压住、少了东西、动效没走完)时,不要重新描述场景内容、 不要从头翻 5000 帧。流程固定成四步:
用户只需要给「几分几秒」+ 一句现象。 例:1:23 右下角示意图里小黑点没在射线汇聚点上,偏左上。
.github/ISSUE_TEMPLATE/bug_report.yml 里有一栏专门收这个时间点。
frame_at.py 把时间点翻译成定位信息(一条命令,秒级):
"$PY" <skill>/scripts/frame_at.py --project . --at 1:23
"$PY" <skill>/scripts/frame_at.py --project . --at 1:23 --box 1400,200,1920,900 # 再裁一块可疑区
"$PY" <skill>/scripts/frame_at.py --project . --list # 全片场景时间表
产出 out/probe/:场景 id / 帧号 / 场景源文件 frames/<id>.html / 节拍文件 / 当刻字幕块
(反查代码里的哪一句 B('…')),以及三张给视觉模型看的图 —— 整帧(1280 宽)、
底部 260px 禁区带(1:1)、场景终态帧。
带视觉的模型看那几张图(这是关键:只用文本描述「蓝点没在中间」定位不到,看图能直接 读出偏了多少、偏哪个方向、被谁压住)。先看终态帧,再看当刻帧。
改 frames/<id>.html → 重跑 check_layout.mjs(几何)→ lint_frames.py(契约)→
render_video.mjs . --only <场景id> 局部重渲 → 回到第 2 步复核同一时间点。
--only <id> 只对末场安全(见 lessons.md #15):改短了必须删尾部过期帧,
且渲染日志的 ✓ 不算证据 —— 用 frame_at.py 回到那个时间点看图确认。八条:1920×1080 系统字体(禁外部字体)→ 颜色只取 theme.css 变量 → 动画只用 GSAP
(window.__tl 注册,禁 CSS transition 入场;@keyframes 循环装饰可用,渲染器会 seek)→
节拍用 B() → 字幕带(80–170px)与进度条带(0–12px)不放内容 → 一场景一焦点
(主角 ≥170px 或大字 ≥96px 带 accent 柔光,配角不发光,文字 ≥22px)→
GSAP 用 ../assets/gsap.min.js(本地内置)→ 主体动画压在 speech_end 前。
check_layout.mjs ERROR 清零 —— 遮挡/越界是唯一一类「lint 全绿但仍然错」的问题
(lint 只看文本规则)。一个几何体只准有一个坐标系:SVG 图元与 HTML 部件不得混用两套基准
(issue #1「蓝点没落在射线汇聚点上」的根因,见 references/frame-contract.md)--mg-sub-fg / --mg-sub-stroke / --mg-track / --mg-tick 即可(见 references/lessons.md #75)。
它是渲染期产物 —— 改它 = 整片重渲,所以第一次全片渲染前先用 --preview 拿到
跨明暗切换的那几十秒。流水线脚本(按执行顺序)
| 路径 | 作用 |
|---|---|
scripts/new_project.py |
阶段 0 脚手架:建目录树 + project.json + narration.json 占位 + theme.css + 两份封面 HTML |
scripts/tts_setup.py |
配音方案向导:选 edge/火山 → 缺密钥则生成 tts.env 模板并停下 → 测连接 → 选音色 → 写回 project.json。输出 NEXT_ACTION=… 供 agent 判断下一步 |
scripts/tts_volcano.py |
火山引擎语音合成 2.0 接口包:唯一读 tts.env 的地方;--check / --voices / --synth。密钥不回显、异常脱敏(_redact) |
scripts/tts_build.py |
配音合成:edge-tts 或 火山引擎(--provider);缓存/硬超时/退避重试/裁静音,manifest 写两级时长。两引擎 manifest 结构一致 |
scripts/timeline_build.py |
layout.json 全局轴 + narration-full.mp3(gap 显式插入) |
scripts/subs.py |
字幕三出口(subs.json / srt+vtt / 画面内层由渲染器注入)+ beats.js 节拍器 |
scripts/lint_frames.py |
渲染前静态体检:八条契约违规逐条报(外链字体/色值字面量/墙钟/B()||N/字幕带压内容/缺中文字体族…)—— 只看文本规则,看不见几何 |
scripts/check_layout.mjs |
渲染前几何体检(终态):侵入字幕禁区 / 出画 / 文字被遮挡 / 文字重叠 = ERROR;越安全边 / 文字压色块 / 色块重叠 = WARN;疑似未对齐 = INFO。量的是字墨范围(Range 逐行)而非元素框。--only / --json / --safe-bottom;有 ERROR 退出码 1 |
scripts/render_video.mjs |
渲染器:浏览器探测 → 逐场景 seek 截图 → ffmpeg 合成;--preview N 快样片,--only <场景id> 只重渲指定场景(仅末场安全,变短后须清尾部过期帧,见 lessons 45),--mux-only 用现有帧重新合成;帧里有满幅照片就必须换截图模式(默认 PNG 编码占 96% 帧时间且与并发无关):--png-fast 无损 4.4×,--jpeg --jpeg-quality 95 13×;--crf N / --preset <名> 单独控制成片码率 |
scripts/qc_check.py |
流/时长/音量/抽帧体检 + contact sheet |
scripts/cover_build.mjs |
封面双方案渲染器:width=1920/1440 两份独立排版 → 2 倍图;--at / --only / --jpg |
辅助工具
| 路径 | 作用 |
|---|---|
scripts/peek_frame.mjs |
单帧速览:不渲全片,秒级截某场景的几个时点看图(--at 是百分比);--guides 叠十字中线 + 字幕禁区线(判「元素有没有对齐」必须开,没有基准线肉眼判不了) |
scripts/frame_at.py |
时间点 → 定位:报「几分几秒」就能拿到场景 id / 帧号 / 源文件 / 当刻字幕块 / 整帧图 / 底部禁区带裁图 / 场景终态帧(--at 1:23 / --list / --box x0,y0,x1,y1 / --final)。画面排障的入口工具 |
scripts/check_integrity.py |
仓库自洽性:版本号/风格目录/计数一致性 + 模板外链扫描(CI 与本地都跑) |
scripts/make_theme.py |
4 预设 + 主题词推色 → theme.css(CSS 变量单源) |
scripts/import_styles.py |
(移植期一次性工具)把已装 html-video 的设计规范抄成纯文本风格目录;跑视频永不需要它 |
tests/geometry-fixture/ |
几何体检的证伪样本:故意坏掉的帧(越界 + 遮挡 + 错位),期望 ERROR 2 / WARN 0 / INFO 1。改 check_layout.mjs 后先拿它验「还抓得到错」,再拿真实项目验「误报没变多」 |
setup_env.sh |
环境自检 / --install 联网装缺项 |
package_skill.py |
打成可移植 zip(--with-deps 含 node_modules) |
参考文档与资源
| 路径 | 作用 |
|---|---|
references/style-catalog.md |
23 个画面风格目录(画布/字体/时间轴/配色纪律 + rich/gsap 分类)——纯知识,非代码依赖 |
references/style-catalog.json |
同上的机器可读版(kf/multi/engine 字段用于自动判类型) |
references/template-guide.md |
模板改编指南(rich 三步法 / gsap 重写法 / 挑风格建议) |
references/frame-contract.md |
契约细则 + 版式基因 + 反例 |
references/cover-guide.md |
封面双方案详规:重排对照表 / 三要素 / 尺寸倍率 / 上传策略 / 七条自检 |
references/workflow-guide.md |
阶段详解 + agent prompt 模板 + 时长档位表 |
references/lessons.md |
踩坑台账(继承 14 条 + 本技能记录,持续追加) |
assets/frame-template.html |
场景模板(契约注释在文件头,B() 用法示例) |
assets/cover-template.html |
封面模板(封面三要素注释在文件头,可改尺寸复用为两份) |
assets/gsap.min.js |
GSAP 3.13 本地内置(离线渲染;License 见同目录 gsap-README.md) |
README.md / README.en.md |
对外项目说明(README.md 中文为默认,README.en.md 英文;含跨 Agent 安装指引) |
CONTRIBUTING.md |
贡献指南:硬性规则、端到端自检、PR 清单 |
CHANGELOG.md |
版本变更史 |
THIRD_PARTY_NOTICES.md |
第三方组件与衍生内容的授权声明(发布前必读) |
本技能遵循 Agent Skills 约定(SKILL.md +
scripts/ + references/ + assets/),不绑定任何单一智能体平台。仓库根目录就是
技能目录,所以 clone 到下表任一路径即可直接生效,不需要再拷子目录。
| 智能体 | 个人级(全局) | 项目级(仓库内) |
|---|---|---|
| WorkBuddy | ~/.workbuddy/skills/ |
<工作区>/.workbuddy/skills/ |
| Claude Code | ~/.claude/skills/ |
.claude/skills/ |
| OpenAI Codex | ~/.codex/skills/ |
.codex/skills/ 或 .agents/skills/ |
| Gemini CLI | ~/.gemini/skills/ |
.gemini/skills/ 或 .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/ |
| 通用约定 | ~/.agents/skills/ |
.agents/skills/ |
手动安装(把 ~/.claude 换成你所用智能体的目录):
git clone https://github.com/OneMoh/html-explainer.git ~/.claude/skills/html-explainer
bash ~/.claude/skills/html-explainer/setup_env.sh --install
也可以直接把这句话交给智能体,让它自己装:
给当前本地环境安装该 Skill:https://github.com/OneMoh/html-explainer.git 安装到你的技能目录,并检测安装必要的运行环境(Python 3.9+ / Node 18+ / Chrome 或 Edge / ffmpeg)
装完新开一个会话,让智能体重新扫描技能目录。同理,调用本技能时应把
<SKILL_ROOT> 替换成实际安装路径 —— 派子 agent 时要展开成绝对路径写进 prompt。
技能目录自包含,不引用本机任何绝对路径(脚本按自身位置定位 SKILL_ROOT)。
仅有两处兜底探测会去探 WorkBuddy 托管的运行时目录(setup_env.sh 的 Python/Node
候选、peek_frame.mjs 的技能根候选)—— 探不到就自动跳过,不影响其它平台。
零依赖声明(重要):本技能不需要 html-video 或 anything2explainer 存在。
两个来源项目的名字只出现在:① 代码注释的出处署名 ② scripts/import_styles.py
这个移植期一次性工具的用法说明里。跑一条视频(tts → timeline → subs → render →
qc → cover)完全不碰这两个项目。风格库是抄成纯文本的设计规范
(references/style-catalog.md),已随技能打包。
# ① 打包(默认不含 node_modules,只有 ~110KB)
python package_skill.py # → dist/html-explainer-v<版本>.zip
python package_skill.py --with-deps # 含 playwright-core,~14MB(目标机全程离线)
# ② 新机器:解压到任意目录(放进上表任一「个人级」技能目录即可被该智能体识别)
bash setup_env.sh --install # 自检 + 装缺项(先装后判定,装成功即算就绪)
# ③ 跑一遍端到端(可选,验证链路)
python scripts/new_project.py demo --topic "人工智能"
# …填 narration.json / 写 frames/*.html / 填 project.json.order…
python scripts/tts_build.py --project . && python scripts/timeline_build.py --project .
python scripts/subs.py --project . && node scripts/render_video.mjs .
python scripts/qc_check.py --project . && node scripts/cover_build.mjs .
依赖探测顺序(都尽量用系统已有的,避免下载):
python3 / pythonnode -v ≥18;没有则扫 ~/.workbuddy/binaries/node/versions/*(WorkBuddy 托管路径)BROWSER_PATH 环境变量imageio_ffmpeg.get_ffmpeg_exe()(pip 装依赖时自带静态二进制)assets/gsap.min.js(离线,不外链)已验证:把 zip 解压到干净目录、只跑 setup_env.sh --install,全链路输出与原目录逐帧一致
(同 346 帧、同抽帧体积、同音画差 0.05s)。
LICENSE)。references/style-catalog.md / .json)由 nexu-io/html-video
(Apache-2.0)的模板设计规范转写而来,已保留署名;转写文本按 Apache-2.0 分发。B() 节拍锚定、封面双方案为本项目原创。assets/gsap-README.md)、
playwright-core(Apache-2.0)。THIRD_PARTY_NOTICES.md。发布/再分发前请连同该文件一起带上。一个 Agent Skill:给任意主题,产出一条带配音、硬字幕、封面的讲解视频。
HTML 写画面 → 确定性逐帧渲染 → 真 MP4。全本地跑,核心链路零 API key、零按次计费。
三条用本技能生成的成片——画面、配音、字幕、封面全部由流水线产出,无手工后期。 封面等素材托管在演示仓库,本仓库零体积。
https://github.com/user-attachments/assets/912e6c2d-831f-43dd-a39f-57b749bb417d
| 港股创新药 · 早盘 | 量化简史 |
|---|---|
| https://github.com/user-attachments/assets/8007843c-088d-457c-8550-81ec912e0add | https://github.com/user-attachments/assets/28949988-449b-4c4e-8d0f-6706af6f64ee |
欢迎关注测试账号,实时观看视频数据
| 抖音 · OnlyOneMoh | 主页实况 · Moen | 抖音 · Moen_xin |
|---|---|---|
html-explainer 把一个主题做成带配音、带硬字幕、带进度条的讲解视频,画面用
HTML/CSS/GSAP 写。
它是一个 Agent Skill(SKILL.md + scripts/ + references/),遵循
Agent Skills 约定,Claude Code / OpenAI Codex /
WorkBuddy / Cursor / Gemini CLI 等都能直接加载。内部是普通的 Python 与 Node 脚本,
手工跑也完全没问题。
你只需要说一句:
把「为什么天空是蓝色的」做成一条 1 分钟的讲解视频。
技能会带着智能体走完:调研 → 解说词 → 配音 → 字幕与节拍 → 挑风格写画面 → 渲染 → 体检 → 封面。
把下面这句发给你的智能体:
给当前本地环境安装该 Skill:https://github.com/OneMoh/html-explainer.git
安装到你的技能目录,并检测安装必要的运行环境(Python 3.9+ / Node 18+ / Chrome 或 Edge / ffmpeg)
它会自己 clone 到对应目录、跑环境自检、把缺的东西装上。装完新开一个会话,让智能体重新 扫描技能目录。
仓库根目录就是技能目录(SKILL.md 在根),直接 clone 进技能目录即可,不用再拷子目录:
git clone https://github.com/OneMoh/html-explainer.git ~/.workbuddy/skills/html-explainer
bash ~/.workbuddy/skills/html-explainer/setup_env.sh --install
Windows 上 ~ 就是 C:\Users\<你的用户名>。
| 智能体 | 个人级(全局) | 项目级(仓库内) |
|---|---|---|
| WorkBuddy | ~/.workbuddy/skills/ |
<工作区>/.workbuddy/skills/ |
| Claude Code | ~/.claude/skills/ |
.claude/skills/ |
| OpenAI Codex | ~/.codex/skills/ |
.codex/skills/ 或 .agents/skills/ |
| Gemini CLI | ~/.gemini/skills/ |
.gemini/skills/ 或 .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/ |
| 其他(通用约定) | ~/.agents/skills/ |
.agents/skills/ |
记不住放哪:优先 .agents/skills/,多数工具都认它(Claude Code 是例外,只认
.claude/skills/)。
| 项 | 最低 | 说明 |
|---|---|---|
| Python | 3.9+ | edge-tts==7.2.8(刻意钉死 —— v7 改过边界 API)、numpy、pillow、imageio-ffmpeg。火山引擎引擎零额外依赖(只用标准库 urllib,不需要装 SDK) |
| Node.js | 18+ | 渲染器与封面器用 |
| 浏览器 | Chrome 或 Edge | 自动探测;都没有才下载 playwright chromium(约 115MB,只需一次) |
| ffmpeg | 任意版本 | 先在 PATH 找;没有则用 imageio-ffmpeg 自带的静态二进制 |
| 磁盘 | 每条成片约 2GB | 帧 PNG 体积大,合成后可删 |
bash setup_env.sh 只检查并报告缺什么,--install 才会装。全程不需要管理员权限。
流水线开始前,智能体会先问你用哪个 TTS,再列候选音色让你挑(也可以直接给它音色 ID):
edge-tts |
火山引擎语音合成 2.0 | |
|---|---|---|
| 你要做什么 | 什么都不用做 | 填一次 API Key |
| 费用 | 免费 | 按字符计费 |
| 音色 | 内置中英文若干 | 豆包 2.0 音色库,含声音复刻 |
| 字级时间戳 | WordBoundary |
sentence.words[] |
| 额外依赖 | edge-tts==7.2.8、imageio-ffmpeg |
零 —— 只用标准库 urllib |
| 什么时候选它 | 默认。开箱可用、够用 | 想要更自然的语气与更好音质 |
两者产出的 audio-manifest.json 结构完全一致,所以时间轴、字幕、渲染全都不用改 ——
换引擎只是换一个字段的事。
技能第一次跑火山时会生成一个 tts.env 并停下来,然后告诉你把 API Key 填进去
(火山控制台 → 语音技术 → API Key 管理)。你填好回来说一声,智能体就去测连接、确认音色,
然后接着往下跑。
这一步刻意不经过对话窗口 —— 密钥不发给智能体,智能体也不需要知道它。
内置常用音色(完整列表见官方音色文档):
| 男声 | 女声 |
|---|---|
| 云舟 2.0(默认)· 温暖阿虎 2.0 · 解说小明 2.0 · 磁性解说男声 2.0悬疑解说 2.0 · 广告解说 2.0 · 儒雅青年 2.0 · 少年梓辛 2.0 · 深夜播客 2.0 | 小何 2.0 · Vivi 2.0 · 知性灿灿 2.0甜美桃子 2.0 · 邻家女孩 2.0 · 温柔淑女 2.0 |
内置列表不够用时,也可以直接用声音复刻出来的音色 ID。
API Key 只存在 tts.env 里,而且只有技能里的一个接口包读它。
智能体调的是那个接口包,拿不到、也不需要读密钥的值:
你(手工填一次)→ tts.env(已被 .gitignore 忽略)
↓ 只有接口包读
合成调用 ← 智能体只调这个,永远拿不到密钥值
↓
火山引擎
ef90******86c8)。.gitignore 覆盖 tts.env / *.env / *.key / *.pem / secrets/;
仓库自检里有专门的密钥防线,并且做过反向自证 —— 故意植入一个假密钥会被当场抓到。AKLT… 形态的 token ——
刻意不用「长字符串就算密钥」这种宽规则,否则连排查要用的请求 ID 一起抹掉。说句实话:合成代码是在你本机执行的,运行时进程里密钥对那一段脚本可见, 「智能体绝对读不到」做不到。上面保证的是不进对话、不进仓库、不进日志、不进分发, 把泄露半径收敛成一次可撤销的事故。
另外别用环境变量存密钥 —— 很多宿主会把进程环境原样打进会话记录。
别把 tts.env.example 改名成 tts.env 去填:那是要留在仓库里的模板,
改名会让后来的人没模板可抄(仓库自检会报错并提示)。复制一份再填。
说一句需求,智能体会在一个视频项目目录里走完整个流水线;产物落在 out/:
slug.mp4、cover_169.png、cover_34.png、slug.srt、slug.vtt,外加 qc_report.md
与 qc_sheet.jpg。
项目目录长这样:
my-video/
├── project.json # slug、fps、尺寸、音色、语速、场景顺序、章节
├── narration.json # [{ id, text }] —— 用 "|" 切字幕块
├── theme.css # 所有颜色,以 CSS 变量形式
├── frames/ # 一个场景一个 HTML;<id>.beats.js 自动生成,绝不手改
├── audio/ render/ research/ script/
└── out/ # MP4、封面、SRT/VTT、QC 报告
多数「HTML 转视频」工具是启动浏览器、播放动画、录屏。这条路会生出一整类间歇性 bug —— 动画在录制开始前就播了、网络字体加载晚了导致半段视频用了错误字体、前几帧拍到的是动画前的状态。
html-explainer 不录制,它 seek:把 GSAP 时间轴定位到那一瞬间
(tl.pause(t, false)),同步所有 CSS 动画(document.getAnimations().currentTime),
等两个动画帧后截图。于是第 1204 帧与下一次渲染的第 1204 帧逐像素一致 —— QC 可以定点抽查、
单个场景可以独立重渲,一整类时序 bug 从根本上无法发生。
代价是每个动画都必须可 seek:墙钟动画(setInterval、requestAnimationFrame 计数、
CSS transition 入场)不可能工作,会被 lint_frames.py 直接拒绝。
| 节拍锚定解说词 | 画面按匹配字幕文本定位动画(B('块文本')),绝不硬编码帧号。改解说词,全片自动重排时间,画面代码零改动。 |
| 词边界字幕 | 时序来自 TTS 的字级时间戳,不按字数插值 —— 中文里两个同字数的短语时长可以差 3 倍。edge-tts 取 WordBoundary,火山引擎取 sentence.words[]。 |
| 两个配音引擎 | edge-tts(免费、免密钥、默认)或火山引擎语音合成 2.0(音质更好,需 API Key)。两者产出的 manifest 结构一致,下游零改动。切换用 --provider。 |
| 密钥不进 agent、不进仓库 | 火山 API Key 只存 tts.env(已 gitignore),只有接口包读它 —— agent 只调 tts_volcano.py,拿不到也不需读密钥。异常信息脱敏;check_integrity.py 有专门的密钥防线检查;打包排除密钥文件。 |
| 两级时钟 | 帧长用 MP3 容器时长(保音画同步);末块字幕按语音真实结束收尾。 |
| 23 种画面风格 | 8 个类别,每种都记录了画布、字阶、时间轴与配色纪律。不用对着空白页从零设计。 |
| 封面双方案 | 每条成片附带 16:9 与独立重排的 3:4 封面 —— 不是裁切,裁切会丢掉 57.8% 的画面宽度。 |
| 渲染前体检 + QC | lint_frames.py 在渲染前报出契约违规;qc_check.py 查响度、时长漂移、抽帧速览。 |
| 结构性离线 | GSAP 内置;浏览器自动探测;ffmpeg 缺失时回退到 imageio-ffmpeg 静态二进制;网络字体被契约禁止。 |
flowchart LR
A["主题"] --> B["调研<br/><i>每个数字带出处</i>"]
B --> C["narration.json<br/><i>竖线切字幕块</i>"]
C --> S["tts_setup.py<br/><i>问 TTS 方案 · 配密钥 · 选音色</i>"]
S --> D["tts_build.py<br/><i>edge-tts / 火山引擎 → MP3 + 词边界</i>"]
D --> E["timeline_build.py<br/><i>全局轴 + 拼接音轨</i>"]
E --> F["subs.py<br/><i>subs.json · srt/vtt · beats.js</i>"]
F --> G["frames/*.html<br/><i>一句一场景,风格取自风格库</i>"]
G --> H{"lint_frames.py<br/>八条契约"}
H -->|不过| G
H -->|通过| I["render_video.mjs<br/><i>逐帧 seek → PNG</i>"]
I --> J["ffmpeg<br/><i>H.264 + AAC 合成</i>"]
J --> K[("out/slug.mp4")]
I --> L["qc_check.py<br/><i>响度 · 漂移 · 抽帧速览</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
B() 节拍系统subs.py 为每个场景生成一份 beats.js,把每块字幕按原文暴露出来,于是画面动画可以直接
锚在吐字时刻上:
tl.fromTo('.card', { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('右边卡片'));
tl.fromTo('.verdict', { scale: 0.8 }, { scale: 1, duration: 0.6 }, B('结论句') + 0.2);
B('块文本') 是「这几个字开始被念出」的时刻,Be('块文本') 是念完的时刻。
这是本项目自己的设计,也是让工作方式变掉的那一块:改一次解说词,重跑三条命令,所有场景自行 重排对时。对比帧号硬编码 —— 改一句话就是整片手工重新对位。
兜底写法是被禁止的。 B('x') || 3.2 会把「节拍查不到」藏在一个过期的数字后面。查不到
就应该让构建直接失败 —— 一条对错了时间的视频,比一次构建失败糟糕得多。
不要对着空白页从零设计画面。 23 种风格、8 个类别,每种都记录了画布、字阶、时间轴结构与
配色纪律 —— 完整目录见 references/style-catalog.md。
涵盖:大胆信号卡 / 奢华极简留白 / NYT 编辑级数据图表 / 瑞士网格 / 故障艺术 / 胶片漏光 / 流体 Hero / 品牌 Logo 收尾 / VFX 文字光标 / 社媒竖版(9:16)/ 产品演示等。
分成两类,改编成本差别很大:
rich(12 个) —— 单文件 + 纯 CSS @keyframes。seek 渲染器零改动即可驱动;改编
只要三步:换系统字体栈、把底部元素抬出字幕带、填进真实内容。gsap(11 个) —— 多 composition + CDN 加载。不要搬代码。 只取它的视觉 DNA,用
CSS keyframes 重新表达。一个项目建议轮换 2–4 种风格。8 个场景共用同一张脸会显得单调。
每条成片产出两张互为姊妹、而非裁切关系的封面。从 16:9 居中裁 3:4,1920px 只剩 810px —— 丢掉 57.8% 的画面宽度,任何横跨全宽的标题都会被切掉一半。所以两张共享同一套视觉基因, 但各自重新排一次版:
| 元素 | 16:9(1920×1080) |
3:4(1440×1080) |
|---|---|---|
| 悖论视觉 | 右侧,左右并置 | 上方,竖排堆叠 |
| 钩子 | 左下,两行,≥96px | 下方,三行,≥120px |
| 每行字数上限 | 7 字 | 5 字 |
两个尺寸的输出都由 new_project.py 直接生成模板,默认出 2 倍图。完整规则与上传前自检清单:
references/cover-guide.md。
| 症状 | 原因 | 处理 |
|---|---|---|
| 每一帧都是静止首帧,且不报错 | 时间轴没注册到 window.__tl;或 page.evaluate 传了字符串而非函数字面量(Playwright 只求值一次,函数体根本不执行) |
传真正的函数字面量 |
| 视频变成 1280×720 | viewport 传给了 browser.launch() —— 它是 context 级选项 |
传给 newPage() |
| 封面出成 |