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 原称两个方法来源项目「均为作者本人作品」, 与事实不符,已改为「他人独立项目」并补充来源链接。
This commit is contained in:
1 parent
6474719119
commit
2581e2c685
13 files changed
+874
-1198
No files matched your search
@@ -1,75 +1,74 @@
|
|||||||
name: Bug report
|
name: Bug 报告
|
||||||
description: Something rendered wrong, failed silently, or crashed.
|
description: 画面渲错了、静默失败,或者直接崩了。
|
||||||
title: "[Bug] "
|
title: "[Bug] "
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for the report. This pipeline fails **silently** by design — a broken
|
感谢反馈。这条流水线是**静默**失败的 —— 一个坏掉的帧通常还是能产出一条看着挺像样的
|
||||||
frame usually still produces a plausible-looking video. The more of the detail
|
视频。下面这些信息你填得越全,定位得越快。
|
||||||
below you can fill in, the faster this gets diagnosed.
|
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: stage
|
id: stage
|
||||||
attributes:
|
attributes:
|
||||||
label: Which stage failed?
|
label: 哪个阶段出错了?
|
||||||
options:
|
options:
|
||||||
- "0 · new_project (scaffold)"
|
- "0 · new_project(建项目)"
|
||||||
- "1 · tts_build (voiceover)"
|
- "1 · tts_build(配音)"
|
||||||
- "2 · timeline_build (global timeline)"
|
- "2 · timeline_build(全局时间轴)"
|
||||||
- "3 · subs (subtitles / beats)"
|
- "3 · subs(字幕 / 节拍)"
|
||||||
- "4 · lint_frames (static audit)"
|
- "4 · lint_frames(静态体检)"
|
||||||
- "5 · render_video (rendering)"
|
- "5 · render_video(渲染)"
|
||||||
- "6 · qc_check (audit)"
|
- "6 · qc_check(体检)"
|
||||||
- "7 · cover_build (covers)"
|
- "7 · cover_build(封面)"
|
||||||
- "setup_env / environment"
|
- "setup_env / 环境问题"
|
||||||
- "Not sure"
|
- "不确定"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
attributes:
|
attributes:
|
||||||
label: What happened?
|
label: 发生了什么?
|
||||||
description: Include the exact command you ran and the full output.
|
description: 请附上你执行的完整命令与完整输出。
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Command: node scripts/render_video.mjs . --preview 30
|
命令:node scripts/render_video.mjs . --preview 30
|
||||||
Expected: 30s preview with animated cards
|
期望:30 秒预览,卡片有动画
|
||||||
Actual: preview renders, but the number counter never moves
|
实际:预览渲出来了,但数字计数器一直不动
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: symptom-class
|
id: symptom-class
|
||||||
attributes:
|
attributes:
|
||||||
label: Did it fail loudly or silently?
|
label: 它是报错了,还是静默出错?
|
||||||
description: >
|
description: >
|
||||||
Silent failures are the interesting ones here. If playback works but the output
|
静默失败才是这里最有价值的一类。如果视频能播但输出是错的
|
||||||
is wrong (frozen element, desynced subtitle, missing text), say so explicitly.
|
(元素卡住、字幕不同步、文字缺失),请明确写出来。
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: environment
|
id: environment
|
||||||
attributes:
|
attributes:
|
||||||
label: Environment
|
label: 环境
|
||||||
description: "Run these and paste the result."
|
description: "跑一下这些,把结果贴上来。"
|
||||||
value: |
|
value: |
|
||||||
- OS:
|
- 操作系统:
|
||||||
- `node -v`:
|
- `node -v`:
|
||||||
- `python -V`:
|
- `python -V`:
|
||||||
- Shell (Git Bash / PowerShell / zsh):
|
- Shell(Git Bash / PowerShell / zsh):
|
||||||
- Browser used for rendering (Chrome / Edge / playwright chromium):
|
- 渲染用浏览器(Chrome / Edge / playwright chromium):
|
||||||
- ffmpeg source (PATH / imageio-ffmpeg):
|
- ffmpeg 来源(PATH / imageio-ffmpeg):
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: 版本
|
||||||
description: The `version:` field in SKILL.md, or the release tag.
|
description: SKILL.md 里的 `version:` 字段,或发布 tag。
|
||||||
placeholder: "1.2.2"
|
placeholder: "1.2.2"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -77,7 +76,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: qc-output
|
id: qc-output
|
||||||
attributes:
|
attributes:
|
||||||
label: QC output (if you got as far as rendering)
|
label: QC 输出(如果你已经跑到渲染那一步)
|
||||||
description: Paste `out/qc_report.md`, or attach the contact sheet.
|
description: 贴 `out/qc_report.md`,或附上抽帧速览图。
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Read the frame contract first
|
- name: 先读画面契约
|
||||||
url: https://github.com/OneMoh/html-explainer/blob/main/references/frame-contract.md
|
url: https://github.com/OneMoh/html-explainer/blob/main/references/frame-contract.md
|
||||||
about: Most "my frame renders but looks wrong" reports are explained by the eight contract rules.
|
about: 绝大多数「我的画面能渲但看着不对」都能被八条契约规则解释。
|
||||||
- name: Check the lessons ledger
|
- name: 先查踩坑台账
|
||||||
url: https://github.com/OneMoh/html-explainer/blob/main/references/lessons.md
|
url: https://github.com/OneMoh/html-explainer/blob/main/references/lessons.md
|
||||||
about: Known silent-failure modes and their diagnoses. Worth a search before filing.
|
about: 已知的静默失败模式及其定位方法。提 issue 之前值得先搜一遍。
|
||||||
- name: Setup and environment issues
|
- name: 安装与环境问题
|
||||||
url: https://github.com/OneMoh/html-explainer/blob/main/README.md#疑难排查
|
url: https://github.com/OneMoh/html-explainer/blob/main/README.md
|
||||||
about: Run `bash setup_env.sh` and compare against the troubleshooting table (English version: README.en.md#troubleshooting).
|
about: 先跑 `bash setup_env.sh`,再对照 README 的「疑难排查」一节。
|
||||||
@@ -1,51 +1,49 @@
|
|||||||
name: Feature request
|
name: 功能建议
|
||||||
description: Propose a new capability, style, or workflow improvement.
|
description: 提议一个新能力、新风格,或流程改进。
|
||||||
title: "[Feature] "
|
title: "[Feature] "
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
This project deliberately keeps a small surface: HTML frames in, MP4 out, with
|
本项目刻意保持很小的接口面:HTML 帧进,MP4 出,音画同步稳得住。会增加构建系统、
|
||||||
audio-video sync that actually holds. Proposals that add a build system, a plugin
|
插件 API 或云依赖的提议,一般都在范围之外 —— 但如果你认为有重要的例外,请说明。
|
||||||
API, or a cloud dependency are usually out of scope — but say so if you think
|
|
||||||
there's an important exception.
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
label: What problem does this solve?
|
label: 它解决什么问题?
|
||||||
description: Describe the situation you're in, not the API you want.
|
description: 描述你处的场景,而不是你想要的 API。
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: proposal
|
id: proposal
|
||||||
attributes:
|
attributes:
|
||||||
label: Proposed approach
|
label: 你建议的做法
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: area
|
id: area
|
||||||
attributes:
|
attributes:
|
||||||
label: Area
|
label: 涉及范围
|
||||||
options:
|
options:
|
||||||
- Visual styles / templates
|
- 画面风格 / 模板
|
||||||
- Narration / TTS
|
- 解说 / TTS
|
||||||
- Subtitles / beat sync
|
- 字幕 / 节拍同步
|
||||||
- Rendering
|
- 渲染
|
||||||
- QC / auditing
|
- QC / 体检
|
||||||
- Covers
|
- 封面
|
||||||
- Docs
|
- 文档
|
||||||
- Other
|
- 其它
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: alternatives
|
id: alternatives
|
||||||
attributes:
|
attributes:
|
||||||
label: Alternatives considered
|
label: 考虑过的替代方案
|
||||||
description: Including "just do it manually" — it's a valid answer.
|
description: 包括「干脆手工做」—— 这也是个有效答案。
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
## What this changes
|
## 这次改了什么
|
||||||
|
|
||||||
<!-- One or two sentences. What was wrong, and what does this do about it? -->
|
<!-- 一两句话。原来错在哪,这个改动做了什么? -->
|
||||||
|
|
||||||
## Why
|
## 为什么要改
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
If this fixes a silent failure, explain what was misbehaving and how you proved the
|
如果这修的是一个静默失败,请说明它原来表现成什么样、你又是怎么证明病因的。
|
||||||
cause. "I tried X, it still broke; I changed Y, it worked" is genuinely useful here —
|
「我试了 X,还是坏的;改了 Y,就好了」在这里是真正有用的信息 ——
|
||||||
this pipeline produces plausible-looking output while being wrong.
|
这条流水线的输出经常看着挺像样,其实是错的。
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## How it was verified
|
## 怎么验证的
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Not "it should work". What did you actually run?
|
不要写「应该能跑」。你实际跑了什么?
|
||||||
e.g. `node scripts/peek_frame.mjs . hook --at 25,50,85` and inspected the 50% frame
|
例如 `node scripts/peek_frame.mjs . hook --at 25,50,85`,并检查了 50% 那一帧
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Checklist
|
## 检查清单
|
||||||
|
|
||||||
- [ ] `python scripts/lint_frames.py --project <project>` reports no FAILs for frames I touched
|
- [ ] `python scripts/lint_frames.py --project <project>` 对我动过的帧没有 FAIL
|
||||||
- [ ] I actually rendered the change and looked at the output
|
- [ ] 我真的渲染过这次改动,并看过输出
|
||||||
- [ ] No external fonts, CDN links, or hardcoded colour literals introduced
|
- [ ] 没有引入外部字体、CDN 链接或硬编码颜色字面量
|
||||||
- [ ] New lesson (if any) appended to `references/lessons.md` with the next number
|
- [ ] 新增的踩坑记录(如果有)已按下一个编号追加到 `references/lessons.md`
|
||||||
- [ ] `python scripts/check_integrity.py` passes
|
- [ ] `python scripts/check_integrity.py` 通过
|
||||||
- [ ] `README.md` and `README.en.md` updated together if user-facing behaviour changed
|
- [ ] 涉及使用者可见行为的改动,`README.md` 与 `README.en.md` 已同步更新
|
||||||
- [ ] `THIRD_PARTY_NOTICES.md` updated if third-party content was added or re-derived
|
- [ ] 如果新增或再衍生了第三方内容,`THIRD_PARTY_NOTICES.md` 已更新
|
||||||
|
|
||||||
## Related issues
|
## 相关 issue
|
||||||
|
|
||||||
<!-- Closes #... -->
|
<!-- Closes #... -->
|
||||||
+11
-13
@@ -7,7 +7,7 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
# Only the latest run per branch matters; cancel superseded ones.
|
# 每个分支只关心最新一次运行;被取代的自动取消。
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -19,8 +19,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Windows + Git Bash is the primary development platform, so it is
|
# Windows + Git Bash 是主要开发平台,所以和 Linux 一起测,
|
||||||
# tested alongside Linux rather than treated as an afterthought.
|
# 而不是被当成事后补的。
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
python: ["3.9", "3.13"]
|
python: ["3.9", "3.13"]
|
||||||
|
|
||||||
@@ -35,27 +35,27 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
|
|
||||||
# --- Repo self-consistency: version drift, catalog drift, stray CDN links ---
|
# --- 仓库自洽性:版本号漂移、风格目录漂移、残留外链 ---
|
||||||
- name: Repository integrity check
|
- name: Repository integrity check
|
||||||
run: python scripts/check_integrity.py
|
run: python scripts/check_integrity.py
|
||||||
|
|
||||||
# --- Python scripts must at least compile on the oldest supported version ---
|
# --- Python 脚本至少在支持的最低版本上要能编译 ---
|
||||||
- name: Compile Python scripts
|
- name: Compile Python scripts
|
||||||
run: python -m compileall -q scripts
|
run: python -m compileall -q scripts
|
||||||
|
|
||||||
# --- Frame audit tool must be importable and its CLI must respond ---
|
# --- 画面体检工具要能被导入,且命令行要能响应 ---
|
||||||
- name: Lint tool smoke test
|
- name: Lint tool smoke test
|
||||||
run: python scripts/lint_frames.py --help
|
run: python scripts/lint_frames.py --help
|
||||||
|
|
||||||
- name: Integrity checker smoke test
|
- name: Integrity checker smoke test
|
||||||
run: python scripts/check_integrity.py --root .
|
run: python scripts/check_integrity.py --root .
|
||||||
|
|
||||||
# --- Shell entrypoint syntax ---
|
# --- Shell 入口语法 ---
|
||||||
- name: Check setup_env.sh syntax
|
- name: Check setup_env.sh syntax
|
||||||
shell: bash
|
shell: bash
|
||||||
run: bash -n setup_env.sh
|
run: bash -n setup_env.sh
|
||||||
|
|
||||||
# --- Renderer / cover builder / peek tool syntax ---
|
# --- 渲染器 / 封面器 / 单帧速览工具的语法 ---
|
||||||
- name: Check Node module syntax
|
- name: Check Node module syntax
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -69,11 +69,9 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
# Frames must never reach out to a CDN or web font: offline rendering
|
# 画面帧绝不允许引 CDN 或网络字体:离线渲染不会报错,只会静默劣化。
|
||||||
# degrades silently rather than failing.
|
# 只扫 HTML —— markdown 文档里*提到*这些 URL 是合理的
|
||||||
# Scope to HTML only — markdown docs legitimately *mention* these URLs
|
# (比如正是指南在教你要把它们删掉),而 GSAP 自带的 README 也会介绍它的 CDN 用法。
|
||||||
# (e.g. the guide explaining that you must delete them), and GSAP's own
|
|
||||||
# bundled README documents its CDN option.
|
|
||||||
hits=$(grep -rInE --include='*.html' \
|
hits=$(grep -rInE --include='*.html' \
|
||||||
'https?://[^"'"'"' ]*(fonts\.googleapis|fonts\.gstatic|cdn\.)' . \
|
'https?://[^"'"'"' ]*(fonts\.googleapis|fonts\.gstatic|cdn\.)' . \
|
||||||
|| true)
|
|| true)
|
||||||
|
|||||||
+81
-105
@@ -1,142 +1,118 @@
|
|||||||
# Changelog
|
# 变更日志
|
||||||
|
|
||||||
All notable changes to `html-explainer` are documented here.
|
`html-explainer` 的所有重要变更都记录在这里。
|
||||||
|
|
||||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。
|
||||||
|
|
||||||
Versions before `1.2.2` were private, internal releases. `1.2.2` is the first public
|
`1.2.2` 之前的版本都是私有内部发布。**`1.2.2` 是 GitHub 上的首个公开版本。**
|
||||||
release on GitHub.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [1.2.2] — 2026-09-21 — first public release
|
## [1.2.2] — 2026-09-21 — 首个公开版本
|
||||||
|
|
||||||
### Added
|
### 新增
|
||||||
|
|
||||||
- **`scripts/lint_frames.py`** — static pre-render audit of every scene frame. Checks the
|
- **`scripts/lint_frames.py`** —— 渲染前的画面静态体检。在花时间渲染整片之前,先检查八条
|
||||||
eight frame-contract rules plus known pitfalls before spending time on a full render:
|
画面契约与已知坑:外部字体 / CDN 引用、硬编码颜色字面量、缺失 `window.__tl` 注册、
|
||||||
external fonts / CDN links, hardcoded colour literals, missing `window.__tl`
|
CDN 版 GSAP、墙钟逻辑(`setInterval`、`requestAnimationFrame` 计数)、CSS `transition`
|
||||||
registration, CDN GSAP, wall-clock logic (`setInterval`, `requestAnimationFrame`
|
入场、`B('…') || 数字` 兜底写法、侵入字幕禁区的内容、缺失中文字体族。
|
||||||
counters), CSS `transition` entrances, `B('…') || number` fallbacks, content intruding
|
- **`scripts/peek_frame.mjs`** —— 单帧速览。把一个场景 seek 到若干时点截图,耗时是**秒级**,
|
||||||
into the subtitle safe zone, and missing CJK font families.
|
于是不用渲全片就能判断画面设计。`--at` 收的是百分比,不是帧号。
|
||||||
- **`scripts/peek_frame.mjs`** — single-frame viewer. Seeks one scene to a few time points
|
- **双语项目 README**(`README.md` 中文为默认,`README.en.md` 英文)。
|
||||||
and screenshots them in seconds, so frame design can be judged without rendering the
|
- **开源治理文件**:`LICENSE`(MIT)、`THIRD_PARTY_NOTICES.md`、`CHANGELOG.md`、
|
||||||
whole video. `--at` takes percentages, not frame numbers.
|
`CONTRIBUTING.md`、`licenses/Apache-2.0.txt`、`.gitignore`、`.gitattributes`、
|
||||||
- **Bilingual project README** (`README.md` in Chinese, `README.en.md` in English).
|
GitHub issue / PR 模板,以及 CI 工作流。
|
||||||
- **Open-source governance files**: `LICENSE` (MIT), `THIRD_PARTY_NOTICES.md`,
|
|
||||||
`CHANGELOG.md`, `CONTRIBUTING.md`, `licenses/Apache-2.0.txt`, `.gitignore`,
|
|
||||||
`.gitattributes`, GitHub issue/PR templates, and a CI workflow.
|
|
||||||
|
|
||||||
### Fixed
|
### 修复
|
||||||
|
|
||||||
- **`speech_end_sec` disagreed between a cold run and a cached re-run** (lessons #31).
|
- **`speech_end_sec` 在冷跑与缓存重跑之间不一致**(lessons #31)。`_result()` 量的是已经裁过
|
||||||
`_result()` measured the already-trimmed output file, so the head-trim amount was
|
静音的输出文件,于是头部裁剪量被减了两次:冷跑报 `7.541s`,缓存重跑报 `7.696s` ——
|
||||||
subtracted twice: a cold run reported `7.541s`, a cached re-run reported `7.696s` —
|
0.155s 的差异破坏了「两次渲染逐帧一致」的保证,并让末块字幕提前消失。现在统一存**裁后
|
||||||
a 0.155s discrepancy that broke the "two runs are frame-identical" guarantee and made
|
坐标系**下的值。
|
||||||
the final subtitle block disappear early. Now stored in post-trim coordinates.
|
|
||||||
|
|
||||||
### Changed
|
### 变更
|
||||||
|
|
||||||
- Documentation: `scripts/lint_frames.py` and `scripts/peek_frame.mjs` added to the
|
- 文档:`scripts/lint_frames.py` 与 `scripts/peek_frame.mjs` 补进命令序列与文件表
|
||||||
command sequence and the file reference table (both previously undocumented).
|
(两者此前都没有被文档记录)。
|
||||||
- Documentation: the lint stage is now an explicit step in the workflow, between subtitle
|
- 文档:lint 现在是工作流里的显式阶段,位置在生成字幕之后、渲染之前。
|
||||||
generation and rendering.
|
- 文档:删除对私有客户项目的引用,使技能可以公开发布。
|
||||||
- Documentation: removed references to private client projects so the skill can ship
|
- 许可:风格目录衍生自 Apache-2.0 内容,现已带上完整的来源署名。
|
||||||
publicly.
|
|
||||||
- Documented licensing: the style catalog is derived from Apache-2.0 content and now
|
|
||||||
carries full upstream attribution.
|
|
||||||
|
|
||||||
### Notes
|
### 备注
|
||||||
|
|
||||||
- Backfilled from field work: preview-render frame deletion (`--keep-frames`), the
|
- 从实战回填:预览渲染会清空帧目录(`--keep-frames`)、重渲单个场景的临时项目法、
|
||||||
temporary-project technique for re-rendering a single scene, and the cold-open rule
|
以及「开场 0.5 秒内就必须出现主角」的冷开场规则(lessons #32–#34)。
|
||||||
that a video's first 0.5s must already show its subject (lessons #32–#34).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [1.2.1] — 2026-09-20
|
## [1.2.1] — 2026-09-20
|
||||||
|
|
||||||
Four rendering / QC correctness fixes, all found by instrumenting the renderer rather
|
四个渲染 / QC 正确性修复,全部是靠给渲染器加探针找出来的,而不是靠肉眼看。
|
||||||
than by inspection. See `references/lessons.md` #27–#30.
|
详见 `references/lessons.md` #27–#30。
|
||||||
|
|
||||||
### Fixed
|
### 修复
|
||||||
|
|
||||||
- **`tl.pause(t)` silently suppressed seek callbacks.** GSAP's signature is
|
- **`tl.pause(t)` 会静默吞掉本次 seek 的回调。** GSAP 的签名是
|
||||||
`pause(atTime, suppressEvents)` and the second parameter defaults to `true`, so any
|
`pause(atTime, suppressEvents)`,第二参默认为 `true`,于是 seek 触发的任何
|
||||||
`onUpdate` / `onStart` / `onComplete` fired by a seek was dropped. A counter animated
|
`onUpdate` / `onStart` / `onComplete` 都被丢弃。用 `onUpdate` 写的计数器因此在成片里恒为
|
||||||
via `onUpdate` therefore sat frozen at its initial value in the output — no error, no
|
初值 —— 不报错、不告警、缩略图尺寸正确,静态检查也查不出来。渲染器与封面器现在调用
|
||||||
warning, correct thumbnail size, and undetectable by static checks. The renderer and
|
`tl.pause(t, false)`。文档同时把数字滚动引导向**纯 transform 的「数字卷轴」**,
|
||||||
cover builder now call `tl.pause(t, false)`. The docs additionally steer number
|
完全不依赖任何回调。
|
||||||
animations toward a pure-transform "digit strip" that depends on no callback at all.
|
- **传给 `chromium.launch()` 的 `deviceScaleFactor` 被静默忽略。** `viewport` 与
|
||||||
- **`deviceScaleFactor` passed to `chromium.launch()` was silently ignored.** Both
|
`deviceScaleFactor` 都是 context 级选项,只在 `newPage()` 上生效。封面实际写成了 1 倍图,
|
||||||
`viewport` and `deviceScaleFactor` are context-level options and only take effect on
|
而日志声称 2 倍。现在构建用 PIL 核验输出尺寸,不再信日志。
|
||||||
`newPage()`. Covers were being written at 1× while the log claimed 2×. The build now
|
- **QC 抽样只覆盖了视频前四分之一。** `qc_check.py` 取字幕块起始时间后写成 `pts[:samples]`,
|
||||||
verifies output dimensions with PIL instead of trusting the log.
|
拿的是**前 N 块**而不是均匀铺开。一条 39 块的视频抽了 12 块,只覆盖 0.25–23.86s,后 6 个
|
||||||
- **QC sampling only covered the first quarter of the video.** `qc_check.py` sampled
|
场景一帧没抽,却报告「全部通过」。现在按步长在全片范围均匀抽样。
|
||||||
subtitle-block start times but sliced `pts[:samples]`, taking the first N blocks rather
|
- **封面排版是在动画中途量的。** 校验紧接页面加载后就跑,读的是 `fromTo` 起始值而不是稳定
|
||||||
than a spread. A 39-block video sampled 12 blocks covering only 0.25–23.86s, leaving six
|
终态,于是报出幽灵偏移。现在测量前先置 `__MG_RENDER__` 并 seek 到 `tl.duration()`,
|
||||||
late scenes completely unchecked while reporting "all passed". Sampling now steps evenly
|
与封面渲染器完全一致。
|
||||||
across the full range.
|
|
||||||
- **Cover layout was being measured mid-animation.** Verification ran straight after page
|
|
||||||
load, so it read `fromTo` start values instead of the settled layout and reported
|
|
||||||
phantom offsets. Measurement now sets `__MG_RENDER__` and seeks to `tl.duration()`
|
|
||||||
first, matching the cover builder exactly.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [1.2.0] — 2026-09-20
|
## [1.2.0] — 2026-09-20
|
||||||
|
|
||||||
### Added
|
### 新增
|
||||||
|
|
||||||
- **Dual-cover system.** Every finished video now produces two independently laid-out
|
- **封面双方案。** 每条成片现在产出两张独立排版的封面而不是一张:`cover_169.png`
|
||||||
covers instead of one: `cover_169.png` (1920×1080, 3840×2160 output) for the feed and
|
(1920×1080,输出 3840×2160)用于信息流与播放器,`cover_34.png`(1440×1080,
|
||||||
player, and `cover_34.png` (1440×1080, 2880×2160 output) for profile grids.
|
输出 2880×2160)用于主页栅格。
|
||||||
- `scripts/cover_build.mjs` — cover renderer reusing the scene renderer's deterministic
|
- `scripts/cover_build.mjs` —— 封面渲染器,复用场景渲染器的确定性 seek 内核,但有三处
|
||||||
seek core, with three deliberate differences: no subtitle layer, a single seek to one
|
刻意不同:不带字幕层、只 seek 到单个「封面时刻」(默认 `--at last`)而不是逐帧步进、
|
||||||
"cover moment" (`--at last` by default) rather than per-frame stepping, and 2× output
|
以及默认 2 倍输出。
|
||||||
by default.
|
- `assets/cover-template.html` —— 封面模板,三个必需元素写在文件头注释里。
|
||||||
- `assets/cover-template.html` — cover template with the three required elements
|
- `references/cover-guide.md` —— 重排对照表、尺寸规则、上传策略与自检清单。
|
||||||
documented in its header.
|
- `scripts/new_project.py` 现在会把两份封面 HTML 一起生成到新项目里。
|
||||||
- `references/cover-guide.md` — re-layout comparison table, sizing rules, upload
|
|
||||||
strategy, and a self-check list.
|
|
||||||
- `scripts/new_project.py` now scaffolds both cover HTML files into a new project.
|
|
||||||
|
|
||||||
### Notes
|
### 备注
|
||||||
|
|
||||||
- The two covers are **sibling layouts sharing one visual DNA, never a crop pair**.
|
- 两张封面是**共享同一套视觉基因的姊妹版式,绝不是一对裁切**。从 16:9 裁 3:4 会丢掉
|
||||||
Cropping 3:4 out of 16:9 discards 57.8% of the frame width (1920 → 810px), which
|
57.8% 的画面宽度(1920 → 810px),必然把大字钩子切掉。重排改的是构型本身:悖论视觉
|
||||||
guarantees a headline hook gets cut. The re-layout changes the composition itself:
|
从「右侧左右并置」变成「上方竖排堆叠」,钩子从两行变成三行。
|
||||||
the paradox visual moves from side-by-side on the right to a vertical stack on top,
|
|
||||||
and the hook goes from two lines to three.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [1.1.0] — 2026-09-20 — first packaged release
|
## [1.1.0] — 2026-09-20 — 首个打包发布
|
||||||
|
|
||||||
### Added
|
### 新增
|
||||||
|
|
||||||
- Core pipeline: `new_project.py` → `tts_build.py` → `timeline_build.py` → `subs.py` →
|
- 核心流水线:`new_project.py` → `tts_build.py` → `timeline_build.py` → `subs.py` →
|
||||||
`render_video.mjs` → `qc_check.py`.
|
`render_video.mjs` → `qc_check.py`。
|
||||||
- **Deterministic seek renderer** (`render_video.mjs`). Instead of screen-recording a
|
- **确定性 seek 渲染器**(`render_video.mjs`)。不录活页面的屏,而是每一帧都靠 seek
|
||||||
live page, every frame is produced by seeking the GSAP timeline (`tl.pause(t, false)`)
|
GSAP 时间轴(`tl.pause(t, false)`)并同步 CSS 动画
|
||||||
and synchronising CSS animations (`document.getAnimations().currentTime = t*1000`)
|
(`document.getAnimations().currentTime = t*1000`)后再截图。这消掉了实时录制的一整类
|
||||||
before screenshotting. This eliminates the entire class of live-recording failures:
|
失败:没有起播竞态、没有字体加载超时、没有不稳定的引导期。
|
||||||
no playback-start race, no font-load timeout, no unstable lead-in.
|
- **`B('块文本')` 节拍同步。** 画面动画按**匹配字幕块文本**定位,而不是写死帧号,于是改解说词
|
||||||
- **`B('block text')` beat sync.** Frame animations are positioned by matching subtitle
|
会重排全片时序,而一个画面文件都不用动。
|
||||||
block text rather than hardcoded frame numbers, so editing narration re-times the whole
|
- `scripts/subs.py` —— 字幕三出口(运行时 JSON、SRT/VTT 外挂、渲染器注入的画面内层),
|
||||||
video without touching a single frame file.
|
外加每场景生成一份 `beats.js`。
|
||||||
- `scripts/subs.py` — three subtitle outputs (runtime JSON, SRT/VTT sidecars, and the
|
- `scripts/make_theme.py` —— 四个预设 + 按主题词推色,写出唯一颜色来源 `theme.css`。
|
||||||
in-frame layer injected by the renderer) plus generated `beats.js` per scene.
|
- `scripts/qc_check.py` —— 流 / 时长 / 响度 / 抽帧体检,附抽帧速览图。
|
||||||
- `scripts/make_theme.py` — four presets plus topic-word colour derivation, writing a
|
- `scripts/import_styles.py` —— 移植期一次性工具,把模板设计规范转写成
|
||||||
single `theme.css` source of truth for all colours.
|
`references/style-catalog.md` / `.json`。
|
||||||
- `scripts/qc_check.py` — stream, duration, loudness, and sampled-frame auditing with a
|
- 23 种画面风格目录,8 个类别,按改编成本分类。
|
||||||
contact sheet.
|
- `setup_env.sh` 做首次环境自检,`--install` 装缺失依赖;`package_skill.py` 打可移植 zip。
|
||||||
- `scripts/import_styles.py` — one-time porting tool that transcribes template design
|
|
||||||
specifications into `references/style-catalog.md` / `.json`.
|
|
||||||
- 23-style visual catalog across 8 categories, classified by adaptation cost.
|
|
||||||
- `setup_env.sh` for first-time environment checks and `--install` for fetching missing
|
|
||||||
dependencies; `package_skill.py` for portable zips.
|
|
||||||
|
|
||||||
[1.2.2]: https://github.com/OneMoh/html-explainer/releases/tag/v1.2.2
|
[1.2.2]: https://github.com/OneMoh/html-explainer/releases/tag/v1.2.2
|
||||||
[1.2.1]: https://github.com/OneMoh/html-explainer/releases/tag/v1.2.1
|
[1.2.1]: https://github.com/OneMoh/html-explainer/releases/tag/v1.2.1
|
||||||
|
|||||||
+75
-93
@@ -1,44 +1,42 @@
|
|||||||
# Contributing to html-explainer
|
# 参与贡献
|
||||||
|
|
||||||
Thanks for taking the time to contribute. This document covers the practical things:
|
感谢你愿意花时间。本文只说实际的东西:怎么把环境跑起来、项目的硬性规则有哪些、
|
||||||
how to get a working environment, what the project's hard rules are, and where different
|
以及不同类型的贡献该往哪儿放。
|
||||||
kinds of contributions actually belong.
|
|
||||||
|
|
||||||
## Ways to contribute
|
## 可以贡献什么
|
||||||
|
|
||||||
| Kind of contribution | Where it goes |
|
| 贡献类型 | 该往哪儿放 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Bug report | [Open an issue](https://github.com/OneMoh/html-explainer/issues/new/choose) using the bug template |
|
| 报告 Bug | 用 bug 模板[开一个 issue](https://github.com/OneMoh/html-explainer/issues/new/choose) |
|
||||||
| Rendering / sync bug you diagnosed | Add a numbered entry to `references/lessons.md` **and** fix the code |
|
| 你已定位的渲染 / 同步问题 | 在 `references/lessons.md` 追加一条编号记录,**并且**修代码 |
|
||||||
| New visual style or a better adaptation recipe | `references/style-catalog.md` notes + a documented example |
|
| 新的画面风格,或更好的改编配方 | 更新 `references/style-catalog.md`,并附一个可运行的示例 |
|
||||||
| Documentation fix | Direct PR — typo fixes need no prior discussion |
|
| 文档修正 | 直接提 PR —— 改错别字不需要先讨论 |
|
||||||
| New feature | Open an issue first so the approach can be agreed before you write it |
|
| 新功能 | 先开 issue 把方案谈拢,再动手写 |
|
||||||
| Platform-specific fix (macOS / Linux paths, shell variants) | Very welcome — the project is developed on Windows + Git Bash |
|
| 平台相关修复(macOS / Linux 路径、其它 shell) | 非常欢迎 —— 本项目在 Windows + Git Bash 上开发 |
|
||||||
|
|
||||||
## Development setup
|
## 开发环境
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/OneMoh/html-explainer.git
|
git clone https://github.com/OneMoh/html-explainer.git
|
||||||
cd html-explainer
|
cd html-explainer
|
||||||
|
|
||||||
# Check the environment, then install anything missing
|
# 先自检,再装缺的东西
|
||||||
bash setup_env.sh # report only
|
bash setup_env.sh # 只报告
|
||||||
bash setup_env.sh --install # fetch missing dependencies
|
bash setup_env.sh --install # 安装缺失依赖
|
||||||
|
|
||||||
# Confirm the toolchain is healthy
|
# 确认工具链正常
|
||||||
node -v && python -V
|
node -v && python -V
|
||||||
```
|
```
|
||||||
|
|
||||||
Requirements: Python ≥ 3.9, Node ≥ 18, and either Chrome or Edge (both are almost always
|
要求:Python ≥ 3.9、Node ≥ 18,以及 Chrome 或 Edge(两者几乎总是已经装好)。
|
||||||
already present). `ffmpeg` is used from `PATH` or falls back to the static binary bundled
|
`ffmpeg` 优先从 `PATH` 取,取不到则回退到 `imageio-ffmpeg` 自带的静态二进制。
|
||||||
with `imageio-ffmpeg`.
|
|
||||||
|
|
||||||
### Running an end-to-end check
|
### 跑一次端到端自检
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/new_project.py demo --topic "人工智能"
|
python scripts/new_project.py demo --topic "人工智能"
|
||||||
cd demo
|
cd demo
|
||||||
# fill narration.json, write frames/*.html, set project.json order
|
# 填 narration.json、写 frames/*.html、设置 project.json 的 order
|
||||||
python ../scripts/tts_build.py --project .
|
python ../scripts/tts_build.py --project .
|
||||||
python ../scripts/timeline_build.py --project .
|
python ../scripts/timeline_build.py --project .
|
||||||
python ../scripts/subs.py --project .
|
python ../scripts/subs.py --project .
|
||||||
@@ -46,95 +44,79 @@ python ../scripts/lint_frames.py --project .
|
|||||||
node ../scripts/render_video.mjs . --preview 30
|
node ../scripts/render_video.mjs . --preview 30
|
||||||
```
|
```
|
||||||
|
|
||||||
If all of that completes and `out/preview.mp4` plays with synchronised audio, your
|
如果这些全部跑完、`out/preview.mp4` 播放时音画同步,你的环境就没问题。
|
||||||
environment is good.
|
|
||||||
|
|
||||||
## The project's hard rules
|
## 项目的硬性规则
|
||||||
|
|
||||||
These are not style preferences. Breaking them produces silent failures — a video that
|
这些不是风格偏好。违反它们会产生**静默失败** —— 视频能渲出来,但是错的。凡是能自动检查的,
|
||||||
renders but is wrong. They are enforced by `scripts/lint_frames.py` where possible.
|
都由 `scripts/lint_frames.py` 强制。
|
||||||
|
|
||||||
1. **No external fonts or CDN resources in any frame.** Web fonts are unreachable from
|
1. **任何画面帧里都不许引外部字体或 CDN 资源。** 网络字体在很多网络下不可达,而且超时是
|
||||||
many networks and time out silently. Use system font stacks only, and reference GSAP
|
静默的。只用系统字体栈,GSAP 一律从随包的 `../assets/gsap.min.js` 引用。
|
||||||
from the bundled `../assets/gsap.min.js`.
|
2. **颜色只能取 `theme.css` 的 CSS 变量。** 画面代码里不许出现十六进制字面量 —— 这正是
|
||||||
2. **Colours come from `theme.css` CSS variables only.** No hex literals in frame code.
|
「换主题只改一个文件」得以成立的原因。
|
||||||
This is what makes a theme change a one-file operation.
|
3. **不许有墙钟逻辑。** `setInterval` 与 `requestAnimationFrame` 计数在确定性 seek 下不可能
|
||||||
3. **No wall-clock logic.** `setInterval` and `requestAnimationFrame` counters cannot
|
工作。动画用 GSAP 或 CSS `@keyframes`。
|
||||||
work under deterministic seeking. Animate with GSAP or CSS `@keyframes`.
|
4. **动画用 GSAP 时间轴,并注册到 `window.__tl`。** 渲染器每帧 seek 这个时间轴;没有它,
|
||||||
4. **Animate with GSAP timelines, registered on `window.__tl`.** The renderer seeks this
|
什么都不动。
|
||||||
timeline once per frame; without it, nothing animates.
|
5. **动画定位用 `B('块文本')`,绝不用写死的时间。** 兜底写法 `B('x') || 3.2` 是被禁止的 ——
|
||||||
5. **Position animations with `B('block text')`, never hardcoded times.** The fallback
|
块查不到就应该在构建时大声失败,而不是静默复用一具过期的数字。
|
||||||
form `B('x') || 3.2` is forbidden — a missing block should fail loudly at build time,
|
6. **保持字幕禁区干净。** 底部 80–170px 不许有任何内容渲染,入场路径也不许穿过它。
|
||||||
not silently reuse a stale number.
|
7. **项目目录与输出文件一律用 ASCII 路径。** 非 ASCII 路径会让 Windows 上的 ffmpeg 出错。
|
||||||
6. **Keep the subtitle safe zone clear.** Nothing may render in the bottom 80–170px, and
|
项目名与输出名保持 ASCII,要展示用的名字以后再改。
|
||||||
entrance paths must not cross it.
|
|
||||||
7. **Use ASCII paths for all project directories and output files.** Non-ASCII paths break
|
|
||||||
ffmpeg on Windows. Project and output names are ASCII; rename for presentation later.
|
|
||||||
|
|
||||||
The full contract, with the reasoning behind each rule and the counter-examples that get
|
完整契约(每条规则背后的理由,以及会被打回的反例)见
|
||||||
a frame rejected, is in [`references/frame-contract.md`](references/frame-contract.md).
|
[`references/frame-contract.md`](references/frame-contract.md)。
|
||||||
|
|
||||||
## Adding a new lesson
|
## 追加一条踩坑记录
|
||||||
|
|
||||||
`references/lessons.md` is the most valuable file in this repository. It exists because
|
`references/lessons.md` 是这个仓库里最值钱的文件。它之所以存在,是因为这条流水线的失败方式
|
||||||
this kind of pipeline fails *silently* — a bug produces a plausible-looking video rather
|
是**静默**的 —— 出问题不会报错,只会产出一条看着挺像样的视频。
|
||||||
than an error.
|
|
||||||
|
|
||||||
A good entry contains, in this order:
|
一条好的记录按这个顺序写:
|
||||||
|
|
||||||
1. **The symptom**, as you first observed it — including how it misled you.
|
1. **症状**,就按你最初观察到的样子 —— 包括它是怎么把你带偏的。
|
||||||
2. **The diagnosis**, and the evidence that proved it (a probe, a diff, a measurement).
|
2. **定位过程**,以及证明病因的证据(探针、diff、测量值)。
|
||||||
3. **The fix**, and what to check afterwards to confirm it.
|
3. **修法**,以及修完该检查什么来确认。
|
||||||
4. **The general rule** this implies, if it implies one.
|
4. **由此得出的通用规则**(如果确实能推出的话)。
|
||||||
|
|
||||||
Number entries sequentially and append. Do not renumber existing entries — they are
|
编号顺次追加。**不要给已有条目重新编号** —— 代码注释里在引用这些编号。
|
||||||
referenced from code comments.
|
|
||||||
|
|
||||||
## Adding a new visual style
|
## 追加一种画面风格
|
||||||
|
|
||||||
1. Record the specification in `references/style-catalog.md` (canvas, type scale, timeline
|
1. 在 `references/style-catalog.md` 与 `references/style-catalog.json` 里记录规范
|
||||||
structure, colour discipline) and `references/style-catalog.json`.
|
(画布、字阶、时间轴结构、配色纪律)。
|
||||||
2. Classify it as `rich` (single file, CSS `@keyframes` timeline — drivable by the seek
|
2. 分类为 `rich`(单文件,CSS `@keyframes` 时间轴 —— seek 渲染器零改动可驱动)或 `gsap`
|
||||||
renderer with no changes) or `gsap` (multi-composition — must be re-expressed, not
|
(多 composition —— 必须重写表达,不能照搬)。
|
||||||
copied).
|
3. 如果这种风格需要超出标准三步(换字体栈、把底部元素抬出字幕带、填真实内容)之外的
|
||||||
3. Add an adaptation recipe to `references/template-guide.md` if the style needs anything
|
处理,在 `references/template-guide.md` 里补一条改编配方。
|
||||||
beyond the standard three steps (swap the font stack, lift bottom elements out of the
|
4. 验证它能渲出来:`node scripts/peek_frame.mjs <project> <id> --at 25,50,85`。
|
||||||
subtitle zone, fill in real content).
|
|
||||||
4. Verify it renders: `node scripts/peek_frame.mjs <project> <id> --at 25,50,85`.
|
|
||||||
|
|
||||||
Style identifiers are `kebab-case` and match the pattern already in use (`frame-*`,
|
风格标识符用 `kebab-case`,并沿用已有的命名模式(`frame-*`、`vfx-*`)。
|
||||||
`vfx-*`).
|
|
||||||
|
|
||||||
## Pull request checklist
|
## PR 检查清单
|
||||||
|
|
||||||
Before opening a PR, please confirm:
|
提 PR 之前请确认:
|
||||||
|
|
||||||
- [ ] `python scripts/lint_frames.py --project <project>` reports no FAILs for any frame
|
- [ ] `python scripts/lint_frames.py --project <project>` 对你动过的每一帧都没有 FAIL
|
||||||
you touched
|
- [ ] 你真的渲染过你的改动。画面代码「看着对」经常是不对的 —— seek 渲染有自己的失败模式
|
||||||
- [ ] You have actually rendered your change. Frame code that "looks right" frequently
|
- [ ] 新增的踩坑记录已带编号写进 `references/lessons.md`
|
||||||
is not — seek-based rendering has its own failure modes
|
- [ ] 没有引入外部字体、CDN 引用或硬编码颜色
|
||||||
- [ ] Any new lesson is added to `references/lessons.md` with a number
|
- [ ] 如果有再分发或再衍生的第三方内容,`THIRD_PARTY_NOTICES.md` 已更新
|
||||||
- [ ] No external fonts, CDN references, or hardcoded colours were introduced
|
- [ ] 涉及使用者可见行为的改动,`README.md` 与 `README.en.md` 已同步更新
|
||||||
- [ ] If you redistributed or re-derived third-party content, `THIRD_PARTY_NOTICES.md`
|
|
||||||
is updated
|
|
||||||
- [ ] `README.md` and `README.en.md` are updated together if user-facing behaviour
|
|
||||||
changed
|
|
||||||
|
|
||||||
## Commit messages
|
## 提交信息
|
||||||
|
|
||||||
Conventional Commits are preferred (`fix:`, `feat:`, `docs:`, `chore:`). A body is
|
建议使用 Conventional Commits(`fix:`、`feat:`、`docs:`、`chore:`)。如果这次改动修的是一个
|
||||||
encouraged when the change fixes a silent failure — explain what was misbehaving and how
|
静默失败,请写正文说明它原来错在哪、你是怎么验证修好了的。
|
||||||
you verified the fix.
|
|
||||||
|
|
||||||
## Scope note
|
## 范围说明
|
||||||
|
|
||||||
This project intentionally does **not** aim to be a framework. It is a pipeline with a
|
本项目刻意**不**打算成为一个框架。它是一条流水线,接口很小、很无聊:HTML 帧进,MP4 出,
|
||||||
small, boring surface: HTML frames in, MP4 out, with audio-video sync that holds. Features
|
音画同步稳得住。会增加构建系统、插件 API,或对云服务产生运行时依赖的功能,一般都在范围之外
|
||||||
that add a build system, a plugin API, or a runtime dependency on a cloud service are
|
—— 如果你认为有重要的例外,开个 issue 聊聊。
|
||||||
generally out of scope — say hello in an issue if you think there's an important exception.
|
|
||||||
|
|
||||||
## License
|
## 许可
|
||||||
|
|
||||||
By contributing, you agree that your contributions are licensed under the MIT License
|
提交贡献即表示你同意你的贡献以 MIT 许可授权(见 [LICENSE](LICENSE)),并确认你有权提交它们。
|
||||||
(see [LICENSE](LICENSE)), and you confirm you have the right to submit them. If you are
|
如果你从别的项目移植了内容,请在同一个 PR 里把它写进 `THIRD_PARTY_NOTICES.md`。
|
||||||
porting content from another project, add it to `THIRD_PARTY_NOTICES.md` in the same PR.
|
|
||||||
+261
-423
@@ -2,17 +2,19 @@
|
|||||||
|
|
||||||
# html-explainer
|
# html-explainer
|
||||||
|
|
||||||
**Turn a topic into a narrated, subtitled, fully-covered explainer video — locally, from HTML, with audio-video sync that actually holds.**
|
**An Agent Skill: give it a topic, get back a narrated explainer video with subtitles and covers.**
|
||||||
|
|
||||||
A deterministic HTML → MP4 pipeline for AI coding agents.
|
Write scenes in HTML → deterministic frame-by-frame rendering → a real MP4. Everything runs
|
||||||
No Remotion. No cloud render queue. No per-render fees. No API keys for the core path.
|
locally; the core pipeline needs no API key and charges no per-render fee.
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](CHANGELOG.md)
|
[](CHANGELOG.md)
|
||||||
[](https://python.org)
|
[](SKILL.md)
|
||||||
[](https://nodejs.org)
|
[](#install)
|
||||||
[](#requirements)
|
[](#install)
|
||||||
[](CONTRIBUTING.md)
|
[](#install)
|
||||||
|
[](#requirements)
|
||||||
|
[](#requirements)
|
||||||
|
|
||||||
[简体中文](README.md) · [English](README.en.md)
|
[简体中文](README.md) · [English](README.en.md)
|
||||||
|
|
||||||
@@ -22,47 +24,143 @@ No Remotion. No cloud render queue. No per-render fees. No API keys for the core
|
|||||||
|
|
||||||
## What it is
|
## What it is
|
||||||
|
|
||||||
`html-explainer` produces explainer and knowledge videos — the kind with a voiceover, hard
|
`html-explainer` turns a topic into a **narrated explainer video with burned-in subtitles and a
|
||||||
subtitles, a progress bar, and motion-graphics scenes built in HTML/CSS/GSAP.
|
progress bar**, with the visuals written in HTML/CSS/GSAP.
|
||||||
|
|
||||||
You write a narration script. An agent (or you) picks a visual style from a library of 23,
|
**It is an Agent Skill** (`SKILL.md` + `scripts/` + `references/`), following the
|
||||||
writes one self-contained HTML file per beat, and this pipeline renders it to a real MP4
|
[Agent Skills](https://code.claude.com/docs/en/skills) convention, so Claude Code, OpenAI Codex,
|
||||||
with frame-accurate subtitle timing and two platform-ready covers.
|
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.
|
||||||
|
|
||||||
Everything runs on your machine. The only network calls are the TTS request and, optionally,
|
You say one sentence:
|
||||||
web research for the script.
|
|
||||||
|
|
||||||
It is packaged as an **Agent Skill** (`SKILL.md` + `scripts/` + `references/`), so it drops
|
> Make a 3-minute explainer video about why the sky is blue.
|
||||||
into Claude Code, WorkBuddy, Cursor, or any agent that can read a skill file — but the
|
|
||||||
scripts themselves are plain Python and Node, and run perfectly well by hand.
|
The skill then walks the agent through: research → script → voiceover → subtitles and beats →
|
||||||
|
scene authoring in one of 23 styles → render → QC → covers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The core idea: deterministic seeking instead of screen recording
|
## Install
|
||||||
|
|
||||||
Most "HTML to video" tools launch a browser, play the animation, and screen-record the
|
### One-line install (recommended)
|
||||||
result. That approach is the source of an entire family of bugs that are maddening because
|
|
||||||
they are *intermittent*: the animation starts before recording does, a web font resolves late
|
Send this to your agent:
|
||||||
so half the video uses the wrong typeface, the first few frames capture a pre-animation
|
|
||||||
|
```text
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
state.
|
||||||
|
|
||||||
`html-explainer` does not record. It **seeks**.
|
`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.
|
||||||
|
|
||||||
For every single frame it:
|
The trade-off: **every animation must be seekable.** Wall-clock animation (`setInterval`,
|
||||||
|
`requestAnimationFrame` counters, CSS `transition` entrances) cannot work and is rejected by
|
||||||
1. sets `window.__MG_RENDER__` so the frame knows not to autoplay,
|
`lint_frames.py`.
|
||||||
2. seeks the GSAP timeline to that exact timestamp with `tl.pause(t, false)`,
|
|
||||||
3. synchronises every CSS animation with `document.getAnimations().currentTime = t * 1000`,
|
|
||||||
4. waits two animation frames, then screenshots.
|
|
||||||
|
|
||||||
Frame 1,204 of a render is bit-identical to frame 1,204 of the next render. That property is
|
|
||||||
what makes the rest of the system possible — QC can audit specific frames, a single scene can
|
|
||||||
be re-rendered in isolation, and a whole class of timing bugs simply cannot occur.
|
|
||||||
|
|
||||||
> The second parameter is not optional. `pause(atTime, suppressEvents)` defaults
|
|
||||||
> `suppressEvents` to `true`, which silently drops every callback fired by the seek — a
|
|
||||||
> counter animated via `onUpdate` will sit frozen at its initial value with no error. This
|
|
||||||
> cost real debugging time; see [`references/lessons.md`](references/lessons.md) #27.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -70,36 +168,13 @@ be re-rendered in isolation, and a whole class of timing bugs simply cannot occu
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Narration-locked beats** | Scenes position their animation by *matching subtitle text* (`B('block text')`), never by hardcoded frame number. Rewrite the script and the whole video re-times itself — **not one line of frame code changes.** |
|
| **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, not character-count interpolation. In Chinese, two same-length phrases can differ 3× in duration, so interpolation drifts badly. |
|
| **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 the MP3 *container* duration (keeps audio and video aligned); the final subtitle block retires on the true *speech* end. Mixing the two produces subtitles that linger ~0.8s per scene. |
|
| **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** | Eight categories — bold signal cards, luxury minimal, NYT-grade data charts, Swiss grid, glitch art, film light-leak, fluid hero, brand outro, and more. Each with canvas, type scale, timeline, and colour discipline documented. |
|
| **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 finished video also ships a 16:9 feed cover and an independently laid-out 3:4 grid cover. Not a crop — a crop loses 57.8% of the frame width. |
|
| **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 linting** | `lint_frames.py` audits all eight frame-contract rules before you spend 15 minutes rendering. |
|
| **Pre-render audit + QC** | `lint_frames.py` reports contract violations before you spend render time; `qc_check.py` checks loudness, drift and samples frames. |
|
||||||
| **Adaptive QC** | Loudness, duration drift, stream integrity, and a sampled contact sheet — with frame-size thresholds derived from the video's own median, so dark themes don't produce false positives. |
|
| **Structurally offline** | GSAP vendored; browser auto-detected; ffmpeg falls back to the `imageio-ffmpeg` static binary; web fonts are banned by contract. |
|
||||||
| **Offline by construction** | GSAP is vendored. Chrome or Edge is auto-detected. ffmpeg falls back to the static binary that ships with `imageio-ffmpeg`. Web fonts are banned by contract. |
|
|
||||||
| **Single source of truth for colour** | Every colour lives in `theme.css` as a CSS variable. Change the theme and the entire video re-skins with zero frame edits. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How it compares
|
|
||||||
|
|
||||||
| | `html-explainer` | Screen-recording HTML tools | Remotion | Cloud video APIs |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| **Render method** | Deterministic seek | Live capture | React → frames | Server-side render |
|
|
||||||
| **Reproducible frames** | Yes | No | Yes | N/A |
|
|
||||||
| **Per-render cost** | Zero | Zero | Zero | Metered |
|
|
||||||
| **Runtime dependency** | Node + Python + a browser | Same | Node + React toolchain | Network + account |
|
|
||||||
| **Authoring model** | HTML/CSS/GSAP per scene | HTML per scene | React components | JSON / template |
|
|
||||||
| **Re-timing after a script edit** | Automatic (`B()` beats) | Manual | Manual | Manual |
|
|
||||||
| **Works offline end-to-end** | Yes | Yes | Yes | No |
|
|
||||||
| **Built-in style library** | 23 documented styles | Varies | Community templates | Vendor templates |
|
|
||||||
| **Cover/thumbnail output** | Two verified layouts | No | No | Sometimes |
|
|
||||||
|
|
||||||
The honest trade-off: because rendering is seek-based, **every animation must be seekable**.
|
|
||||||
Wall-clock animation (`setInterval`, `requestAnimationFrame` counters, CSS `transition`
|
|
||||||
entrances) cannot work and is rejected by the linter. That constraint is what buys
|
|
||||||
determinism, and it is enforced rather than merely advised.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -107,339 +182,110 @@ determinism, and it is enforced rather than merely advised.
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
A["Topic"] --> B["Research<br/><i>facts with sources</i>"]
|
A["Topic"] --> B["Research<br/><i>every figure sourced</i>"]
|
||||||
B --> C["narration.json<br/><i>text, | splits subtitle blocks</i>"]
|
B --> C["narration.json<br/><i>pipes split subtitle blocks</i>"]
|
||||||
C --> D["tts_build.py<br/><i>edge-tts → MP3 + word boundaries</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>"]
|
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>"]
|
E --> F["subs.py<br/><i>subs.json, srt/vtt, beats.js</i>"]
|
||||||
F --> G["frames/*.html<br/><i>one scene per beat, style from library</i>"]
|
F --> G["frames/*.html<br/><i>one scene per line, styled from the library</i>"]
|
||||||
G --> H{"lint_frames.py<br/>8 contract rules"}
|
G --> H{"lint_frames.py<br/>eight contract rules"}
|
||||||
H -->|fail| G
|
H -->|fail| G
|
||||||
H -->|pass| I["render_video.mjs<br/><i>seek → PNG per frame</i>"]
|
H -->|pass| I["render_video.mjs<br/><i>seek per frame to PNG</i>"]
|
||||||
I --> J["ffmpeg<br/><i>H.264 + AAC mux</i>"]
|
I --> J["ffmpeg<br/><i>H.264 + AAC mux</i>"]
|
||||||
J --> K[("out/slug.mp4")]
|
J --> K[("out/slug.mp4")]
|
||||||
I --> L["qc_check.py<br/><i>loudness · drift · contact sheet</i>"]
|
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>"]
|
K --> M["cover_build.mjs<br/><i>cover_169.png, cover_34.png</i>"]
|
||||||
|
|
||||||
style K fill:#1f6feb,color:#fff
|
style K fill:#1f6feb,color:#fff
|
||||||
style H fill:#8957e5,color:#fff
|
style H fill:#8957e5,color:#fff
|
||||||
style M fill:#238636,color:#fff
|
style M fill:#238636,color:#fff
|
||||||
```
|
```
|
||||||
|
|
||||||
The order matters. `beats.js` is generated from the TTS output, which is why changing the
|
|
||||||
script re-times everything downstream without touching scene code.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
| Requirement | Minimum | Notes |
|
|
||||||
|---|---|---|
|
|
||||||
| Python | 3.9+ | `edge-tts==7.2.8` (pinned deliberately — v7 changed the boundary API), `numpy`, `pillow`, `imageio-ffmpeg` |
|
|
||||||
| Node.js | 18+ | Runs the renderer and cover builder |
|
|
||||||
| Browser | Chrome or Edge | Auto-detected. Neither present → playwright chromium (~115MB, downloaded once) |
|
|
||||||
| ffmpeg | any | Found on `PATH`, otherwise the static binary bundled with `imageio-ffmpeg` is used |
|
|
||||||
| Disk | ~2 GB per finished video | Frame PNGs are large; they can be discarded after muxing |
|
|
||||||
|
|
||||||
No administrator rights are needed anywhere. Everything installs to user scope.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/OneMoh/html-explainer.git
|
|
||||||
cd html-explainer
|
|
||||||
|
|
||||||
# 1 · Check the environment (reports what's missing)
|
|
||||||
bash setup_env.sh
|
|
||||||
|
|
||||||
# 2 · Install anything missing
|
|
||||||
bash setup_env.sh --install
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run the pipeline inside a project directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PY=<path to your venv python> # expand to an absolute path if you hand work to an agent
|
|
||||||
SKILL=$(pwd) # this repo
|
|
||||||
|
|
||||||
# Stage 0 — scaffold a project
|
|
||||||
"$PY" "$SKILL/scripts/new_project.py" ~/videos/ai-intro ai-intro --topic "人工智能"
|
|
||||||
cd ~/videos/ai-intro
|
|
||||||
|
|
||||||
# …write narration.json, fill project.json's order,
|
|
||||||
# and author one frames/<id>.html per scene…
|
|
||||||
|
|
||||||
# Stages 1–3 — voiceover, timeline, subtitles
|
|
||||||
"$PY" "$SKILL/scripts/tts_build.py" --project .
|
|
||||||
"$PY" "$SKILL/scripts/timeline_build.py" --project .
|
|
||||||
"$PY" "$SKILL/scripts/subs.py" --project .
|
|
||||||
|
|
||||||
# Stage 4 — audit frames before spending render time
|
|
||||||
"$PY" "$SKILL/scripts/lint_frames.py" --project .
|
|
||||||
|
|
||||||
# Stage 5 — render (--preview N gives a fast draft of the first N seconds)
|
|
||||||
node "$SKILL/scripts/render_video.mjs" . --preview 30
|
|
||||||
|
|
||||||
# Stage 6 — QC
|
|
||||||
"$PY" "$SKILL/scripts/qc_check.py" --project .
|
|
||||||
|
|
||||||
# Stage 7 — full render + covers
|
|
||||||
node "$SKILL/scripts/render_video.mjs" .
|
|
||||||
node "$SKILL/scripts/cover_build.mjs" .
|
|
||||||
```
|
|
||||||
|
|
||||||
Output lands in `out/`: `slug.mp4`, `cover_169.png`, `cover_34.png`, `slug.srt`, `slug.vtt`,
|
|
||||||
plus `qc_report.md` and `qc_sheet.jpg`.
|
|
||||||
|
|
||||||
> **Always pass `--keep-frames` if you intend to reuse the rendered PNGs.** Preview mode
|
|
||||||
> deletes the frame directory after muxing, by design. See
|
|
||||||
> [`references/lessons.md`](references/lessons.md) #32.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Command reference
|
## Command reference
|
||||||
|
|
||||||
### Pipeline
|
| Command | Purpose |
|
||||||
|
|
||||||
| Command | Does |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `python scripts/new_project.py <dir> <slug> [--topic T] [--preset P] [--fps 30] [--width 1920] [--height 1080] [--lang zh]` | Scaffolds a project: config, theme, templates, and both cover HTML files |
|
| `new_project.py <dir> <slug> --topic "..."` | Scaffold a project |
|
||||||
| `python scripts/tts_build.py --project . [--voice V] [--rate +8%] [--no-trim] [--no-cache]` | Synthesises narration; caches, timeouts, retries, trims silence; writes both duration clocks |
|
| `tts_build.py --project .` | Synthesise voiceover (caching, timeouts, backoff retries, silence trim) |
|
||||||
| `python scripts/timeline_build.py --project . [--gap SEC]` | Builds the global timeline and stitches `narration-full.mp3` |
|
| `timeline_build.py --project .` | Global timeline + stitched `narration-full.mp3` |
|
||||||
| `python scripts/subs.py --project . [--fps N]` | Emits `subs.json`, `srt`/`vtt` sidecars, and one `beats.js` per scene |
|
| `subs.py --project .` | Three subtitle outputs + one `beats.js` per scene |
|
||||||
| `python scripts/lint_frames.py --project . [--only id,id] [--include-covers]` | Static audit of every frame against the eight contract rules |
|
| `lint_frames.py --project .` | Audit frames against the eight contract rules |
|
||||||
| `node scripts/render_video.mjs <dir> [--out PATH] [--preview SEC] [--fps N] [--concurrency N] [--jpeg] [--scale N] [--keep-frames] [--browser PATH]` | Renders and muxes to MP4 |
|
| `render_video.mjs <dir>` | Render and mux the MP4 |
|
||||||
| `python scripts/qc_check.py --project . [--samples N]` | Loudness / duration / stream audit plus a contact sheet |
|
| `qc_check.py --project .` | Loudness / duration / stream checks + contact sheet |
|
||||||
| `node scripts/cover_build.mjs <dir> [--at last\|SEC] [--only 169\|34] [--scale N] [--jpg] [--keep-frames]` | Renders both covers at 2× by default |
|
| `cover_build.mjs <dir>` | Render both covers, 2× by default |
|
||||||
|
|
||||||
### Utilities
|
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.
|
||||||
|
|
||||||
| Command | Does |
|
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
|
||||||
| `node scripts/peek_frame.mjs <dir> <id> [--at 40,80]` | Screenshots one scene at percentage points — seconds, not minutes. `--all` for every scene |
|
word), `package_skill.py` (build a portable zip).
|
||||||
| `python scripts/make_theme.py --topic "医疗" --use` | Derives a palette from a topic word, or `--preset violet\|cyan\|amber\|mono` |
|
|
||||||
| `python scripts/check_integrity.py` | Version / catalog / count consistency and stray-CDN scan (also runs in CI) |
|
**Full flag reference is in [`SKILL.md`](SKILL.md), or pass `--help` to any script.**
|
||||||
| `python setup_env.sh --install` → `bash setup_env.sh --install` | Environment check and dependency install |
|
|
||||||
| `python scripts/package_skill.py [--with-deps]` | Builds a portable zip (omit `--with-deps` for ~110KB) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The `B()` beat system
|
## The `B()` beat system
|
||||||
|
|
||||||
This is the part that changes how the work feels.
|
`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:
|
||||||
A generated `beats.js` per scene exposes every subtitle block by its text:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
B('右边卡片') // the second the phrase "右边卡片" starts being spoken
|
tl.fromTo('.card', { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('right-hand card'));
|
||||||
Be('证据') // the second that phrase finishes
|
tl.fromTo('.verdict', { scale: 0.8 }, { scale: 1, duration: 0.6 }, B('the verdict') + 0.2);
|
||||||
```
|
```
|
||||||
|
|
||||||
So scene animation reads like this:
|
`B('block text')` is the moment those words start being spoken; `Be('block text')` is the moment
|
||||||
|
they finish.
|
||||||
|
|
||||||
```js
|
This is this project's own design, and it is the part that changes how the work feels: edit the
|
||||||
var tl = gsap.timeline({ paused: true });
|
narration once, re-run three commands, and every scene re-times itself. Compare with hardcoded
|
||||||
window.__tl = tl;
|
frame numbers — one edited sentence means re-aligning the entire video by hand.
|
||||||
|
|
||||||
tl.fromTo('.card', { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('右边卡片'));
|
**Fallbacks are forbidden.** `B('x') || 3.2` hides "beat not found" behind a stale number. A miss
|
||||||
tl.fromTo('.verdict',{ scale: 0.8 }, { scale: 1, duration: 0.6, ease: 'back.out(2)' }, B('结论句') + 0.2);
|
should fail the build — a mis-timed video is far worse than a failed build.
|
||||||
```
|
|
||||||
|
|
||||||
Reword the narration and you re-run three commands. The beats shift, every scene re-times
|
|
||||||
itself, and no frame file is edited. Compare that with frame-number hardcoding, where one
|
|
||||||
changed sentence means re-aligning the entire video by hand.
|
|
||||||
|
|
||||||
**The fallback form is banned.** `B('x') || 3.2` hides a broken beat lookup behind a stale
|
|
||||||
number. If a block can't be found the build should fail loudly — a mis-timed video is worse
|
|
||||||
than a failed build.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Visual style library
|
## Visual style library
|
||||||
|
|
||||||
**Do not design scenes from a blank page.** 23 styles across 8 categories, each documented
|
**Do not design scenes from a blank page.** 23 styles across 8 categories, each recording its
|
||||||
with its canvas, type scale, timeline structure, and colour discipline
|
canvas, type scale, timeline structure and colour discipline — full catalogue in
|
||||||
([`references/style-catalog.md`](references/style-catalog.md)).
|
[`references/style-catalog.md`](references/style-catalog.md) (Chinese).
|
||||||
|
|
||||||
| Category | Styles |
|
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
|
||||||
| Presentation / title cards | Bold Poster, Bold Signal, Build Minimal, Creative Voltage, Electric Studio, Glitch Title, Kinetic Type, Swiss Grid, Warm Grain |
|
social formats, product promos and more.
|
||||||
| Data visualisation | NYT Data Chart, Data Rollup, NYT Graph, Swiss Grid Data |
|
|
||||||
| Diagrams / process | Takram Organic, Decision Tree |
|
|
||||||
| Cinematic atmosphere | Film Light-Leak |
|
|
||||||
| Marketing / hero | Fluid Background Hero |
|
|
||||||
| Bumpers | Brand Logo Outro |
|
|
||||||
| Social portrait (9:16) | Play Mode, Vignelli |
|
|
||||||
| Product demo | Product Promo, Product Promo · 30s |
|
|
||||||
| Effects | VFX Text Cursor |
|
|
||||||
|
|
||||||
Two classes, two very different adaptation costs:
|
Two classes, with very different adaptation costs:
|
||||||
|
|
||||||
- **`rich` (12)** — a single file driven by pure CSS `@keyframes`. The seek renderer drives
|
- **`rich` (12)** — single file, pure CSS `@keyframes`. The seek renderer drives them **with no
|
||||||
these **with zero code changes**; adapting one is three steps: swap the web font for a
|
changes**; adaptation is three steps: swap web fonts for a system stack, lift bottom elements out
|
||||||
system stack, lift bottom elements out of the subtitle zone, fill in real content.
|
of the subtitle band, drop in real content.
|
||||||
- **`gsap` (11)** — multi-composition, CDN-loaded. **Do not port the code.** A multi-scene
|
- **`gsap` (11)** — multiple compositions loaded from a CDN. **Do not port the code.** Take only
|
||||||
composition has no playback hook inside a single seekable frame, so importing it yields a
|
the visual DNA and re-express it in CSS keyframes.
|
||||||
static first frame and no error whatsoever. Take the visual DNA and re-express it as CSS
|
|
||||||
keyframes.
|
|
||||||
|
|
||||||
Rotate two to four styles across a video. Eight scenes sharing one template looks stale;
|
Rotate 2–4 styles per project. Eight scenes sharing one look reads as monotonous.
|
||||||
mixing an aggressive opener, a spare mid-section, and a data-driven conclusion does not.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dual-cover system
|
## Dual covers
|
||||||
|
|
||||||
A finished video produces **two covers that are siblings, not a crop pair**.
|
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
|
||||||
Cropping 3:4 out of 16:9 yields 810px from 1920px — discarding **57.8% of the frame width**.
|
share one visual DNA, but each is **laid out again from scratch**:
|
||||||
Any full-width headline gets sliced. So both covers share one visual DNA (palette,
|
|
||||||
backdrop, key visual, hook copy) but are each laid out from scratch:
|
|
||||||
|
|
||||||
| Element | 16:9 (`1920×1080`) | 3:4 (`1440×1080`) |
|
| Element | 16:9 (`1920×1080`) | 3:4 (`1440×1080`) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Paradox visual | Right side, side-by-side | Top, vertically stacked |
|
| Paradox visual | Right side, side by side | Top, stacked vertically |
|
||||||
| Hook | Bottom-left, 2 lines, ≥96px | Bottom, 3 lines, ≥120px |
|
| Hook | Bottom-left, two lines, ≥96px | Bottom, three lines, ≥120px |
|
||||||
| Corner label | Top-left | Top-centre or dropped |
|
|
||||||
| Max characters per line | 7 | 5 |
|
| Max characters per line | 7 | 5 |
|
||||||
|
|
||||||
Three elements are mandatory, and a missing one means rework: a large hook with genuine
|
`new_project.py` generates the templates for both sizes directly; output is 2× by default. Full
|
||||||
tension, a central visual that *argues a contradiction* (two figures moving opposite ways,
|
rules and the pre-upload checklist: [`references/cover-guide.md`](references/cover-guide.md)
|
||||||
a split line, light against dark — not a decorative shape), and margin to spare
|
(Chinese).
|
||||||
(≥96px / ≥110px on all sides).
|
|
||||||
|
|
||||||
Output is 2× by default: `3840×2160` and `2880×2160`, sized for platform compression.
|
|
||||||
|
|
||||||
Full rules and the pre-upload checklist: [`references/cover-guide.md`](references/cover-guide.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The frame contract
|
|
||||||
|
|
||||||
One scene is one self-contained HTML file. Eight rules — enforced by `lint_frames.py`, and
|
|
||||||
each one exists because breaking it fails *quietly*:
|
|
||||||
|
|
||||||
| # | Rule | Breaks like this |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | Canvas `1920×1080` (or the size declared in `project.json`), system fonts only | Render viewport is the canvas |
|
|
||||||
| 2 | **No external fonts or CDNs** | Font fetch times out; type weight shifts mid-video |
|
|
||||||
| 3 | Colours only from `../theme.css` variables | A theme change becomes a partial re-skin |
|
|
||||||
| 4 | GSAP timeline registered on `window.__tl` | Renderer finds no timeline and errors |
|
|
||||||
| 5 | Position animation with `B('block text')` | Frame and voiceover drift apart |
|
|
||||||
| 6 | Keep the bottom 80–170px clear (subtitle zone) | Subtitles overprint content; both become unreadable |
|
|
||||||
| 7 | One focal point per frame; text ≥22px | QC rejection |
|
|
||||||
| 8 | Load GSAP from `../assets/gsap.min.js` | CDN unreachable → offline render fails |
|
|
||||||
|
|
||||||
Numbers animated in a frame should use a **pure-transform digit strip** rather than
|
|
||||||
`onUpdate`. The renderer passes `suppressEvents = false` so callbacks do work, but a
|
|
||||||
transform-based animation depends on no callback at all and therefore survives every seek
|
|
||||||
strategy.
|
|
||||||
|
|
||||||
Details, layout vocabulary, and the rejection examples: [`references/frame-contract.md`](references/frame-contract.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project layout
|
|
||||||
|
|
||||||
```
|
|
||||||
html-explainer/
|
|
||||||
├── SKILL.md # Agent-facing entry point: workflow, commands, contracts
|
|
||||||
├── README.md # Project documentation (Chinese, default)
|
|
||||||
├── README.en.md # This document
|
|
||||||
├── LICENSE · THIRD_PARTY_NOTICES.md · licenses/
|
|
||||||
├── CHANGELOG.md · CONTRIBUTING.md
|
|
||||||
│
|
|
||||||
├── scripts/
|
|
||||||
│ ├── new_project.py # Scaffold
|
|
||||||
│ ├── tts_build.py # edge-tts → MP3 + word boundaries
|
|
||||||
│ ├── timeline_build.py # Global axis + stitched audio
|
|
||||||
│ ├── subs.py # Subtitles ×3 outputs + beats.js
|
|
||||||
│ ├── lint_frames.py # Pre-render contract audit
|
|
||||||
│ ├── render_video.mjs # Deterministic seek renderer
|
|
||||||
│ ├── qc_check.py # Post-render audit + contact sheet
|
|
||||||
│ ├── cover_build.mjs # Dual-cover renderer
|
|
||||||
│ ├── peek_frame.mjs # Single-scene screenshot preview
|
|
||||||
│ ├── make_theme.py # Palette derivation → theme.css
|
|
||||||
│ ├── check_integrity.py # Repo self-consistency (CI)
|
|
||||||
│ ├── import_styles.py # One-time style-catalog import tool
|
|
||||||
│ └── package_skill.py # Portable zip builder
|
|
||||||
│
|
|
||||||
├── references/
|
|
||||||
│ ├── style-catalog.md / .json # 23 styles, 8 categories
|
|
||||||
│ ├── template-guide.md # Adaptation recipes
|
|
||||||
│ ├── frame-contract.md # The eight rules, with reasoning
|
|
||||||
│ ├── cover-guide.md # Dual-cover specification
|
|
||||||
│ ├── workflow-guide.md # Stage-by-stage detail + agent prompt templates
|
|
||||||
│ └── lessons.md # 34 numbered silent-failure post-mortems
|
|
||||||
│
|
|
||||||
├── assets/
|
|
||||||
│ ├── frame-template.html # Scene template
|
|
||||||
│ ├── cover-template.html # Cover template
|
|
||||||
│ └── gsap.min.js # GSAP 3.13, bundled for offline rendering
|
|
||||||
│
|
|
||||||
├── node/ # Vendored playwright-core
|
|
||||||
├── setup_env.sh # Environment check / install
|
|
||||||
└── .github/workflows/ci.yml # CI: integrity, syntax, CDN scan
|
|
||||||
```
|
|
||||||
|
|
||||||
A **video project** looks like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-video/
|
|
||||||
├── project.json # slug, fps, size, voice, rate, scene order, chapters
|
|
||||||
├── narration.json # [{ id, text }] — "|" splits subtitle blocks
|
|
||||||
├── theme.css # Every colour, as CSS variables
|
|
||||||
├── frames/
|
|
||||||
│ ├── _template.html
|
|
||||||
│ ├── <id>.html # One scene per file
|
|
||||||
│ ├── <id>.beats.js # Generated — never edit by hand
|
|
||||||
│ ├── cover_169.html
|
|
||||||
│ └── cover_34.html
|
|
||||||
├── audio/ render/ research/ script/
|
|
||||||
└── out/ # MP4, covers, SRT/VTT, QC report
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
`project.json`:
|
|
||||||
|
|
||||||
| Key | Meaning |
|
|
||||||
|---|---|
|
|
||||||
| `slug` | Output filename stem |
|
|
||||||
| `lang` | `zh` or `en` — selects the default voice |
|
|
||||||
| `fps` | Frame rate (default 30) |
|
|
||||||
| `width` / `height` | Canvas size (default `1920×1080`) |
|
|
||||||
| `gap` | Silence inserted between scenes, in seconds |
|
|
||||||
| `order` | Scene IDs in playback order — **fill this in after writing `narration.json`** |
|
|
||||||
| `chapters` | Optional `[{ title, startSegment }]` for progress-bar ticks |
|
|
||||||
| `theme` | Theme preset name |
|
|
||||||
| `voice` | edge-tts voice, e.g. `zh-CN-YunxiNeural` |
|
|
||||||
| `rate` | Speech rate, e.g. `+8%` |
|
|
||||||
| `progress` | Whether to render the global progress bar |
|
|
||||||
|
|
||||||
Writing narration:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "id": "hook", "text": "开场钩子,一到两句。|用竖线切字幕块,每块不超过16个字。" },
|
|
||||||
{ "id": "point1", "text": "一个场景一句解说。|字幕块之间用竖线分隔。" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `id` becomes the scene name and must be ASCII (`hook`, `point1`, `ending`).
|
|
||||||
- `|` splits subtitle blocks, **not** scenes. One narration entry = one scene.
|
|
||||||
- Keep Chinese subtitle blocks ≤16 characters, and split at semantic boundaries.
|
|
||||||
- Never add punctuation back to control line breaks — the renderer treats subtitles as
|
|
||||||
on-screen text and strips punctuation.
|
|
||||||
- Two adjacent numbers need a Chinese comma between them, or edge-tts reads them as one
|
|
||||||
fused number (`沪深300` + `4479.55` → "three million four hundred seventy-nine…").
|
|
||||||
|
|
||||||
Aim for roughly 5.4–6 Chinese characters per second. If the total lands more than 15% off
|
|
||||||
your target duration, rewrite sentences — do not distort the speech rate.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -447,99 +293,90 @@ your target duration, rewrite sentences — do not distort the speech rate.
|
|||||||
|
|
||||||
| Symptom | Cause | Fix |
|
| Symptom | Cause | Fix |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Every frame is the static first frame; nothing errors | A timeline was never registered on `window.__tl`, or `page.evaluate` was handed a *string* instead of a function literal (Playwright evaluates a string once and returns the function object — the body never runs) | Register the timeline; pass a real function literal. Lessons #17 |
|
| 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 |
|
||||||
| Numbers render but never move | `onUpdate` callbacks suppressed by a seek | Fixed in the renderer (`pause(t, false)`). Animate with a transform-based digit strip instead. Lessons #27 |
|
| Video comes out 1280×720 | `viewport` was passed to `browser.launch()` — it is a *context*-level option | Pass it to `newPage()` |
|
||||||
| Video is 1280×720 instead of 1920×1080 | `viewport` passed to `browser.launch()` — it is a *context* option | Pass it to `newPage()`. Lessons #18 |
|
| Cover renders at 1× | Same trap, `deviceScaleFactor` twin | Pass to `newPage()` + `screenshot({ scale: 'device' })` |
|
||||||
| Covers come out at 1× though the log says 2× | Same trap with `deviceScaleFactor` | Pass to `newPage()` + `screenshot({ scale: 'device' })`; verify with PIL. Lessons #28 |
|
| 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 — ffmpeg on Windows reads UTF-8 as ANSI | Keep project, output, and frame paths ASCII. Lessons #11 |
|
| `No such file or directory` on Windows | Non-ASCII path — Windows ffmpeg reads UTF-8 as ANSI | Keep paths ASCII |
|
||||||
| Node can't find `/tmp/...` | Git Bash's `/tmp` ≠ Node's `/tmp` on Windows | Pass `C:/...` forward-slash absolute paths. Lessons #12 |
|
| Preview finished, frame PNGs are gone | Preview mode clears the frame directory by design | Pass `--keep-frames` |
|
||||||
| QC reports every frame as suspicious | Fixed frame-size threshold, invalid on dark themes | Now adaptive (35% of the video's own median). Lessons #19 |
|
|
||||||
| QC says everything passed but late scenes were never checked | Sampling that slices the first N blocks instead of stepping evenly | Fixed. Lessons #29 |
|
|
||||||
| Subtitles vanish slightly early on the last block | `speech_end_sec` computed in pre-trim coordinates | Fixed in 1.2.2. Lessons #31 |
|
|
||||||
| First 0.5s of the video is nearly black | Entrances fading in from `opacity: 0` on the first narration beat | Start earlier, use `power4.out`, and let a mask wipe provide the reveal instead of opacity. Lessons #34 |
|
|
||||||
| Rendered PNGs disappeared after a preview | Preview mode deletes the frame directory by design | Pass `--keep-frames`. Lessons #32 |
|
|
||||||
| Re-rendered the whole video to fix one scene | No per-scene switch exists | Use the temporary-project technique. Lessons #33 |
|
|
||||||
|
|
||||||
**`references/lessons.md` is the most valuable file here.** 34 numbered entries, each
|
**`references/lessons.md` is the most valuable file in this repository.** 34 numbered entries, each
|
||||||
recording a bug that produced a plausible-looking video while being wrong — including how
|
one a bug where "the video looked fine but was wrong" — including how it was misdiagnosed at first.
|
||||||
it was misdiagnosed first. Read it before debugging from scratch.
|
Read it before debugging from scratch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Who this is for
|
## Who this is for
|
||||||
|
|
||||||
**Good fit**
|
**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.
|
||||||
|
|
||||||
- You want to turn articles, documentation, or a topic into narrated videos at some volume.
|
**Poor fit:** live-action editing, talking-head footage, re-creating an existing video; wanting a
|
||||||
- You already work with an AI coding agent and would rather express scenes as HTML than
|
drag-and-drop editor (scenes are code, deliberately); wanting React component animation as the
|
||||||
learn a motion-graphics suite.
|
authoring model — that is the domain of the sibling project
|
||||||
- You need reproducible output — re-render the same project and get the same frames.
|
[`anything2explainer`](https://github.com/Vincentwei1021/anything2explainer).
|
||||||
- You need it to run offline, or you object to per-render pricing.
|
|
||||||
- You want the infrastructure work done: subtitles, audio sync, covers, QC.
|
|
||||||
|
|
||||||
**Poor fit**
|
---
|
||||||
|
|
||||||
- Real-footage editing, talking-head video, or recreating an existing video.
|
## Prior work and sibling projects
|
||||||
- React component animation as the authoring model (that is the job of the Remotion-based
|
|
||||||
`anything2explainer`, this project's sibling).
|
`html-explainer` did not invent its methodology. It has two clear sources of ideas. Both are
|
||||||
- A graphical drag-and-drop editor. Scenes are code, deliberately.
|
**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**](https://github.com/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**](https://github.com/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`](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
|
## Roadmap
|
||||||
|
|
||||||
- [ ] Per-scene render switch (`--only <id>`) — today this needs the temporary-project workaround
|
- [ ] Per-scene render switch (`--only <id>`) — today this needs the temporary-project workaround
|
||||||
- [ ] Cover layout measurement tool (line count / four-side margins / element overlap / hook size in one pass) — today you write a dozen lines yourself
|
- [ ] Cover layout measurement tool (line count / margins / overlap / hook font size in one pass)
|
||||||
- [ ] Additional cover aspect ratios via the spec table in `cover_build.mjs`
|
- [ ] More cover aspect ratios
|
||||||
- [ ] English-first style notes in the catalog (currently Chinese descriptions throughout)
|
- [ ] English descriptions in the style catalogue
|
||||||
- [ ] Optional background-music bed with automatic ducking under narration
|
- [ ] Optional music bed with automatic ducking under narration
|
||||||
- [ ] Frame-contract lint rules for focal-point and glow discipline (currently eyeball-only)
|
|
||||||
- [ ] Pre-built project templates for common video shapes (60s short, 3-min explainer)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Issues and PRs are welcome. The most valuable contribution is a **diagnosed silent
|
Issues and PRs are welcome. The most valuable contribution is a **diagnosed silent failure** added
|
||||||
failure** added to `references/lessons.md` — the symptom as it first misled you, the
|
to `references/lessons.md` — write down how it first misled you, the evidence that proved the cause,
|
||||||
evidence that proved the cause, the fix, and the resulting rule.
|
the fix, and the general rule that follows. Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) first
|
||||||
|
(Chinese).
|
||||||
Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) first. It covers the project's hard rules,
|
|
||||||
how to run an end-to-end check, and the PR checklist.
|
|
||||||
|
|
||||||
CI checks the repository for version drift, style-catalog drift, and stray CDN references
|
|
||||||
on Linux and Windows across Python 3.9 and 3.13.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Acknowledgements
|
## Licence and acknowledgements
|
||||||
|
|
||||||
The narration and audio-sync methodology in this project — word-boundary subtitles, the
|
|
||||||
two-level clock, speech-rate calibration, and the QC criteria — grew out of two earlier
|
|
||||||
projects by the same author, `anything2explainer` and `html-video-workbuddy-driver`.
|
|
||||||
|
|
||||||
The 23-style visual catalog transcribes the template design specifications of
|
|
||||||
[nexu-io/html-video](https://github.com/nexu-io/html-video) (Apache-2.0). Seven of those
|
|
||||||
styles in turn trace back to MIT-licensed design work by
|
|
||||||
[Zara Zhang](https://github.com/zarazhangrui/frontend-slides),
|
|
||||||
[alchaincyf](https://github.com/alchaincyf/huashu-design), and Nate Herk.
|
|
||||||
|
|
||||||
No upstream source code is bundled or required at runtime — the catalog is a written
|
|
||||||
specification, comparable to copying a recipe out of a restaurant. Full attribution, licence
|
|
||||||
texts, and the exact style-level mapping are in
|
|
||||||
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
|
|
||||||
|
|
||||||
GSAP is bundled under GreenSock's [standard "no charge" licence](https://gsap.com/standard-license).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Licence
|
|
||||||
|
|
||||||
[MIT](LICENSE) © 2026 Moh
|
[MIT](LICENSE) © 2026 Moh
|
||||||
|
|
||||||
The MIT licence covers the original code and documentation. Third-party components and the
|
MIT covers the original code and documentation. Bundled third-party components and the derived
|
||||||
derived style specification remain under their own terms — see
|
style specifications remain under their own terms; GSAP is bundled under GreenSock's
|
||||||
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
|
[standard "no charge" licence](https://gsap.com/standard-license). Full notices are in
|
||||||
|
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) (Chinese).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -547,6 +384,7 @@ derived style specification remain under their own terms — see
|
|||||||
|
|
||||||
**Author: Moh**
|
**Author: Moh**
|
||||||
|
|
||||||
Built because re-aligning an entire video by hand after one changed sentence is a bad way to spend an afternoon.
|
Built because "edit one sentence, then re-align the entire video by hand" is not a good way to
|
||||||
|
spend an afternoon.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -2,17 +2,18 @@
|
|||||||
|
|
||||||
# html-explainer
|
# html-explainer
|
||||||
|
|
||||||
**把一个主题做成有配音、有硬字幕、有封面的讲解视频 —— 全本地、用 HTML 写画面,音画同步稳得住。**
|
**一个 Agent Skill:给任意主题,产出一条带配音、硬字幕、封面的讲解视频。**
|
||||||
|
|
||||||
面向 AI 编程智能体的确定性 HTML → MP4 渲染流水线。
|
HTML 写画面 → 确定性逐帧渲染 → 真 MP4。全本地跑,核心链路零 API key、零按次计费。
|
||||||
不需要 Remotion,不需要云端渲染队列,没有按次计费,核心链路不需要任何 API key。
|
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](CHANGELOG.md)
|
[](CHANGELOG.md)
|
||||||
[](https://python.org)
|
[](SKILL.md)
|
||||||
[](https://nodejs.org)
|
[](#安装)
|
||||||
[](#环境要求)
|
[](#安装)
|
||||||
[](CONTRIBUTING.md)
|
[](#安装)
|
||||||
|
[](#环境要求)
|
||||||
|
[](#环境要求)
|
||||||
|
|
||||||
[简体中文](README.md) · [English](README.en.md)
|
[简体中文](README.md) · [English](README.en.md)
|
||||||
|
|
||||||
@@ -20,43 +21,138 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 这是什么
|
## 它是什么
|
||||||
|
|
||||||
`html-explainer` 用来生产科普 / 讲解 / 知识类视频 —— 就是那种带配音、带硬字幕、带进度条,
|
`html-explainer` 把一个主题做成**带配音、带硬字幕、带进度条**的讲解视频,画面用
|
||||||
画面用 HTML/CSS/GSAP 做的动效视频。
|
HTML/CSS/GSAP 写。
|
||||||
|
|
||||||
你写一份解说词。智能体(或者你自己)从 23 种画面风格里挑一个,为每个节拍写一个自包含的
|
它**是一个 Agent Skill**(`SKILL.md` + `scripts/` + `references/`),遵循
|
||||||
HTML 文件,然后这条流水线把它渲染成真正的 MP4:字幕逐字对齐,外加两张可直接上传的封面。
|
[Agent Skills](https://code.claude.com/docs/en/skills) 约定,Claude Code / OpenAI Codex /
|
||||||
|
WorkBuddy / Cursor / Gemini CLI 等都能直接加载。内部是普通的 Python 与 Node 脚本,
|
||||||
|
手工跑也完全没问题。
|
||||||
|
|
||||||
所有计算都在你自己的机器上。唯一的联网请求是 TTS 合成,以及(可选的)为写稿做的资料调研。
|
你只需要说一句:
|
||||||
|
|
||||||
它以 **Agent Skill** 的形式封装(`SKILL.md` + `scripts/` + `references/`),可以直接放进
|
> 把「为什么天空是蓝色的」做成一条 3 分钟的讲解视频。
|
||||||
Claude Code、WorkBuddy、Cursor,或任何能读技能文件的智能体 —— 但脚本本身只是普通的
|
|
||||||
Python 和 Node,手工跑完全没问题。
|
技能会带着智能体走完:调研 → 解说词 → 配音 → 字幕与节拍 → 挑风格写画面 → 渲染 → 体检 → 封面。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 核心思路:确定性 seek,而不是屏幕录制
|
## 安装
|
||||||
|
|
||||||
多数「HTML 转视频」工具的做法是:启动浏览器、播放动画、录屏。这条路线会生出一整类让人
|
### 一句话安装(推荐)
|
||||||
抓狂的 bug,因为它们都是**间歇性**的:动画在录制开始前就播了、网络字体加载晚了导致半段
|
|
||||||
视频用了错误的字体、前几帧拍到的是动画前的状态。
|
|
||||||
|
|
||||||
`html-explainer` 不录制,它 **seek**。
|
把下面这句发给你的智能体:
|
||||||
|
|
||||||
每一帧都执行这四步:
|
```text
|
||||||
|
给当前本地环境安装该 Skill:https://github.com/OneMoh/html-explainer.git
|
||||||
|
安装到你的技能目录,并检测安装必要的运行环境(Python 3.9+ / Node 18+ / Chrome 或 Edge / ffmpeg)
|
||||||
|
```
|
||||||
|
|
||||||
1. 置位 `window.__MG_RENDER__`,让画面知道自己不该自动起播;
|
它会自己 clone 到对应目录、跑环境自检、把缺的东西装上。装完**新开一个会话**,让智能体重新
|
||||||
2. 用 `tl.pause(t, false)` 把 GSAP 时间轴定位到那一瞬间;
|
扫描技能目录。
|
||||||
3. 用 `document.getAnimations().currentTime = t * 1000` 同步所有 CSS 动画;
|
|
||||||
4. 等两个动画帧后截图。
|
|
||||||
|
|
||||||
渲染出来的第 1204 帧,和下一次渲染的第 1204 帧,逐像素一致。正是这个性质让后续的一切成为
|
### 手动安装
|
||||||
可能 —— QC 可以定点抽查某一帧、单个场景可以独立重渲,而一整类时序 bug 从根本上无法发生。
|
|
||||||
|
|
||||||
> 第二个参数不是可选项。`pause(atTime, suppressEvents)` 的 `suppressEvents` 默认为 `true`,
|
仓库根目录**就是**技能目录(`SKILL.md` 在根),直接 clone 进技能目录即可,不用再拷子目录:
|
||||||
> 它会**静默丢弃**这次 seek 触发的所有回调 —— 用 `onUpdate` 写的数字滚动会永远停在初值,
|
|
||||||
> 且零报错。这个坑花掉了真实的调试时间,见 [`references/lessons.md`](references/lessons.md) #27。
|
```bash
|
||||||
|
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` |
|
||||||
|
| Node.js | 18+ | 渲染器与封面器用 |
|
||||||
|
| 浏览器 | Chrome 或 Edge | 自动探测;都没有才下载 playwright chromium(约 115MB,只需一次) |
|
||||||
|
| ffmpeg | 任意版本 | 先在 `PATH` 找;没有则用 `imageio-ffmpeg` 自带的静态二进制 |
|
||||||
|
| 磁盘 | 每条成片约 2GB | 帧 PNG 体积大,合成后可删 |
|
||||||
|
|
||||||
|
`bash setup_env.sh` 只检查并报告缺什么,`--install` 才会装。全程不需要管理员权限。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
一条视频由**七条命令**跑完,都在**视频项目目录**里执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PY=<你的 venv python 路径> # 派给子智能体时要展开成绝对路径
|
||||||
|
SKILL=<技能安装目录>
|
||||||
|
|
||||||
|
# 0 · 建项目脚手架(目录树 + 配置 + 主题 + 两份封面模板)
|
||||||
|
"$PY" "$SKILL/scripts/new_project.py" ~/videos/sky-blue sky-blue --topic "为什么天空是蓝色的"
|
||||||
|
cd ~/videos/sky-blue
|
||||||
|
# …填 narration.json、填 project.json 的 order、为每个场景写一个 frames/<id>.html…
|
||||||
|
|
||||||
|
"$PY" "$SKILL/scripts/tts_build.py" --project . # 1 · 配音
|
||||||
|
"$PY" "$SKILL/scripts/timeline_build.py" --project . # 2 · 全局时间轴
|
||||||
|
"$PY" "$SKILL/scripts/subs.py" --project . # 3 · 字幕 + 节拍
|
||||||
|
"$PY" "$SKILL/scripts/lint_frames.py" --project . # 4 · 渲染前体检画面
|
||||||
|
node "$SKILL/scripts/render_video.mjs" . --preview 30 # 5 · 先出 30 秒样片
|
||||||
|
"$PY" "$SKILL/scripts/qc_check.py" --project . # 6 · QC
|
||||||
|
node "$SKILL/scripts/render_video.mjs" . # 7 · 全片渲染
|
||||||
|
node "$SKILL/scripts/cover_build.mjs" . # + 封面双方案
|
||||||
|
```
|
||||||
|
|
||||||
|
产物都在 `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 报告
|
||||||
|
```
|
||||||
|
|
||||||
|
顺序不能乱:`beats.js` 由 TTS 产出推导而来。这就是为什么改了解说词之后,下游全部自动重排
|
||||||
|
时序,而画面代码一行都不用动。
|
||||||
|
|
||||||
|
> **只要打算复用渲出来的 PNG,就一定要加 `--keep-frames`。** 预览模式在合成后会按设计
|
||||||
|
> 清空帧目录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 为什么不用录屏
|
||||||
|
|
||||||
|
多数「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` 直接拒绝。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,35 +160,13 @@ Python 和 Node,手工跑完全没问题。
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **节拍锚定解说词** | 画面按**匹配字幕文本**定位动画(`B('块文本')`),绝不硬编码帧号。改解说词,全片自动重排时间 —— **画面代码一行都不用改。** |
|
| **节拍锚定解说词** | 画面按**匹配字幕文本**定位动画(`B('块文本')`),绝不硬编码帧号。改解说词,全片自动重排时间,画面代码零改动。 |
|
||||||
| **词边界字幕** | 时序来自 edge-tts 的 `WordBoundary` 事件,不按字数插值。中文里两个同字数的短语时长可以差 3 倍,插值会明显错位。 |
|
| **词边界字幕** | 时序来自 edge-tts 的 `WordBoundary` 事件,不按字数插值 —— 中文里两个同字数的短语时长可以差 3 倍。 |
|
||||||
| **两级时钟** | 帧长用 MP3 **容器时长**(保证音画同步);末块字幕按**语音真实结束**收尾。两者混用会让每场景字幕多停约 0.8 秒。 |
|
| **两级时钟** | 帧长用 MP3 **容器时长**(保音画同步);末块字幕按**语音真实结束**收尾。 |
|
||||||
| **23 种画面风格** | 8 个类别 —— 大胆信号卡、奢华极简、NYT 编辑级数据图表、瑞士网格、故障艺术、胶片漏光、流体 Hero、品牌收尾等,每种都记录了画布、字阶、时间轴与配色纪律。 |
|
| **23 种画面风格** | 8 个类别,每种都记录了画布、字阶、时间轴与配色纪律。不用对着空白页从零设计。 |
|
||||||
| **封面双方案** | 每条成片都附带 16:9 信息流封面与**独立排版**的 3:4 主页封面 —— 不是裁切,裁切会丢掉 57.8% 的画面宽度。 |
|
| **封面双方案** | 每条成片附带 16:9 与**独立重排**的 3:4 封面 —— 不是裁切,裁切会丢掉 57.8% 的画面宽度。 |
|
||||||
| **渲染前静态体检** | `lint_frames.py` 在你花 15 分钟渲染之前,先把八条画面契约的违规逐条报出来。 |
|
| **渲染前体检 + QC** | `lint_frames.py` 在渲染前报出契约违规;`qc_check.py` 查响度、时长漂移、抽帧速览。 |
|
||||||
| **自适应 QC** | 响度、时长漂移、流完整性、抽帧速览图 —— 抽帧体积阈值取本片自身中位数推导,暗色主题不会产生假阳性。 |
|
| **结构性离线** | GSAP 内置;浏览器自动探测;ffmpeg 缺失时回退到 `imageio-ffmpeg` 静态二进制;网络字体被契约禁止。 |
|
||||||
| **结构性离线** | GSAP 内置;Chrome / Edge 自动探测;ffmpeg 缺失时回退到 `imageio-ffmpeg` 自带的静态二进制;网络字体被契约禁止。 |
|
|
||||||
| **配色单一来源** | 所有颜色都是 `theme.css` 里的 CSS 变量。换主题即整片换肤,画面代码零改动。 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 横向对比
|
|
||||||
|
|
||||||
| | `html-explainer` | 录屏式 HTML 工具 | Remotion | 云端视频 API |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| **渲染方式** | 确定性 seek | 实时录制 | React → 逐帧 | 服务端渲染 |
|
|
||||||
| **帧可复现** | 是 | 否 | 是 | 不适用 |
|
|
||||||
| **单次成本** | 零 | 零 | 零 | 按量计费 |
|
|
||||||
| **运行时依赖** | Node + Python + 浏览器 | 同上 | Node + React 工具链 | 网络 + 账号 |
|
|
||||||
| **创作模型** | 每场景一个 HTML | 每场景一个 HTML | React 组件 | JSON / 模板 |
|
|
||||||
| **改稿后重新对时** | 自动(`B()` 节拍) | 手工 | 手工 | 手工 |
|
|
||||||
| **全程离线可跑** | 是 | 是 | 是 | 否 |
|
|
||||||
| **内置风格库** | 23 种(含规范文档) | 视情况 | 社区模板 | 厂商模板 |
|
|
||||||
| **封面 / 缩略图产出** | 两套已验证版式 | 无 | 无 | 有时有 |
|
|
||||||
|
|
||||||
诚实的取舍:因为渲染是 seek 式的,**每一个动画都必须可 seek**。墙钟动画(`setInterval`、
|
|
||||||
`requestAnimationFrame` 计数、CSS `transition` 入场)不可能工作,会被 linter 直接拒绝。
|
|
||||||
这个约束换来了确定性 —— 而且它是被强制执行的,不只是写在文档里的建议。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -119,309 +193,84 @@ flowchart LR
|
|||||||
style M fill:#238636,color:#fff
|
style M fill:#238636,color:#fff
|
||||||
```
|
```
|
||||||
|
|
||||||
顺序不能乱。`beats.js` 由 TTS 产出推导而来 —— 这就是为什么改了解说词之后,下游全部自动
|
|
||||||
重排时序,而场景代码完全不用动。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 环境要求
|
|
||||||
|
|
||||||
| 项 | 最低 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| Python | 3.9+ | `edge-tts==7.2.8`(刻意钉死 —— v7 改过边界 API)、`numpy`、`pillow`、`imageio-ffmpeg` |
|
|
||||||
| Node.js | 18+ | 渲染器与封面器用 |
|
|
||||||
| 浏览器 | Chrome 或 Edge | 自动探测。都没有才下载 playwright chromium(约 115MB,只需一次) |
|
|
||||||
| ffmpeg | 任意版本 | 先在 `PATH` 找;没有则用 `imageio-ffmpeg` 自带的静态二进制 |
|
|
||||||
| 磁盘 | 每条成片约 2GB | 帧 PNG 体积大,合成后可删 |
|
|
||||||
|
|
||||||
全程不需要管理员权限,依赖一律装用户级。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/OneMoh/html-explainer.git
|
|
||||||
cd html-explainer
|
|
||||||
|
|
||||||
# 1 · 环境自检(缺什么报什么)
|
|
||||||
bash setup_env.sh
|
|
||||||
|
|
||||||
# 2 · 安装缺失依赖
|
|
||||||
bash setup_env.sh --install
|
|
||||||
```
|
|
||||||
|
|
||||||
然后在项目目录里跑流水线:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PY=<你的 venv python 路径> # 派给子智能体时要展开成绝对路径
|
|
||||||
SKILL=$(pwd) # 本仓库
|
|
||||||
|
|
||||||
# 阶段 0 —— 建项目脚手架
|
|
||||||
"$PY" "$SKILL/scripts/new_project.py" ~/videos/ai-intro ai-intro --topic "人工智能"
|
|
||||||
cd ~/videos/ai-intro
|
|
||||||
|
|
||||||
# …写 narration.json、填 project.json 的 order、
|
|
||||||
# 并为每个场景写一个 frames/<id>.html…
|
|
||||||
|
|
||||||
# 阶段 1–3 —— 配音、全局轴、字幕
|
|
||||||
"$PY" "$SKILL/scripts/tts_build.py" --project .
|
|
||||||
"$PY" "$SKILL/scripts/timeline_build.py" --project .
|
|
||||||
"$PY" "$SKILL/scripts/subs.py" --project .
|
|
||||||
|
|
||||||
# 阶段 4 —— 渲染前先体检画面(省下整片渲染的时间)
|
|
||||||
"$PY" "$SKILL/scripts/lint_frames.py" --project .
|
|
||||||
|
|
||||||
# 阶段 5 —— 渲染(--preview 30 先出前 30 秒的快速样片)
|
|
||||||
node "$SKILL/scripts/render_video.mjs" . --preview 30
|
|
||||||
|
|
||||||
# 阶段 6 —— QC
|
|
||||||
"$PY" "$SKILL/scripts/qc_check.py" --project .
|
|
||||||
|
|
||||||
# 阶段 7 —— 全片渲染 + 封面
|
|
||||||
node "$SKILL/scripts/render_video.mjs" .
|
|
||||||
node "$SKILL/scripts/cover_build.mjs" .
|
|
||||||
```
|
|
||||||
|
|
||||||
产物在 `out/`:`slug.mp4`、`cover_169.png`、`cover_34.png`、`slug.srt`、`slug.vtt`,
|
|
||||||
外加 `qc_report.md` 与 `qc_sheet.jpg`。
|
|
||||||
|
|
||||||
> **只要打算复用渲出来的 PNG,就一定要加 `--keep-frames`。** 预览模式在合成后会按设计
|
|
||||||
> 清空帧目录。见 [`references/lessons.md`](references/lessons.md) #32。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 命令参考
|
## 命令参考
|
||||||
|
|
||||||
### 主流水线
|
|
||||||
|
|
||||||
| 命令 | 作用 |
|
| 命令 | 作用 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `python scripts/new_project.py <dir> <slug> [--topic T] [--preset P] [--fps 30] [--width 1920] [--height 1080] [--lang zh]` | 建项目脚手架:配置、主题、模板,以及两份封面 HTML |
|
| `new_project.py <dir> <slug> --topic "主题"` | 建项目脚手架 |
|
||||||
| `python scripts/tts_build.py --project . [--voice V] [--rate +8%] [--no-trim] [--no-cache]` | 合成配音;带缓存、超时、退避重试、裁静音;写两种时长口径 |
|
| `tts_build.py --project .` | 合成配音(带缓存、超时、退避重试、裁静音) |
|
||||||
| `python scripts/timeline_build.py --project . [--gap SEC]` | 生成全局时间轴并拼接 `narration-full.mp3` |
|
| `timeline_build.py --project .` | 全局时间轴 + 拼接 `narration-full.mp3` |
|
||||||
| `python scripts/subs.py --project . [--fps N]` | 产出 `subs.json`、`srt`/`vtt` 外挂字幕,以及每场景一份 `beats.js` |
|
| `subs.py --project .` | 字幕三出口 + 每场景 `beats.js` |
|
||||||
| `python scripts/lint_frames.py --project . [--only id,id] [--include-covers]` | 按八条契约静态体检所有画面帧 |
|
| `lint_frames.py --project .` | 按八条契约静态体检画面帧 |
|
||||||
| `node scripts/render_video.mjs <dir> [--out PATH] [--preview SEC] [--fps N] [--concurrency N] [--jpeg] [--scale N] [--keep-frames] [--browser PATH]` | 渲染并合成 MP4 |
|
| `render_video.mjs <dir>` | 渲染并合成 MP4 |
|
||||||
| `python scripts/qc_check.py --project . [--samples N]` | 响度 / 时长 / 流体检 + 抽帧速览图 |
|
| `qc_check.py --project .` | 响度 / 时长 / 流体检 + 抽帧速览图 |
|
||||||
| `node scripts/cover_build.mjs <dir> [--at last\|SEC] [--only 169\|34] [--scale N] [--jpg] [--keep-frames]` | 渲两张封面,默认 2 倍图 |
|
| `cover_build.mjs <dir>` | 渲染封面双方案,默认 2 倍图 |
|
||||||
|
|
||||||
### 辅助工具
|
常用可选参数:`--preview N` 只渲前 N 秒样片、`--keep-frames` 保留帧 PNG、
|
||||||
|
`--only id,id` 只体检指定场景、`--scale N` 输出倍率。
|
||||||
|
|
||||||
| 命令 | 作用 |
|
另有几个顺手的小工具:`peek_frame.mjs <dir> <id>`(单场景秒级截图,改画面时最常用)、
|
||||||
|---|---|
|
`make_theme.py --topic "医疗" --use`(按主题词推配色)、`package_skill.py`(打成可移植 zip)。
|
||||||
| `node scripts/peek_frame.mjs <dir> <id> [--at 40,80]` | 给单个场景在若干百分比时点截图 —— 是「秒级」而不是「分钟级」。`--all` 可覆盖全部场景 |
|
|
||||||
| `python scripts/make_theme.py --topic "医疗" --use` | 按主题词推导配色,或用 `--preset violet\|cyan\|amber\|mono` |
|
**完整参数表见 [`SKILL.md`](SKILL.md),或对任一脚本加 `--help`。**
|
||||||
| `python scripts/check_integrity.py` | 版本号 / 风格目录 / 计数一致性 + 外链扫描(CI 也会跑) |
|
|
||||||
| `bash setup_env.sh --install` | 环境自检与依赖安装 |
|
|
||||||
| `python scripts/package_skill.py [--with-deps]` | 打成可移植 zip(不加 `--with-deps` 约 110KB) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `B()` 节拍系统
|
## `B()` 节拍系统
|
||||||
|
|
||||||
这是让整个工作方式变掉的那一块。
|
`subs.py` 为每个场景生成一份 `beats.js`,把每块字幕按**原文**暴露出来,于是画面动画可以直接
|
||||||
|
锚在吐字时刻上:
|
||||||
`subs.py` 为每个场景生成一份 `beats.js`,把每块字幕按**原文**暴露出来:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
B('右边卡片') // 「右边卡片」这几个字开始被念出的那一秒
|
tl.fromTo('.card', { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('右边卡片'));
|
||||||
Be('证据') // 这句话念完的那一秒
|
tl.fromTo('.verdict', { scale: 0.8 }, { scale: 1, duration: 0.6 }, B('结论句') + 0.2);
|
||||||
```
|
```
|
||||||
|
|
||||||
于是画面动画可以这样写:
|
`B('块文本')` 是「这几个字开始被念出」的时刻,`Be('块文本')` 是念完的时刻。
|
||||||
|
|
||||||
```js
|
这是本项目自己的设计,也是让工作方式变掉的那一块:改一次解说词,重跑三条命令,所有场景自行
|
||||||
var tl = gsap.timeline({ paused: true });
|
重排对时。对比帧号硬编码 —— 改一句话就是整片手工重新对位。
|
||||||
window.__tl = tl;
|
|
||||||
|
|
||||||
tl.fromTo('.card', { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('右边卡片'));
|
**兜底写法是被禁止的。** `B('x') || 3.2` 会把「节拍查不到」藏在一个过期的数字后面。查不到
|
||||||
tl.fromTo('.verdict',{ scale: 0.8 }, { scale: 1, duration: 0.6, ease: 'back.out(2)' }, B('结论句') + 0.2);
|
就应该让构建直接失败 —— 一条对错了时间的视频,比一次构建失败糟糕得多。
|
||||||
```
|
|
||||||
|
|
||||||
改一次解说词,重跑三条命令。节拍随之平移,所有场景自行重排对时,没有一个画面文件被改动。
|
|
||||||
对比帧号硬编码的写法:改一句话意味着整片手工重新对位。
|
|
||||||
|
|
||||||
**兜底写法是被禁止的。** `B('x') || 3.2` 会把「节拍查不到」这件事藏在一个过期的数字后面。
|
|
||||||
查不到就应该让构建直接失败 —— 一条对错了时间的视频,比一次构建失败糟糕得多。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 画面风格库
|
## 画面风格库
|
||||||
|
|
||||||
**不要对着空白页从零设计画面。** 23 种风格、8 个类别,每种都记录了画布、字阶、时间轴结构与
|
**不要对着空白页从零设计画面。** 23 种风格、8 个类别,每种都记录了画布、字阶、时间轴结构与
|
||||||
配色纪律([`references/style-catalog.md`](references/style-catalog.md))。
|
配色纪律 —— 完整目录见 [`references/style-catalog.md`](references/style-catalog.md)。
|
||||||
|
|
||||||
| 类别 | 风格 |
|
涵盖:大胆信号卡 / 奢华极简留白 / NYT 编辑级数据图表 / 瑞士网格 / 故障艺术 / 胶片漏光 /
|
||||||
|---|---|
|
流体 Hero / 品牌 Logo 收尾 / VFX 文字光标 / 社媒竖版(9:16)/ 产品演示等。
|
||||||
| 演示 / 标题卡 | 大胆海报帧、大胆信号卡帧、奢华极简留白帧、创意电压分屏帧、电光工作室分屏帧、故障艺术标题帧、Kinetic Type、Swiss Grid、Warm Grain |
|
|
||||||
| 数据可视化 | NYT 风数据图表帧、数据滚动帧、NYT Graph、瑞士网格数据帧 |
|
|
||||||
| 图解 / 流程 | 东方柔和有机帧、Decision Tree |
|
|
||||||
| 氛围 / 空镜 | 胶片漏光电影帧 |
|
|
||||||
| 营销 / Hero | 流体背景 Hero 帧 |
|
|
||||||
| 片头片尾 | 品牌 Logo 收尾帧 |
|
|
||||||
| 社媒竖版(9:16) | Play Mode、Vignelli |
|
|
||||||
| 产品演示 | Product Promo、Product Promo · 30s |
|
|
||||||
| 特效 | VFX 文字光标 |
|
|
||||||
|
|
||||||
分成两类,改编成本差别很大:
|
分成两类,改编成本差别很大:
|
||||||
|
|
||||||
- **`rich`(12 个)** —— 单文件 + 纯 CSS `@keyframes` 驱动。seek 渲染器**零改动即可驱动**;
|
- **`rich`(12 个)** —— 单文件 + 纯 CSS `@keyframes`。seek 渲染器**零改动即可驱动**;改编
|
||||||
改编只要三步:把网络字体换成系统字体栈、把底部元素抬出字幕带、填进真实内容。
|
只要三步:换系统字体栈、把底部元素抬出字幕带、填进真实内容。
|
||||||
- **`gsap`(11 个)** —— 多 composition + CDN 加载。**不要搬代码。** 多场景组合在单个可
|
- **`gsap`(11 个)** —— 多 composition + CDN 加载。**不要搬代码。** 只取它的视觉 DNA,用
|
||||||
seek 的帧里没有起播钩子,整段搬进来只会得到静止首帧,而且零报错。只取它的视觉 DNA,
|
CSS keyframes 重新表达。
|
||||||
用 CSS keyframes 重新表达。
|
|
||||||
|
|
||||||
一个项目建议轮换 2–4 种风格。8 个场景共用同一张脸会显得单调;用一个强冲击开场、一段留白
|
一个项目建议轮换 2–4 种风格。8 个场景共用同一张脸会显得单调。
|
||||||
论述、一个理性数据收尾,观感完全不同。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 封面双方案
|
## 封面双方案
|
||||||
|
|
||||||
每条成片产出**两张互为姊妹、而非裁切关系**的封面。
|
每条成片产出**两张互为姊妹、而非裁切关系**的封面。从 16:9 居中裁 3:4,1920px 只剩 810px ——
|
||||||
|
丢掉 57.8% 的画面宽度,任何横跨全宽的标题都会被切掉一半。所以两张共享同一套视觉基因,
|
||||||
从 16:9 居中裁 3:4,1920px 只剩 810px —— 丢掉 **57.8% 的画面宽度**。任何横跨全宽的标题都会
|
但各自**重新排一次版**:
|
||||||
被切掉一半。所以两张封面共享同一套视觉基因(配色、幕底、主视觉、钩子文案),但各自**重新
|
|
||||||
排一次版**:
|
|
||||||
|
|
||||||
| 元素 | 16:9(`1920×1080`) | 3:4(`1440×1080`) |
|
| 元素 | 16:9(`1920×1080`) | 3:4(`1440×1080`) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 悖论视觉 | 右侧,左右并置 | 上方,竖排堆叠 |
|
| 悖论视觉 | 右侧,左右并置 | 上方,竖排堆叠 |
|
||||||
| 钩子 | 左下,两行,≥96px | 下方,三行,≥120px |
|
| 钩子 | 左下,两行,≥96px | 下方,三行,≥120px |
|
||||||
| 角标 | 左上 | 顶部居中或省略 |
|
|
||||||
| 每行字数上限 | 7 字 | 5 字 |
|
| 每行字数上限 | 7 字 | 5 字 |
|
||||||
|
|
||||||
三个要素缺一即返工:一句**真正有张力**的大字钩子、一个**在讲矛盾**的主视觉(两个数字反向
|
两个尺寸的输出都由 `new_project.py` 直接生成模板,默认出 2 倍图。完整规则与上传前自检清单:
|
||||||
运动、一道分裂线、一明一暗 —— 不是装饰图形)、以及四边都留有余量(≥96px / ≥110px)。
|
[`references/cover-guide.md`](references/cover-guide.md)。
|
||||||
|
|
||||||
默认输出 2 倍图:`3840×2160` 与 `2880×2160`,用来应付平台压缩。
|
|
||||||
|
|
||||||
完整规则与上传前自检清单:[`references/cover-guide.md`](references/cover-guide.md)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 画面契约
|
|
||||||
|
|
||||||
一个场景 = 一个自包含 HTML 文件。八条规则 —— 由 `lint_frames.py` 强制执行,每一条的存在
|
|
||||||
都是因为违反它会**静默失败**:
|
|
||||||
|
|
||||||
| # | 规则 | 违反了会怎样 |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | 画布 `1920×1080`(或 `project.json` 声明的尺寸),只用系统字体 | 渲染视口即画布 |
|
|
||||||
| 2 | **绝不引外部字体或 CDN** | 字体请求超时,片中字重跳变 |
|
|
||||||
| 3 | 颜色只取 `../theme.css` 的变量 | 换主题变成半换皮 |
|
|
||||||
| 4 | GSAP 时间轴注册到 `window.__tl` | 渲染器找不到时间轴,直接报错 |
|
|
||||||
| 5 | 用 `B('块文本')` 定位动画 | 画面与配音变成两张皮 |
|
|
||||||
| 6 | 底部 80–170px 保持干净(字幕带) | 字幕压字,两边都读不清 |
|
|
||||||
| 7 | 每帧一个焦点,文字 ≥22px | QC 退回 |
|
|
||||||
| 8 | GSAP 从 `../assets/gsap.min.js` 加载 | CDN 不可达 → 离线渲染失败 |
|
|
||||||
|
|
||||||
帧里的数字滚动应该用**纯 transform 的「数字卷轴」**,而不是 `onUpdate`。渲染器已经传
|
|
||||||
`suppressEvents = false` 让回调能跑,但基于 transform 的动画不依赖任何回调,因此在任何
|
|
||||||
seek 策略下都成立。
|
|
||||||
|
|
||||||
细则、版式基因与反例:[`references/frame-contract.md`](references/frame-contract.md)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
html-explainer/
|
|
||||||
├── SKILL.md # 面向智能体的入口:流程、命令、契约
|
|
||||||
├── README.md # 本文档(中文,默认)
|
|
||||||
├── README.en.md # 英文版
|
|
||||||
├── LICENSE · THIRD_PARTY_NOTICES.md · licenses/
|
|
||||||
├── CHANGELOG.md · CONTRIBUTING.md
|
|
||||||
│
|
|
||||||
├── scripts/
|
|
||||||
│ ├── new_project.py # 建项目
|
|
||||||
│ ├── tts_build.py # edge-tts → MP3 + 词边界
|
|
||||||
│ ├── timeline_build.py # 全局轴 + 拼接音轨
|
|
||||||
│ ├── subs.py # 字幕三出口 + beats.js
|
|
||||||
│ ├── lint_frames.py # 渲染前契约体检
|
|
||||||
│ ├── render_video.mjs # 确定性 seek 渲染器
|
|
||||||
│ ├── qc_check.py # 成片体检 + 抽帧速览图
|
|
||||||
│ ├── cover_build.mjs # 封面双方案渲染器
|
|
||||||
│ ├── peek_frame.mjs # 单场景速览截图
|
|
||||||
│ ├── make_theme.py # 推色 → theme.css
|
|
||||||
│ ├── check_integrity.py # 仓库自洽性(CI)
|
|
||||||
│ ├── import_styles.py # 一次性风格目录导入工具
|
|
||||||
│ └── package_skill.py # 可移植 zip 打包
|
|
||||||
│
|
|
||||||
├── references/
|
|
||||||
│ ├── style-catalog.md / .json # 23 种风格 / 8 个类别
|
|
||||||
│ ├── template-guide.md # 改编三步法 / 重写法
|
|
||||||
│ ├── frame-contract.md # 八条契约与理由
|
|
||||||
│ ├── cover-guide.md # 封面双方案详规
|
|
||||||
│ ├── workflow-guide.md # 阶段详解 + agent prompt 模板
|
|
||||||
│ └── lessons.md # 34 条静默失败复盘
|
|
||||||
│
|
|
||||||
├── assets/
|
|
||||||
│ ├── frame-template.html # 场景模板
|
|
||||||
│ ├── cover-template.html # 封面模板
|
|
||||||
│ └── gsap.min.js # GSAP 3.13,内置供离线渲染
|
|
||||||
│
|
|
||||||
├── node/ # 随包 playwright-core
|
|
||||||
├── setup_env.sh # 环境自检 / 安装
|
|
||||||
└── .github/workflows/ci.yml # CI:自洽性、语法、外链扫描
|
|
||||||
```
|
|
||||||
|
|
||||||
一个 **视频项目** 长这样:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-video/
|
|
||||||
├── project.json # slug、fps、尺寸、音色、语速、场景顺序、章节
|
|
||||||
├── narration.json # [{ id, text }] —— 用 "|" 切字幕块
|
|
||||||
├── theme.css # 所有颜色,以 CSS 变量形式
|
|
||||||
├── frames/
|
|
||||||
│ ├── _template.html
|
|
||||||
│ ├── <id>.html # 一个场景一个文件
|
|
||||||
│ ├── <id>.beats.js # 自动生成 —— 绝不手改
|
|
||||||
│ ├── cover_169.html
|
|
||||||
│ └── cover_34.html
|
|
||||||
├── audio/ render/ research/ script/
|
|
||||||
└── out/ # MP4、封面、SRT/VTT、QC 报告
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 配置说明
|
|
||||||
|
|
||||||
`project.json`:
|
|
||||||
|
|
||||||
| 字段 | 含义 |
|
|
||||||
|---|---|
|
|
||||||
| `slug` | 输出文件名主干 |
|
|
||||||
| `lang` | `zh` 或 `en` —— 决定默认音色 |
|
|
||||||
| `fps` | 帧率(默认 30) |
|
|
||||||
| `width` / `height` | 画布尺寸(默认 `1920×1080`) |
|
|
||||||
| `gap` | 场景之间插入的静音秒数 |
|
|
||||||
| `order` | 场景 ID 的播放顺序 —— **写完 `narration.json` 后必须填** |
|
|
||||||
| `chapters` | 可选 `[{ title, startSegment }]`,给进度条加章节刻度 |
|
|
||||||
| `theme` | 主题预设名 |
|
|
||||||
| `voice` | edge-tts 音色,如 `zh-CN-YunxiNeural` |
|
|
||||||
| `rate` | 语速,如 `+8%` |
|
|
||||||
| `progress` | 是否渲染全局进度条 |
|
|
||||||
|
|
||||||
写解说词:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "id": "hook", "text": "开场钩子,一到两句。|用竖线切字幕块,每块不超过16个字。" },
|
|
||||||
{ "id": "point1", "text": "一个场景一句解说。|字幕块之间用竖线分隔。" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `id` 会成为场景名,必须是 ASCII(`hook`、`point1`、`ending`)。
|
|
||||||
- `|` 切的是**字幕块**,不是场景。一条解说词 = 一个场景。
|
|
||||||
- 中文字幕每块 ≤16 字,切点落在语义边界上。
|
|
||||||
- 不要靠加回标点来控制断行 —— 渲染器把字幕当作屏幕文本,会去掉标点。
|
|
||||||
- **相邻的两个数字之间要插中文逗号**,否则 edge-tts 会把它们熔成一个数
|
|
||||||
(`沪深300` + `4479.55` → 「三百万四千四百七十九点五五点」)。
|
|
||||||
|
|
||||||
中文语速约 5.4–6 字/秒。如果总时长偏离目标超过 15%,改句子 —— 不要扭曲语速去硬凑。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -429,18 +278,12 @@ my-video/
|
|||||||
|
|
||||||
| 症状 | 原因 | 处理 |
|
| 症状 | 原因 | 处理 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 每一帧都是静止首帧,且不报错 | 时间轴没注册到 `window.__tl`;或 `page.evaluate` 传了**字符串**而不是函数字面量(Playwright 只把字符串求值一次,返回的是函数对象,函数体根本不执行) | 注册时间轴;传真正的函数字面量。lessons #17 |
|
| 每一帧都是静止首帧,且不报错 | 时间轴没注册到 `window.__tl`;或 `page.evaluate` 传了**字符串**而非函数字面量(Playwright 只求值一次,函数体根本不执行) | 传真正的函数字面量 |
|
||||||
| 数字能渲染,但永远不动 | seek 抑制了 `onUpdate` 回调 | 渲染器已修(`pause(t, false)`)。帧里改用 transform 数字卷轴。lessons #27 |
|
| 视频变成 1280×720 | `viewport` 传给了 `browser.launch()` —— 它是 *context* 级选项 | 传给 `newPage()` |
|
||||||
| 视频变成 1280×720 而不是 1920×1080 | `viewport` 传给了 `browser.launch()` —— 它是 *context* 级选项 | 传给 `newPage()`。lessons #18 |
|
| 封面出成 1 倍图 | 同上,`deviceScaleFactor` 的孪生坑 | 传 `newPage()` + `screenshot({ scale: 'device' })` |
|
||||||
| 封面出成 1 倍图,但日志写着 2 倍 | 同上,`deviceScaleFactor` 的孪生坑 | 传给 `newPage()` + `screenshot({ scale: 'device' })`;用 PIL 核尺寸。lessons #28 |
|
| 数字能渲染但永远不动 | seek 抑制了 `onUpdate` 回调 | 渲染器已修(`pause(t, false)`);帧里改用 transform 数字卷轴 |
|
||||||
| Windows 上 `No such file or directory` | 非 ASCII 路径 —— Windows 的 ffmpeg 把 UTF-8 当 ANSI 读 | 项目、输出、抽帧路径全部保持 ASCII。lessons #11 |
|
| Windows 上 `No such file or directory` | 非 ASCII 路径 —— Windows 的 ffmpeg 把 UTF-8 当 ANSI 读 | 路径保持 ASCII |
|
||||||
| Node 找不到 `/tmp/...` | Git Bash 的 `/tmp` ≠ Node 的 `/tmp` | 传 `C:/...` 正斜杠绝对路径。lessons #12 |
|
| 预览渲完,帧 PNG 不见了 | 预览模式按设计会清空帧目录 | 加 `--keep-frames` |
|
||||||
| QC 把所有帧都报成可疑 | 抽帧体积阈值写死了,在暗色主题上失效 | 已改为自适应(本片中位数的 35%)。lessons #19 |
|
|
||||||
| QC 说全过,但后半段其实一帧没抽 | 抽样逻辑取了前 N 块,而不是按步长均匀挑 | 已修。lessons #29 |
|
|
||||||
| 末块字幕提前消失 | `speech_end_sec` 用了裁前坐标 | 1.2.2 已修。lessons #31 |
|
|
||||||
| 视频前 0.5 秒几乎全黑 | 第一句解说配了从 `opacity: 0` 开始的淡入 | 提前入场起点、换 `power4.out`,并让遮罩上推本身去提供遮挡而不是叠加透明度。lessons #34 |
|
|
||||||
| 预览渲完,帧 PNG 不见了 | 预览模式按设计会清空帧目录 | 加 `--keep-frames`。lessons #32 |
|
|
||||||
| 为了修一个场景重渲了整片 | 渲染器没有单场景开关 | 用临时项目法。lessons #33 |
|
|
||||||
|
|
||||||
**`references/lessons.md` 是这个仓库里最值钱的文件。** 34 条编号记录,每一条都是一个
|
**`references/lessons.md` 是这个仓库里最值钱的文件。** 34 条编号记录,每一条都是一个
|
||||||
「成片看着挺正常、其实是错的」的 bug —— 包括它一开始是怎么被误判的。从零开始 debug 之前,
|
「成片看着挺正常、其实是错的」的 bug —— 包括它一开始是怎么被误判的。从零开始 debug 之前,
|
||||||
@@ -450,71 +293,64 @@ my-video/
|
|||||||
|
|
||||||
## 适合谁
|
## 适合谁
|
||||||
|
|
||||||
**适合**
|
**适合**:想成规模地把文章 / 文档 / 某个主题做成带解说的视频;已经在用 AI 编程智能体,
|
||||||
|
且宁愿用 HTML 描述画面也不想学动效软件;需要可复现的输出;需要离线跑或拒绝按次计费。
|
||||||
|
|
||||||
- 想成规模地把文章、文档或某个主题做成带解说的视频。
|
**不适合**:实拍剪辑、真人口播、复刻已有视频;想要图形化拖拽编辑器(画面是代码,这是刻意
|
||||||
- 已经在用 AI 编程智能体,并且宁愿用 HTML 描述画面,也不想学一套动效软件。
|
选择);想把 React 组件动画作为创作模型 —— 那是姊妹项目
|
||||||
- 需要可复现的输出 —— 同一个项目重渲一次,帧应当一致。
|
[`anything2explainer`](https://github.com/Vincentwei1021/anything2explainer) 的领域。
|
||||||
- 需要离线跑,或者不接受按次计费。
|
|
||||||
- 想把字幕、音画同步、封面、QC 这些基础工程直接拿走用。
|
|
||||||
|
|
||||||
**不适合**
|
---
|
||||||
|
|
||||||
- 实拍剪辑、真人口播,或者复刻一条已有的视频。
|
## 参考思路与姊妹项目
|
||||||
- 想把「React 组件动画」作为创作模型(那是本项目的姊妹项目、基于 Remotion 的
|
|
||||||
`anything2explainer` 的领域)。
|
`html-explainer` 的方法论不是凭空来的。它有两处明确的思想来源,两个项目都**由不同作者独立
|
||||||
- 想要图形化拖拽编辑器。画面是代码,这是刻意选择。
|
开发,与本项目不是同一作者**;本仓库不打包、运行时不依赖它们的任何代码 —— 吸收的是设计规范
|
||||||
|
与工程经验。
|
||||||
|
|
||||||
|
- [**Vincentwei1021/anything2explainer**](https://github.com/Vincentwei1021/anything2explainer)
|
||||||
|
(TypeScript / Remotion)—— 「主题进 → 带解说的讲解视频出」。本项目从这条线继承的是
|
||||||
|
**解说与音画同步方法论**:词边界字幕、两级时钟、语速标定、QC 判据。
|
||||||
|
*差异*:它是 Remotion + React 组件动画;本项目用 HTML/CSS/GSAP + 确定性 seek。
|
||||||
|
|
||||||
|
- [**nexu-io/html-video**](https://github.com/nexu-io/html-video)(Apache-2.0,Open Design
|
||||||
|
团队)—— 「在本机把 HTML 变成真 MP4」。本项目从这条线继承的是 **23 种画面风格目录**,
|
||||||
|
转写自它的模板设计规范。其中 7 种又可追溯到 MIT 许可的设计作品。
|
||||||
|
*差异*:它靠实时录制;本项目靠 seek 逐帧渲染,帧可复现。
|
||||||
|
|
||||||
|
**本项目自己的部分**:确定性 seek 渲染器、`B()` 节拍锚定、封面双方案,以及
|
||||||
|
`lint_frames.py` / `qc_check.py` 的判据。完整的署名与逐风格对应关系见
|
||||||
|
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。
|
||||||
|
|
||||||
|
如果你要的是 React 组件动画、或 Studio 可视化协作,直接去用上面两个项目;如果你想改一句
|
||||||
|
解说词而不用手工重新对位、并且要帧级可复现,用本项目。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 路线图
|
## 路线图
|
||||||
|
|
||||||
- [ ] 单场景渲染开关(`--only <id>`)—— 目前需要临时项目法绕过
|
- [ ] 单场景渲染开关(`--only <id>`)—— 目前需要临时项目法绕过
|
||||||
- [ ] 封面排版终态测量器(行数 / 四边边距 / 元素重叠 / 钩子字号一次判完)—— 目前需自行写十几行
|
- [ ] 封面排版终态测量器(行数 / 四边边距 / 元素重叠 / 钩子字号一次判完)
|
||||||
- [ ] 通过 `cover_build.mjs` 里的规格表支持更多封面比例
|
- [ ] 更多封面比例
|
||||||
- [ ] 风格目录补英文说明(目前描述以中文为主)
|
- [ ] 风格目录补英文说明
|
||||||
- [ ] 可选背景音乐轨,并对解说自动做闪避(ducking)
|
- [ ] 可选背景音乐轨,并对解说自动做闪避(ducking)
|
||||||
- [ ] 「焦点 / 发光纪律」的 lint 规则(目前只能靠肉眼)
|
|
||||||
- [ ] 常见视频形态的项目模板(60 秒短片、3 分钟讲解)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 参与贡献
|
## 参与贡献
|
||||||
|
|
||||||
欢迎提 issue 和 PR。最有价值的贡献,是一条**已被定位的静默失败**,加进
|
欢迎提 issue 和 PR。最有价值的贡献,是一条**已被定位的静默失败**,加进
|
||||||
`references/lessons.md` —— 写下它一开始是怎么误导你的、证明病因的证据、修法,以及由此
|
`references/lessons.md` —— 写下它一开始是怎么误导你的、证明病因的证据、修法,以及由此得出的
|
||||||
得出的通用规则。
|
通用规则。请先读 [`CONTRIBUTING.md`](CONTRIBUTING.md)。
|
||||||
|
|
||||||
请先读 [`CONTRIBUTING.md`](CONTRIBUTING.md),里面有本项目的硬性规则、端到端自检方法,
|
|
||||||
以及 PR 检查清单。
|
|
||||||
|
|
||||||
CI 会在 Linux 与 Windows 上、Python 3.9 与 3.13 上检查版本号漂移、风格目录漂移和外链引用。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 致谢
|
## 许可与致谢
|
||||||
|
|
||||||
本项目的解说与音画同步方法论 —— 词边界字幕、两级时钟、语速标定、QC 判据 —— 源自作者
|
|
||||||
更早的两个项目 `anything2explainer` 与 `html-video-workbuddy-driver`。
|
|
||||||
|
|
||||||
23 种画面风格目录转写自 [nexu-io/html-video](https://github.com/nexu-io/html-video)
|
|
||||||
(Apache-2.0)的模板设计规范。其中 7 种风格又可追溯到 MIT 许可的设计作品,作者分别是
|
|
||||||
[Zara Zhang](https://github.com/zarazhangrui/frontend-slides)、
|
|
||||||
[alchaincyf](https://github.com/alchaincyf/huashu-design) 与 Nate Herk。
|
|
||||||
|
|
||||||
本仓库不打包、运行时不依赖任何上游源码 —— 风格目录是一份文字规范,类比把菜谱抄回家。
|
|
||||||
完整的署名、许可证原文与逐风格对应关系见
|
|
||||||
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。
|
|
||||||
|
|
||||||
GSAP 按 GreenSock 的 [standard "no charge" 许可](https://gsap.com/standard-license) 内置。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 许可
|
|
||||||
|
|
||||||
[MIT](LICENSE) © 2026 Moh
|
[MIT](LICENSE) © 2026 Moh
|
||||||
|
|
||||||
MIT 许可覆盖原创代码与文档。第三方组件与衍生出的风格规范仍适用其各自的条款 —— 见
|
MIT 覆盖原创代码与文档。第三方组件与衍生出的风格规范仍适用其各自的条款;GSAP 按 GreenSock
|
||||||
|
的 [standard "no charge" 许可](https://gsap.com/standard-license) 内置。完整声明见
|
||||||
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。
|
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -218,13 +218,49 @@ GSAP 用 `../assets/gsap.min.js`(本地内置)→ 主体动画压在 speech_
|
|||||||
| `assets/frame-template.html` | 场景模板(契约注释在文件头,B() 用法示例) |
|
| `assets/frame-template.html` | 场景模板(契约注释在文件头,B() 用法示例) |
|
||||||
| `assets/cover-template.html` | **封面模板**(封面三要素注释在文件头,可改尺寸复用为两份) |
|
| `assets/cover-template.html` | **封面模板**(封面三要素注释在文件头,可改尺寸复用为两份) |
|
||||||
| `assets/gsap.min.js` | GSAP 3.13 本地内置(离线渲染;License 见同目录 `gsap-README.md`) |
|
| `assets/gsap.min.js` | GSAP 3.13 本地内置(离线渲染;License 见同目录 `gsap-README.md`) |
|
||||||
| `README.md` / `README.en.md` | 对外项目说明(中文为主,英文副) |
|
| `README.md` / `README.en.md` | 对外项目说明(**README.md 中文为默认**,`README.en.md` 英文;含跨 Agent 安装指引) |
|
||||||
|
| `CONTRIBUTING.md` | 贡献指南:硬性规则、端到端自检、PR 清单 |
|
||||||
| `CHANGELOG.md` | 版本变更史 |
|
| `CHANGELOG.md` | 版本变更史 |
|
||||||
| `THIRD_PARTY_NOTICES.md` | 第三方组件与衍生内容的授权声明(**发布前必读**) |
|
| `THIRD_PARTY_NOTICES.md` | 第三方组件与衍生内容的授权声明(**发布前必读**) |
|
||||||
|
|
||||||
|
## 跨 Agent 安装
|
||||||
|
|
||||||
|
本技能遵循 [Agent Skills](https://code.claude.com/docs/en/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` 换成你所用智能体的目录):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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`)。
|
技能目录自包含,不引用本机任何绝对路径(脚本按自身位置定位 `SKILL_ROOT`)。
|
||||||
|
仅有两处**兜底探测**会去探 WorkBuddy 托管的运行时目录(`setup_env.sh` 的 Python/Node
|
||||||
|
候选、`peek_frame.mjs` 的技能根候选)—— 探不到就自动跳过,不影响其它平台。
|
||||||
|
|
||||||
**零依赖声明(重要)**:本技能**不需要** html-video 或 anything2explainer 存在。
|
**零依赖声明(重要)**:本技能**不需要** html-video 或 anything2explainer 存在。
|
||||||
两个来源项目的名字只出现在:① 代码注释的出处署名 ② `scripts/import_styles.py`
|
两个来源项目的名字只出现在:① 代码注释的出处署名 ② `scripts/import_styles.py`
|
||||||
@@ -237,7 +273,7 @@ qc → cover)**完全不碰这两个项目**。风格库是抄成纯文本的
|
|||||||
python package_skill.py # → dist/html-explainer-v<版本>.zip
|
python package_skill.py # → dist/html-explainer-v<版本>.zip
|
||||||
python package_skill.py --with-deps # 含 playwright-core,~14MB(目标机全程离线)
|
python package_skill.py --with-deps # 含 playwright-core,~14MB(目标机全程离线)
|
||||||
|
|
||||||
# ② 新机器:解压到任意目录(推荐 ~/.workbuddy/skills/)
|
# ② 新机器:解压到任意目录(放进上表任一「个人级」技能目录即可被该智能体识别)
|
||||||
bash setup_env.sh --install # 自检 + 装缺项(先装后判定,装成功即算就绪)
|
bash setup_env.sh --install # 自检 + 装缺项(先装后判定,装成功即算就绪)
|
||||||
|
|
||||||
# ③ 跑一遍端到端(可选,验证链路)
|
# ③ 跑一遍端到端(可选,验证链路)
|
||||||
@@ -249,8 +285,8 @@ python scripts/qc_check.py --project . && node scripts/cover_build.mjs .
|
|||||||
```
|
```
|
||||||
|
|
||||||
**依赖探测顺序**(都尽量用系统已有的,避免下载):
|
**依赖探测顺序**(都尽量用系统已有的,避免下载):
|
||||||
- Python:先找托管 venv,再 `python3` / `python`
|
- Python:先找 WorkBuddy 托管 venv,再 `python3` / `python`
|
||||||
- Node:`node -v` ≥18;没有则扫 `~/.workbuddy/binaries/node/versions/*`
|
- Node:`node -v` ≥18;没有则扫 `~/.workbuddy/binaries/node/versions/*`(WorkBuddy 托管路径)
|
||||||
- 浏览器:Chrome → Edge → playwright chromium → `BROWSER_PATH` 环境变量
|
- 浏览器:Chrome → Edge → playwright chromium → `BROWSER_PATH` 环境变量
|
||||||
- ffmpeg:PATH → `imageio_ffmpeg.get_ffmpeg_exe()`(pip 装依赖时自带静态二进制)
|
- ffmpeg:PATH → `imageio_ffmpeg.get_ffmpeg_exe()`(pip 装依赖时自带静态二进制)
|
||||||
- GSAP:包内 `assets/gsap.min.js`(离线,不外链)
|
- GSAP:包内 `assets/gsap.min.js`(离线,不外链)
|
||||||
@@ -263,6 +299,9 @@ python scripts/qc_check.py --project . && node scripts/cover_build.mjs .
|
|||||||
- 本技能**原创代码与文档**:MIT © 2026 **Moh**(见 `LICENSE`)。
|
- 本技能**原创代码与文档**:MIT © 2026 **Moh**(见 `LICENSE`)。
|
||||||
- **画面风格目录**(`references/style-catalog.md` / `.json`)由 [nexu-io/html-video](https://github.com/nexu-io/html-video)
|
- **画面风格目录**(`references/style-catalog.md` / `.json`)由 [nexu-io/html-video](https://github.com/nexu-io/html-video)
|
||||||
(Apache-2.0)的模板设计规范转写而来,已保留署名;转写文本按 Apache-2.0 分发。
|
(Apache-2.0)的模板设计规范转写而来,已保留署名;转写文本按 Apache-2.0 分发。
|
||||||
|
- **方法论思路来源**:[Vincentwei1021/anything2explainer](https://github.com/Vincentwei1021/anything2explainer)
|
||||||
|
(词边界字幕 / 两级时钟 / 语速标定 / QC 判据)。两者均为**他人独立项目,与本技能不是同一作者**。
|
||||||
|
确定性 seek 渲染器、`B()` 节拍锚定、封面双方案为本项目原创。
|
||||||
- **打包内置**:GSAP 3.13(GreenSock 标准「no charge」许可,见 `assets/gsap-README.md`)、
|
- **打包内置**:GSAP 3.13(GreenSock 标准「no charge」许可,见 `assets/gsap-README.md`)、
|
||||||
playwright-core(Apache-2.0)。
|
playwright-core(Apache-2.0)。
|
||||||
- 完整清单与逐条授权见 **`THIRD_PARTY_NOTICES.md`**。发布/再分发前请连同该文件一起带上。
|
- 完整清单与逐条授权见 **`THIRD_PARTY_NOTICES.md`**。发布/再分发前请连同该文件一起带上。
|
||||||
+94
-100
@@ -1,123 +1,110 @@
|
|||||||
# Third-Party Notices
|
# 第三方声明
|
||||||
|
|
||||||
`html-explainer` is MIT-licensed (see [LICENSE](LICENSE)). That license covers the
|
`html-explainer` 以 MIT 许可发布(见 [LICENSE](LICENSE))。该许可覆盖本项目原创的
|
||||||
**original code and documentation** authored for this project.
|
**代码与文档**。
|
||||||
|
|
||||||
It does **not** cover the third-party components and derived content listed below.
|
它**不**覆盖下面列出的第三方组件与衍生内容 —— 那些仍适用其各自的条款,按要求在此复现。
|
||||||
Those remain under their own terms, reproduced here as required.
|
|
||||||
|
|
||||||
> **If you redistribute this project (fork, zip, package, mirror, or ship it inside a
|
> **如果你要再分发本项目(fork、打包 zip、发布为包、镜像,或放进某个产品里),必须把本文件
|
||||||
> product), you must carry this file along with it.**
|
> 一起带上。**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Bundled in this repository
|
## 1. 随仓库打包的组件
|
||||||
|
|
||||||
### GSAP 3.13.0 — `assets/gsap.min.js`
|
### GSAP 3.13.0 — `assets/gsap.min.js`
|
||||||
|
|
||||||
- **Copyright**: © 2025 GreenSock. All rights reserved. Author: Jack Doyle.
|
- **版权**:© 2025 GreenSock. All rights reserved. 作者 Jack Doyle。
|
||||||
- **License**: GreenSock standard "no charge" license — <https://gsap.com/standard-license>
|
- **许可**:GreenSock standard "no charge" 许可 —— <https://gsap.com/standard-license>
|
||||||
- **Status**: Bundled verbatim for offline rendering. Not modified.
|
- **状态**:为离线渲染逐字内置,未做修改。
|
||||||
- **Note**: This is **not** an OSI license. GSAP is free to use, including commercially,
|
- **说明**:这**不是**一个 OSI 许可。GSAP 在其标准许可条款下可免费使用(含商业用途)。
|
||||||
under its standard license terms. The copyright header inside `assets/gsap.min.js`
|
`assets/gsap.min.js` 里的版权头不得移除。上游 README 一并保留在 `assets/gsap-README.md`。
|
||||||
must not be removed. Upstream README is kept alongside it at
|
|
||||||
`assets/gsap-README.md`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Runtime dependencies (installed, not vendored)
|
## 2. 运行时依赖(安装,而非随包内置)
|
||||||
|
|
||||||
These are declared as dependencies and fetched by `bash setup_env.sh --install`.
|
这些在依赖清单中声明,由 `bash setup_env.sh --install` 拉取,**不**提交进本仓库。
|
||||||
They are not committed to this repository.
|
|
||||||
|
|
||||||
| Component | Version (tested) | License | Upstream |
|
| 组件 | 版本(已测) | 许可 | 上游 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `edge-tts` | 7.2.8 (pinned) | LGPL-3.0 | <https://github.com/rany2/edge-tts> |
|
| `edge-tts` | 7.2.8(钉死) | LGPL-3.0 | <https://github.com/rany2/edge-tts> |
|
||||||
| `playwright-core` | 1.63.0 | Apache-2.0 | <https://github.com/microsoft/playwright> |
|
| `playwright-core` | 1.63.0 | Apache-2.0 | <https://github.com/microsoft/playwright> |
|
||||||
| `numpy` | 2.5.3 | BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0 | <https://numpy.org> |
|
| `numpy` | 2.5.3 | BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0 | <https://numpy.org> |
|
||||||
| `pillow` | 12.3.0 | MIT-CMU | <https://python-pillow.org> |
|
| `pillow` | 12.3.0 | MIT-CMU | <https://python-pillow.org> |
|
||||||
| `imageio-ffmpeg` | 0.6.0 | BSD-2-Clause | <https://github.com/imageio/imageio-ffmpeg> |
|
| `imageio-ffmpeg` | 0.6.0 | BSD-2-Clause | <https://github.com/imageio/imageio-ffmpeg> |
|
||||||
| `ffmpeg` (binary) | via `imageio-ffmpeg` | LGPL-2.1+ / GPL-2.0+ (varies by build) | <https://ffmpeg.org/legal.html> |
|
| `ffmpeg`(二进制) | 来自 `imageio-ffmpeg` | LGPL-2.1+ / GPL-2.0+(随构建而异) | <https://ffmpeg.org/legal.html> |
|
||||||
|
|
||||||
**A note on `edge-tts` (LGPL-3.0) and `ffmpeg`:** these are invoked as **separate
|
**关于 `edge-tts`(LGPL-3.0)与 `ffmpeg`**:本项目以**独立进程调用 / 作为未修改的库导入**
|
||||||
processes / imported as unmodified libraries**; this project does not statically link
|
的方式使用它们,不做静态链接,也不分发修改过的副本。如果你打算再分发一个**打包的** ffmpeg
|
||||||
them or ship modified copies. If you plan to redistribute a *bundled* binary build of
|
二进制构建,请先看 FFmpeg 自己的许可页面 —— 编译选项决定该二进制属于 LGPL 还是 GPL。
|
||||||
ffmpeg, review FFmpeg's own licensing page — build flags determine whether a given
|
|
||||||
binary is LGPL or GPL.
|
|
||||||
|
|
||||||
`edge-tts` is pinned to `7.2.8` intentionally: the 7.x series has had breaking changes
|
`edge-tts` 是**刻意**钉在 `7.2.8` 的:7.x 系列在其语音边界 API 上有过破坏性变更,而本项目
|
||||||
in its speech-boundary API, and this project depends on `WordBoundary` events.
|
依赖 `WordBoundary` 事件。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Derived content — the style catalog
|
## 3. 衍生内容 —— 风格目录
|
||||||
|
|
||||||
`references/style-catalog.md` and `references/style-catalog.json` are a **transcription
|
`references/style-catalog.md` 与 `references/style-catalog.json` 是**对第三方模板设计规范的
|
||||||
of third-party template design specifications** — canvas size, typography, timeline
|
转写** —— 画布尺寸、字体排印、时间轴结构、配色纪律 —— 覆盖 23 种具名风格。其中**不包含**任何
|
||||||
structure, colour discipline — for 23 named styles. No template source code, markup, or
|
上游的模板源码、标记或素材。记录的只有事实性的设计参数与风格名称,目的是让人能从零写出一个
|
||||||
asset from any upstream is included. Only factual design parameters and style names are
|
兼容的画面帧。
|
||||||
recorded, so that a compatible frame can be authored from scratch.
|
|
||||||
|
|
||||||
This catalog was produced by `scripts/import_styles.py`, which reads an installed copy of
|
该目录由 `scripts/import_styles.py` 产出:它读取一份**已安装的** html-video,把规范转写成纯
|
||||||
html-video and writes the specification out as plain text. **It is a one-time porting
|
文本。**它是一个移植期的一次性工具。** 跑一条 html-explainer 视频从不读取 html-video,
|
||||||
tool.** Running an html-explainer video never reads html-video, and no html-video code is
|
任何环节都不会执行或链接 html-video 的代码。
|
||||||
executed or linked at any point.
|
|
||||||
|
|
||||||
### 3.1 `nexu-io/html-video` — 23 templates
|
### 3.1 `nexu-io/html-video` —— 23 个模板
|
||||||
|
|
||||||
- **Copyright**: the html-video authors (Open Design / nexu-io)
|
- **版权**:html-video 的作者(Open Design / nexu-io)
|
||||||
- **License**: Apache-2.0
|
- **许可**:Apache-2.0
|
||||||
- **Source**: <https://github.com/nexu-io/html-video>
|
- **来源**:<https://github.com/nexu-io/html-video>
|
||||||
- **What was derived**: design specifications and style identifiers for all 23 templates
|
- **衍生了什么**:全部 23 个模板的设计规范与风格标识符(`frame-bold-poster` …
|
||||||
(`frame-bold-poster` … `vfx-text-cursor`), transcribed into `references/style-catalog.*`.
|
`vfx-text-cursor`),转写进 `references/style-catalog.*`。
|
||||||
- **Changes made**: specifications reformatted into a flat Markdown/JSON catalog; each
|
- **做了哪些改动**:规范重排为扁平的 Markdown / JSON 目录;每个模板分类为 `rich`
|
||||||
template classified as `rich` (single-file CSS `@keyframes`) or `gsap` (multi-composition);
|
(单文件 CSS `@keyframes`)或 `gsap`(多 composition);补充了改编成本与字幕禁区约束的
|
||||||
annotated with adaptation cost and subtitle-safe-zone constraints. Original template
|
标注。原始模板代码、composition 与素材**没有**被复制。
|
||||||
code, compositions, and assets were **not** copied.
|
- Apache-2.0 要求衍生作品的接收者获得一份许可副本。逐字副本见
|
||||||
- Apache-2.0 requires that recipients of a derivative work receive a copy of the license.
|
[`licenses/Apache-2.0.txt`](licenses/Apache-2.0.txt)。上游仓库不含 `NOTICE` 文件,
|
||||||
A verbatim copy is included at [`licenses/Apache-2.0.txt`](licenses/Apache-2.0.txt).
|
因此不涉及 `NOTICE` 传递(Apache-2.0 §4(d))。
|
||||||
The upstream repository ships no `NOTICE` file, so no `NOTICE` propagation applies
|
|
||||||
(Apache-2.0 §4(d)).
|
|
||||||
|
|
||||||
### 3.2 Upstreams of the html-video templates
|
### 3.2 html-video 模板的上游
|
||||||
|
|
||||||
`references/style-catalog.json` records a `via` field per style, preserving the
|
`references/style-catalog.json` 为每种风格记录了 `via` 字段,保留了 html-video 为其模板附带的
|
||||||
provenance that html-video attaches to each of its templates. Seven of the 23 styles
|
来源信息。23 种风格中有 7 种可追溯到更早的 MIT 许可项目。由于本项目的目录描述了同样的风格,
|
||||||
trace back to an earlier MIT-licensed project. Those notices are reproduced here because
|
这些声明在此一并复现。
|
||||||
this project's catalog describes the same styles.
|
|
||||||
|
|
||||||
Exact mapping, as recorded in the catalog:
|
按目录中记录的精确对应关系:
|
||||||
|
|
||||||
#### `frontend-slides` — 4 styles
|
#### `frontend-slides` —— 4 种风格
|
||||||
|
|
||||||
- **Copyright**: Zara Zhang © 2025
|
- **版权**:Zara Zhang © 2025
|
||||||
- **License**: MIT
|
- **许可**:MIT
|
||||||
- **Source**: <https://github.com/zarazhangrui/frontend-slides>
|
- **来源**:<https://github.com/zarazhangrui/frontend-slides>
|
||||||
- **Styles**: `frame-bold-poster`, `frame-bold-signal`, `frame-creative-voltage`,
|
- **风格**:`frame-bold-poster`、`frame-bold-signal`、`frame-creative-voltage`、
|
||||||
`frame-electric-studio`
|
`frame-electric-studio`
|
||||||
|
|
||||||
#### `huashu-design` — 3 styles
|
#### `huashu-design` —— 3 种风格
|
||||||
|
|
||||||
- **Copyright**: alchaincyf (花叔 · 花生) © 2026
|
- **版权**:alchaincyf(花叔 · 花生)© 2026
|
||||||
- **License**: MIT
|
- **许可**:MIT
|
||||||
- **Source**: <https://github.com/alchaincyf/huashu-design>
|
- **来源**:<https://github.com/alchaincyf/huashu-design>
|
||||||
- **Styles**: `frame-build-minimal`, `frame-pentagram-stat`, `frame-takram-organic`
|
- **风格**:`frame-build-minimal`、`frame-pentagram-stat`、`frame-takram-organic`
|
||||||
|
|
||||||
#### `hyperframes-student-kit` (Nate Herk) — 1 style
|
#### `hyperframes-student-kit`(Nate Herk)—— 1 种风格
|
||||||
|
|
||||||
- **Copyright**: Nate Herk
|
- **版权**:Nate Herk
|
||||||
- **License**: MIT (as recorded in the catalog's `license` field for this entry)
|
- **许可**:MIT(按目录中该条目的 `license` 字段记录)
|
||||||
- **Styles**: `frame-product-promo-30s` — recorded upstream as a fork of
|
- **风格**:`frame-product-promo-30s` —— 上游记录为 `linear-promo-30s` 的 fork,
|
||||||
`linear-promo-30s`, with brand-specific copy and assets replaced by generic
|
品牌相关的文案与素材已替换为通用占位内容。
|
||||||
placeholders.
|
|
||||||
|
|
||||||
html-video's own `ATTRIBUTIONS.md` documents the first two upstreams and states its rule
|
html-video 自己的 `ATTRIBUTIONS.md` 记录了前两个上游,并给出了它对此类模板的处理原则:静态
|
||||||
for all such templates: static slide designs were re-expressed as original CSS/SVG
|
幻灯片设计被重新表达为原创的 CSS/SVG 关键帧时间轴,配以新的示例数据,不做逐字源码复制;部分
|
||||||
keyframe timelines, with new sample data and no verbatim source copying, and the studio
|
风格上挂着的设计事务所名字(Pentagram、Build、Takram)记录为**仅表灵感来源,绝不表示隶属或
|
||||||
names attached to some styles (Pentagram, Build, Takram) are recorded as **inspiration
|
背书**。本项目沿用这一立场。
|
||||||
only, never as affiliation or endorsement**. This project inherits that position.
|
|
||||||
|
|
||||||
MIT License notice for all three upstreams above:
|
上述三个上游的 MIT 许可声明:
|
||||||
|
|
||||||
```
|
```
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@@ -139,34 +126,41 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.3 Studio names — inspiration only, no affiliation
|
### 3.3 设计事务所名称 —— 仅表灵感,无隶属关系
|
||||||
|
|
||||||
Some style names reference real design studios or publications as a record of stylistic
|
部分风格名引用了真实的设计事务所或出版物,作为风格灵感来源的记录(Pentagram、Build、
|
||||||
inspiration (Pentagram, Build, Takram, NYT, Vignelli, Swiss Grid). These are **factual
|
Takram、NYT、Vignelli、Swiss Grid)。这些**仅为事实性的灵感说明**。本项目 —— 以及它之前的
|
||||||
statements of inspiration only**. This project — and html-video before it — is **not
|
html-video —— **与其中任何一方均无隶属、背书或赞助关系**。项目中不含任何第三方的 logo、
|
||||||
affiliated with, endorsed by, or sponsored by** any of them. No third-party logo,
|
字标、字体文件或品牌素材;所有画面都用系统字体从零创作。
|
||||||
wordmark, typeface file, or branded asset is included; all frames are authored from
|
|
||||||
scratch with system fonts.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Design lineage of the workflow
|
## 4. 方法论上的思路来源
|
||||||
|
|
||||||
The narration/audio-sync methodology (word-boundary subtitles, two-level clock, speech-rate
|
解说与音画同步方法论(词边界字幕、两级时钟、语速标定、QC 判据)参考了下列项目,它们
|
||||||
calibration, QC criteria) was developed in the author's earlier private projects,
|
**由不同作者独立开发,与本项目不是同一作者**:
|
||||||
`anything2explainer` and `html-video-workbuddy-driver`. Both are the author's own work.
|
|
||||||
No code from either is shipped or required at runtime; the names appear only in comments
|
- **[Vincentwei1021/anything2explainer](https://github.com/Vincentwei1021/anything2explainer)**
|
||||||
and historical notes.
|
—— Remotion + TypeScript 路线。本项目从其承继了「解说 + 音画同步」这套工程方法论。
|
||||||
|
- **[nexu-io/html-video](https://github.com/nexu-io/html-video)** —— Apache-2.0。除第 3.1 节
|
||||||
|
所述的风格目录外,其「在本机把 HTML 变成 MP4」的整体工程取舍亦有参考价值。
|
||||||
|
|
||||||
|
原始思路作者亦在自己的私有工作中做过 WorkBuddy 平台的适配版本(`anything2explainer` 技能的
|
||||||
|
本地特化版、`html-video-workbuddy-driver`)。**这些适配版不随本仓库分发,运行时也不需要。**
|
||||||
|
名字仅出现在注释与历史记录中。
|
||||||
|
|
||||||
|
**本项目自己的设计**:确定性 seek 渲染器、`B()` 节拍锚定、封面双方案,以及
|
||||||
|
`lint_frames.py` / `qc_check.py` 的判据。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Summary for redistributors
|
## 再分发者须知
|
||||||
|
|
||||||
| Must ship with your copy | Why |
|
| 必须随你的副本一起提供 | 原因 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `LICENSE` | MIT terms for the original code |
|
| `LICENSE` | 原创代码的 MIT 条款 |
|
||||||
| `THIRD_PARTY_NOTICES.md` (this file) | Apache-2.0 §4 + MIT notice retention |
|
| `THIRD_PARTY_NOTICES.md`(本文件) | Apache-2.0 §4 + MIT 通知保留要求 |
|
||||||
| `licenses/Apache-2.0.txt` | Apache-2.0 §4(a) requires a copy of the license |
|
| `licenses/Apache-2.0.txt` | Apache-2.0 §4(a) 要求附带一份许可副本 |
|
||||||
| `assets/gsap-README.md` | GSAP license reference; keep the header in `gsap.min.js` intact |
|
| `assets/gsap-README.md` | GSAP 许可参考;并保持 `gsap.min.js` 内的版权头完整 |
|
||||||
|
|
||||||
*Last reviewed: 2026-09-21.*
|
*最后审阅:2026-09-21。*
|
||||||
+16
-3
@@ -24,7 +24,16 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
// 本脚本放在**项目**的 script/ 下(不是技能目录里),所以不能靠相对路径找技能根。
|
// 本脚本放在**项目**的 script/ 下(不是技能目录里),所以不能靠相对路径找技能根。
|
||||||
// 解析顺序:env → 向上找含 node/playwright-core 的目录 → ~/.workbuddy/skills/html-explainer
|
// 解析顺序:env → 向上逐级找含 node/playwright-core 的目录 → 各智能体技能目录下的同名落点。
|
||||||
|
// 与平台无关:任何一处命中即可,命中不了就报错退出(不会静默用错目录)。
|
||||||
|
const SKILL_SUBDIRS = [
|
||||||
|
['.workbuddy', 'skills'], // WorkBuddy
|
||||||
|
['.claude', 'skills'], // Claude Code
|
||||||
|
['.codex', 'skills'], // OpenAI Codex
|
||||||
|
['.gemini', 'skills'], // Gemini CLI
|
||||||
|
['.cursor', 'skills'], // Cursor
|
||||||
|
['.agents', 'skills'], // 通用约定
|
||||||
|
];
|
||||||
function resolveSkillRoot() {
|
function resolveSkillRoot() {
|
||||||
const cands = [];
|
const cands = [];
|
||||||
if (process.env.HTML_EXPLAINER_ROOT) cands.push(process.env.HTML_EXPLAINER_ROOT);
|
if (process.env.HTML_EXPLAINER_ROOT) cands.push(process.env.HTML_EXPLAINER_ROOT);
|
||||||
@@ -35,8 +44,12 @@ function resolveSkillRoot() {
|
|||||||
if (p === d) break;
|
if (p === d) break;
|
||||||
d = p;
|
d = p;
|
||||||
}
|
}
|
||||||
if (process.env.USERPROFILE) cands.push(path.join(process.env.USERPROFILE, '.workbuddy', 'skills', 'html-explainer'));
|
const homes = [process.env.USERPROFILE, process.env.HOME].filter(Boolean);
|
||||||
if (process.env.HOME) cands.push(path.join(process.env.HOME, '.workbuddy', 'skills', 'html-explainer'));
|
for (const home of homes) {
|
||||||
|
for (const seg of SKILL_SUBDIRS) {
|
||||||
|
cands.push(path.join(home, ...seg, 'html-explainer'));
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const c of cands) {
|
for (const c of cands) {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(path.join(c, 'node', 'node_modules', 'playwright-core', 'package.json'))) return c;
|
if (fs.existsSync(path.join(c, 'node', 'node_modules', 'playwright-core', 'package.json'))) return c;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ info() { echo " → $1"; }
|
|||||||
echo "== html-explainer 环境自检 =="
|
echo "== html-explainer 环境自检 =="
|
||||||
|
|
||||||
# ---------- Python ----------
|
# ---------- Python ----------
|
||||||
|
# 候选顺序:WorkBuddy 托管 venv(本机既有依赖,优先复用)→ 系统 python3 / python。
|
||||||
|
# 前两个是 WorkBuddy 专属路径,其它平台探不到就自动跳过,不影响使用。
|
||||||
PY=""
|
PY=""
|
||||||
for c in "$HOME/.workbuddy/binaries/python/envs/default/Scripts/python.exe" \
|
for c in "$HOME/.workbuddy/binaries/python/envs/default/Scripts/python.exe" \
|
||||||
"$HOME/.workbuddy/binaries/python/envs/default/bin/python" \
|
"$HOME/.workbuddy/binaries/python/envs/default/bin/python" \
|
||||||
@@ -63,6 +65,7 @@ if command -v node >/dev/null 2>&1; then
|
|||||||
NV="$(node_major)"
|
NV="$(node_major)"
|
||||||
if [ "${NV:-0}" -ge 18 ]; then ok "Node: $(node -v)"; else miss "Node ≥18 需要,当前 $(node -v)"; fi
|
if [ "${NV:-0}" -ge 18 ]; then ok "Node: $(node -v)"; else miss "Node ≥18 需要,当前 $(node -v)"; fi
|
||||||
else
|
else
|
||||||
|
# 兜底:WorkBuddy 托管的 Node(非标准安装,路径带版本号)。仅本机可用,其它平台跳过。
|
||||||
for d in "$HOME/.workbuddy/binaries/node/versions"/*/; do
|
for d in "$HOME/.workbuddy/binaries/node/versions"/*/; do
|
||||||
if [ -x "$d/node.exe" ] || [ -x "$d/node" ]; then
|
if [ -x "$d/node.exe" ] || [ -x "$d/node" ]; then
|
||||||
export PATH="$d:$PATH"; ok "Node(托管): $(node -v)"; break
|
export PATH="$d:$PATH"; ok "Node(托管): $(node -v)"; break
|
||||||
|
|||||||
Reference in new issue
Block a user