From 2581e2c685a997b22c273eaee10fe0d40f8eac1d Mon Sep 17 00:00:00 2001 From: MohZhuang <1917703871@qq.com> Date: Mon, 21 Sep 2026 13:09:52 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=87=8D=E6=9E=84=20README=20=E5=B9=B6?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=96=87=E6=A1=A3=E8=AF=AD=E8=A8=80=E4=B8=BA?= =?UTF-8?q?=E4=B8=AD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 原称两个方法来源项目「均为作者本人作品」, 与事实不符,已改为「他人独立项目」并补充来源链接。 --- .github/ISSUE_TEMPLATE/bug_report.yml | 71 ++- .github/ISSUE_TEMPLATE/config.yml | 14 +- .github/ISSUE_TEMPLATE/feature_request.yml | 38 +- .github/PULL_REQUEST_TEMPLATE.md | 36 +- .github/workflows/ci.yml | 24 +- CHANGELOG.md | 186 +++--- CONTRIBUTING.md | 168 +++-- README.en.md | 684 ++++++++------------- README.md | 588 +++++++----------- SKILL.md | 47 +- THIRD_PARTY_NOTICES.md | 194 +++--- scripts/peek_frame.mjs | 19 +- setup_env.sh | 3 + 13 files changed, 874 insertions(+), 1198 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c71dd6e..0acad67 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,75 +1,74 @@ -name: Bug report -description: Something rendered wrong, failed silently, or crashed. +name: Bug 报告 +description: 画面渲错了、静默失败,或者直接崩了。 title: "[Bug] " labels: ["bug"] body: - type: markdown attributes: 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 id: stage attributes: - label: Which stage failed? + label: 哪个阶段出错了? options: - - "0 · new_project (scaffold)" - - "1 · tts_build (voiceover)" - - "2 · timeline_build (global timeline)" - - "3 · subs (subtitles / beats)" - - "4 · lint_frames (static audit)" - - "5 · render_video (rendering)" - - "6 · qc_check (audit)" - - "7 · cover_build (covers)" - - "setup_env / environment" - - "Not sure" + - "0 · new_project(建项目)" + - "1 · tts_build(配音)" + - "2 · timeline_build(全局时间轴)" + - "3 · subs(字幕 / 节拍)" + - "4 · lint_frames(静态体检)" + - "5 · render_video(渲染)" + - "6 · qc_check(体检)" + - "7 · cover_build(封面)" + - "setup_env / 环境问题" + - "不确定" validations: required: true - type: textarea id: what-happened attributes: - label: What happened? - description: Include the exact command you ran and the full output. + label: 发生了什么? + description: 请附上你执行的完整命令与完整输出。 placeholder: | - Command: node scripts/render_video.mjs . --preview 30 - Expected: 30s preview with animated cards - Actual: preview renders, but the number counter never moves + 命令:node scripts/render_video.mjs . --preview 30 + 期望:30 秒预览,卡片有动画 + 实际:预览渲出来了,但数字计数器一直不动 validations: required: true - type: textarea id: symptom-class attributes: - label: Did it fail loudly or silently? + label: 它是报错了,还是静默出错? 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: required: false - type: textarea id: environment attributes: - label: Environment - description: "Run these and paste the result." + label: 环境 + description: "跑一下这些,把结果贴上来。" value: | - - OS: - - `node -v`: - - `python -V`: - - Shell (Git Bash / PowerShell / zsh): - - Browser used for rendering (Chrome / Edge / playwright chromium): - - ffmpeg source (PATH / imageio-ffmpeg): + - 操作系统: + - `node -v`: + - `python -V`: + - Shell(Git Bash / PowerShell / zsh): + - 渲染用浏览器(Chrome / Edge / playwright chromium): + - ffmpeg 来源(PATH / imageio-ffmpeg): validations: required: true - type: input id: version attributes: - label: Version - description: The `version:` field in SKILL.md, or the release tag. + label: 版本 + description: SKILL.md 里的 `version:` 字段,或发布 tag。 placeholder: "1.2.2" validations: required: true @@ -77,7 +76,7 @@ body: - type: textarea id: qc-output attributes: - label: QC output (if you got as far as rendering) - description: Paste `out/qc_report.md`, or attach the contact sheet. + label: QC 输出(如果你已经跑到渲染那一步) + description: 贴 `out/qc_report.md`,或附上抽帧速览图。 validations: required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ff9b01d..ff93386 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Read the frame contract first + - name: 先读画面契约 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. - - name: Check the lessons ledger + about: 绝大多数「我的画面能渲但看着不对」都能被八条契约规则解释。 + - name: 先查踩坑台账 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. - - name: Setup and environment issues - 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: 已知的静默失败模式及其定位方法。提 issue 之前值得先搜一遍。 + - name: 安装与环境问题 + url: https://github.com/OneMoh/html-explainer/blob/main/README.md + about: 先跑 `bash setup_env.sh`,再对照 README 的「疑难排查」一节。 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index e40b784..1383577 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,51 +1,49 @@ -name: Feature request -description: Propose a new capability, style, or workflow improvement. +name: 功能建议 +description: 提议一个新能力、新风格,或流程改进。 title: "[Feature] " labels: ["enhancement"] body: - type: markdown attributes: value: | - This project deliberately keeps a small surface: HTML frames in, MP4 out, with - audio-video sync that actually holds. Proposals that add a build system, a plugin - API, or a cloud dependency are usually out of scope — but say so if you think - there's an important exception. + 本项目刻意保持很小的接口面:HTML 帧进,MP4 出,音画同步稳得住。会增加构建系统、 + 插件 API 或云依赖的提议,一般都在范围之外 —— 但如果你认为有重要的例外,请说明。 - type: textarea id: problem attributes: - label: What problem does this solve? - description: Describe the situation you're in, not the API you want. + label: 它解决什么问题? + description: 描述你处的场景,而不是你想要的 API。 validations: required: true - type: textarea id: proposal attributes: - label: Proposed approach + label: 你建议的做法 validations: required: true - type: dropdown id: area attributes: - label: Area + label: 涉及范围 options: - - Visual styles / templates - - Narration / TTS - - Subtitles / beat sync - - Rendering - - QC / auditing - - Covers - - Docs - - Other + - 画面风格 / 模板 + - 解说 / TTS + - 字幕 / 节拍同步 + - 渲染 + - QC / 体检 + - 封面 + - 文档 + - 其它 validations: required: true - type: textarea id: alternatives attributes: - label: Alternatives considered - description: Including "just do it manually" — it's a valid answer. + label: 考虑过的替代方案 + description: 包括「干脆手工做」—— 这也是个有效答案。 validations: required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index dc8bfdb..6f0aef6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,32 +1,32 @@ -## What this changes +## 这次改了什么 - + -## Why +## 为什么要改 -## How it was verified +## 怎么验证的 -## Checklist +## 检查清单 -- [ ] `python scripts/lint_frames.py --project ` reports no FAILs for frames I touched -- [ ] I actually rendered the change and looked at the output -- [ ] No external fonts, CDN links, or hardcoded colour literals introduced -- [ ] New lesson (if any) appended to `references/lessons.md` with the next number -- [ ] `python scripts/check_integrity.py` passes -- [ ] `README.md` and `README.en.md` updated together if user-facing behaviour changed -- [ ] `THIRD_PARTY_NOTICES.md` updated if third-party content was added or re-derived +- [ ] `python scripts/lint_frames.py --project ` 对我动过的帧没有 FAIL +- [ ] 我真的渲染过这次改动,并看过输出 +- [ ] 没有引入外部字体、CDN 链接或硬编码颜色字面量 +- [ ] 新增的踩坑记录(如果有)已按下一个编号追加到 `references/lessons.md` +- [ ] `python scripts/check_integrity.py` 通过 +- [ ] 涉及使用者可见行为的改动,`README.md` 与 `README.en.md` 已同步更新 +- [ ] 如果新增或再衍生了第三方内容,`THIRD_PARTY_NOTICES.md` 已更新 -## Related issues +## 相关 issue diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a6ea80..68f5125 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: branches: [main] workflow_dispatch: -# Only the latest run per branch matters; cancel superseded ones. +# 每个分支只关心最新一次运行;被取代的自动取消。 concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -19,8 +19,8 @@ jobs: strategy: fail-fast: false matrix: - # Windows + Git Bash is the primary development platform, so it is - # tested alongside Linux rather than treated as an afterthought. + # Windows + Git Bash 是主要开发平台,所以和 Linux 一起测, + # 而不是被当成事后补的。 os: [ubuntu-latest, windows-latest] python: ["3.9", "3.13"] @@ -35,27 +35,27 @@ jobs: with: node-version: "22" - # --- Repo self-consistency: version drift, catalog drift, stray CDN links --- + # --- 仓库自洽性:版本号漂移、风格目录漂移、残留外链 --- - name: Repository integrity check run: python scripts/check_integrity.py - # --- Python scripts must at least compile on the oldest supported version --- + # --- Python 脚本至少在支持的最低版本上要能编译 --- - name: Compile Python scripts run: python -m compileall -q scripts - # --- Frame audit tool must be importable and its CLI must respond --- + # --- 画面体检工具要能被导入,且命令行要能响应 --- - name: Lint tool smoke test run: python scripts/lint_frames.py --help - name: Integrity checker smoke test run: python scripts/check_integrity.py --root . - # --- Shell entrypoint syntax --- + # --- Shell 入口语法 --- - name: Check setup_env.sh syntax shell: bash run: bash -n setup_env.sh - # --- Renderer / cover builder / peek tool syntax --- + # --- 渲染器 / 封面器 / 单帧速览工具的语法 --- - name: Check Node module syntax shell: bash run: | @@ -69,11 +69,9 @@ jobs: shell: bash run: | set -e - # Frames must never reach out to a CDN or web font: offline rendering - # degrades silently rather than failing. - # Scope to HTML only — markdown docs legitimately *mention* these URLs - # (e.g. the guide explaining that you must delete them), and GSAP's own - # bundled README documents its CDN option. + # 画面帧绝不允许引 CDN 或网络字体:离线渲染不会报错,只会静默劣化。 + # 只扫 HTML —— markdown 文档里*提到*这些 URL 是合理的 + # (比如正是指南在教你要把它们删掉),而 GSAP 自带的 README 也会介绍它的 CDN 用法。 hits=$(grep -rInE --include='*.html' \ 'https?://[^"'"'"' ]*(fonts\.googleapis|fonts\.gstatic|cdn\.)' . \ || true) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea56952..9d29fc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), +版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。 -Versions before `1.2.2` were private, internal releases. `1.2.2` is the first public -release on GitHub. +`1.2.2` 之前的版本都是私有内部发布。**`1.2.2` 是 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 - eight frame-contract rules plus known pitfalls before spending time on a full render: - external fonts / CDN links, hardcoded colour literals, missing `window.__tl` - registration, CDN GSAP, wall-clock logic (`setInterval`, `requestAnimationFrame` - counters), CSS `transition` entrances, `B('…') || number` fallbacks, content intruding - into the subtitle safe zone, and missing CJK font families. -- **`scripts/peek_frame.mjs`** — single-frame viewer. Seeks one scene to a few time points - and screenshots them in seconds, so frame design can be judged without rendering the - whole video. `--at` takes percentages, not frame numbers. -- **Bilingual project README** (`README.md` in Chinese, `README.en.md` in English). -- **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. +- **`scripts/lint_frames.py`** —— 渲染前的画面静态体检。在花时间渲染整片之前,先检查八条 + 画面契约与已知坑:外部字体 / CDN 引用、硬编码颜色字面量、缺失 `window.__tl` 注册、 + CDN 版 GSAP、墙钟逻辑(`setInterval`、`requestAnimationFrame` 计数)、CSS `transition` + 入场、`B('…') || 数字` 兜底写法、侵入字幕禁区的内容、缺失中文字体族。 +- **`scripts/peek_frame.mjs`** —— 单帧速览。把一个场景 seek 到若干时点截图,耗时是**秒级**, + 于是不用渲全片就能判断画面设计。`--at` 收的是百分比,不是帧号。 +- **双语项目 README**(`README.md` 中文为默认,`README.en.md` 英文)。 +- **开源治理文件**:`LICENSE`(MIT)、`THIRD_PARTY_NOTICES.md`、`CHANGELOG.md`、 + `CONTRIBUTING.md`、`licenses/Apache-2.0.txt`、`.gitignore`、`.gitattributes`、 + GitHub issue / PR 模板,以及 CI 工作流。 -### Fixed +### 修复 -- **`speech_end_sec` disagreed between a cold run and a cached re-run** (lessons #31). - `_result()` measured the already-trimmed output file, so the head-trim amount was - subtracted twice: a cold run reported `7.541s`, a cached re-run reported `7.696s` — - 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. +- **`speech_end_sec` 在冷跑与缓存重跑之间不一致**(lessons #31)。`_result()` 量的是已经裁过 + 静音的输出文件,于是头部裁剪量被减了两次:冷跑报 `7.541s`,缓存重跑报 `7.696s` —— + 0.155s 的差异破坏了「两次渲染逐帧一致」的保证,并让末块字幕提前消失。现在统一存**裁后 + 坐标系**下的值。 -### Changed +### 变更 -- Documentation: `scripts/lint_frames.py` and `scripts/peek_frame.mjs` added to the - command sequence and the file reference table (both previously undocumented). -- Documentation: the lint stage is now an explicit step in the workflow, between subtitle - generation and rendering. -- Documentation: removed references to private client projects so the skill can ship - publicly. -- Documented licensing: the style catalog is derived from Apache-2.0 content and now - carries full upstream attribution. +- 文档:`scripts/lint_frames.py` 与 `scripts/peek_frame.mjs` 补进命令序列与文件表 + (两者此前都没有被文档记录)。 +- 文档:lint 现在是工作流里的显式阶段,位置在生成字幕之后、渲染之前。 +- 文档:删除对私有客户项目的引用,使技能可以公开发布。 +- 许可:风格目录衍生自 Apache-2.0 内容,现已带上完整的来源署名。 -### Notes +### 备注 -- Backfilled from field work: preview-render frame deletion (`--keep-frames`), the - temporary-project technique for re-rendering a single scene, and the cold-open rule - that a video's first 0.5s must already show its subject (lessons #32–#34). +- 从实战回填:预览渲染会清空帧目录(`--keep-frames`)、重渲单个场景的临时项目法、 + 以及「开场 0.5 秒内就必须出现主角」的冷开场规则(lessons #32–#34)。 --- ## [1.2.1] — 2026-09-20 -Four rendering / QC correctness fixes, all found by instrumenting the renderer rather -than by inspection. See `references/lessons.md` #27–#30. +四个渲染 / QC 正确性修复,全部是靠给渲染器加探针找出来的,而不是靠肉眼看。 +详见 `references/lessons.md` #27–#30。 -### Fixed +### 修复 -- **`tl.pause(t)` silently suppressed seek callbacks.** GSAP's signature is - `pause(atTime, suppressEvents)` and the second parameter defaults to `true`, so any - `onUpdate` / `onStart` / `onComplete` fired by a seek was dropped. A counter animated - 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 - 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. -- **`deviceScaleFactor` passed to `chromium.launch()` was silently ignored.** Both - `viewport` and `deviceScaleFactor` are context-level options and only take effect on - `newPage()`. Covers were being written at 1× while the log claimed 2×. The build now - verifies output dimensions with PIL instead of trusting the log. -- **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 - than a spread. A 39-block video sampled 12 blocks covering only 0.25–23.86s, leaving six - 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. +- **`tl.pause(t)` 会静默吞掉本次 seek 的回调。** GSAP 的签名是 + `pause(atTime, suppressEvents)`,第二参默认为 `true`,于是 seek 触发的任何 + `onUpdate` / `onStart` / `onComplete` 都被丢弃。用 `onUpdate` 写的计数器因此在成片里恒为 + 初值 —— 不报错、不告警、缩略图尺寸正确,静态检查也查不出来。渲染器与封面器现在调用 + `tl.pause(t, false)`。文档同时把数字滚动引导向**纯 transform 的「数字卷轴」**, + 完全不依赖任何回调。 +- **传给 `chromium.launch()` 的 `deviceScaleFactor` 被静默忽略。** `viewport` 与 + `deviceScaleFactor` 都是 context 级选项,只在 `newPage()` 上生效。封面实际写成了 1 倍图, + 而日志声称 2 倍。现在构建用 PIL 核验输出尺寸,不再信日志。 +- **QC 抽样只覆盖了视频前四分之一。** `qc_check.py` 取字幕块起始时间后写成 `pts[:samples]`, + 拿的是**前 N 块**而不是均匀铺开。一条 39 块的视频抽了 12 块,只覆盖 0.25–23.86s,后 6 个 + 场景一帧没抽,却报告「全部通过」。现在按步长在全片范围均匀抽样。 +- **封面排版是在动画中途量的。** 校验紧接页面加载后就跑,读的是 `fromTo` 起始值而不是稳定 + 终态,于是报出幽灵偏移。现在测量前先置 `__MG_RENDER__` 并 seek 到 `tl.duration()`, + 与封面渲染器完全一致。 --- ## [1.2.0] — 2026-09-20 -### Added +### 新增 -- **Dual-cover system.** Every finished video now produces two independently laid-out - covers instead of one: `cover_169.png` (1920×1080, 3840×2160 output) for the feed and - player, and `cover_34.png` (1440×1080, 2880×2160 output) for profile grids. - - `scripts/cover_build.mjs` — cover renderer reusing the scene renderer's deterministic - seek core, with three deliberate differences: no subtitle layer, a single seek to one - "cover moment" (`--at last` by default) rather than per-frame stepping, and 2× output - by default. - - `assets/cover-template.html` — cover template with the three required elements - documented in its header. - - `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. +- **封面双方案。** 每条成片现在产出两张独立排版的封面而不是一张:`cover_169.png` + (1920×1080,输出 3840×2160)用于信息流与播放器,`cover_34.png`(1440×1080, + 输出 2880×2160)用于主页栅格。 + - `scripts/cover_build.mjs` —— 封面渲染器,复用场景渲染器的确定性 seek 内核,但有三处 + 刻意不同:不带字幕层、只 seek 到单个「封面时刻」(默认 `--at last`)而不是逐帧步进、 + 以及默认 2 倍输出。 + - `assets/cover-template.html` —— 封面模板,三个必需元素写在文件头注释里。 + - `references/cover-guide.md` —— 重排对照表、尺寸规则、上传策略与自检清单。 +- `scripts/new_project.py` 现在会把两份封面 HTML 一起生成到新项目里。 -### Notes +### 备注 -- The two covers are **sibling layouts sharing one visual DNA, never a crop pair**. - Cropping 3:4 out of 16:9 discards 57.8% of the frame width (1920 → 810px), which - 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. +- 两张封面是**共享同一套视觉基因的姊妹版式,绝不是一对裁切**。从 16:9 裁 3:4 会丢掉 + 57.8% 的画面宽度(1920 → 810px),必然把大字钩子切掉。重排改的是构型本身:悖论视觉 + 从「右侧左右并置」变成「上方竖排堆叠」,钩子从两行变成三行。 --- -## [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` → - `render_video.mjs` → `qc_check.py`. -- **Deterministic seek renderer** (`render_video.mjs`). Instead of screen-recording a - live page, every frame is produced by seeking the GSAP timeline (`tl.pause(t, false)`) - and synchronising CSS animations (`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('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 - video without touching a single frame file. -- `scripts/subs.py` — three subtitle outputs (runtime JSON, SRT/VTT sidecars, and the - in-frame layer injected by the renderer) plus generated `beats.js` per scene. -- `scripts/make_theme.py` — four presets plus topic-word colour derivation, writing a - single `theme.css` source of truth for all colours. -- `scripts/qc_check.py` — stream, duration, loudness, and sampled-frame auditing with a - contact sheet. -- `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. +- 核心流水线:`new_project.py` → `tts_build.py` → `timeline_build.py` → `subs.py` → + `render_video.mjs` → `qc_check.py`。 +- **确定性 seek 渲染器**(`render_video.mjs`)。不录活页面的屏,而是每一帧都靠 seek + GSAP 时间轴(`tl.pause(t, false)`)并同步 CSS 动画 + (`document.getAnimations().currentTime = t*1000`)后再截图。这消掉了实时录制的一整类 + 失败:没有起播竞态、没有字体加载超时、没有不稳定的引导期。 +- **`B('块文本')` 节拍同步。** 画面动画按**匹配字幕块文本**定位,而不是写死帧号,于是改解说词 + 会重排全片时序,而一个画面文件都不用动。 +- `scripts/subs.py` —— 字幕三出口(运行时 JSON、SRT/VTT 外挂、渲染器注入的画面内层), + 外加每场景生成一份 `beats.js`。 +- `scripts/make_theme.py` —— 四个预设 + 按主题词推色,写出唯一颜色来源 `theme.css`。 +- `scripts/qc_check.py` —— 流 / 时长 / 响度 / 抽帧体检,附抽帧速览图。 +- `scripts/import_styles.py` —— 移植期一次性工具,把模板设计规范转写成 + `references/style-catalog.md` / `.json`。 +- 23 种画面风格目录,8 个类别,按改编成本分类。 +- `setup_env.sh` 做首次环境自检,`--install` 装缺失依赖;`package_skill.py` 打可移植 zip。 [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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4898970..9955d07 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 | -| Rendering / sync bug you diagnosed | Add a numbered entry to `references/lessons.md` **and** fix the code | -| New visual style or a better adaptation recipe | `references/style-catalog.md` notes + a documented example | -| Documentation fix | Direct PR — typo fixes need no prior discussion | -| New feature | Open an issue first so the approach can be agreed before you write it | -| Platform-specific fix (macOS / Linux paths, shell variants) | Very welcome — the project is developed on Windows + Git Bash | +| 报告 Bug | 用 bug 模板[开一个 issue](https://github.com/OneMoh/html-explainer/issues/new/choose) | +| 你已定位的渲染 / 同步问题 | 在 `references/lessons.md` 追加一条编号记录,**并且**修代码 | +| 新的画面风格,或更好的改编配方 | 更新 `references/style-catalog.md`,并附一个可运行的示例 | +| 文档修正 | 直接提 PR —— 改错别字不需要先讨论 | +| 新功能 | 先开 issue 把方案谈拢,再动手写 | +| 平台相关修复(macOS / Linux 路径、其它 shell) | 非常欢迎 —— 本项目在 Windows + Git Bash 上开发 | -## Development setup +## 开发环境 ```bash git clone https://github.com/OneMoh/html-explainer.git cd html-explainer -# Check the environment, then install anything missing -bash setup_env.sh # report only -bash setup_env.sh --install # fetch missing dependencies +# 先自检,再装缺的东西 +bash setup_env.sh # 只报告 +bash setup_env.sh --install # 安装缺失依赖 -# Confirm the toolchain is healthy +# 确认工具链正常 node -v && python -V ``` -Requirements: Python ≥ 3.9, Node ≥ 18, and either Chrome or Edge (both are almost always -already present). `ffmpeg` is used from `PATH` or falls back to the static binary bundled -with `imageio-ffmpeg`. +要求:Python ≥ 3.9、Node ≥ 18,以及 Chrome 或 Edge(两者几乎总是已经装好)。 +`ffmpeg` 优先从 `PATH` 取,取不到则回退到 `imageio-ffmpeg` 自带的静态二进制。 -### Running an end-to-end check +### 跑一次端到端自检 ```bash python scripts/new_project.py demo --topic "人工智能" 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/timeline_build.py --project . python ../scripts/subs.py --project . @@ -46,95 +44,79 @@ python ../scripts/lint_frames.py --project . node ../scripts/render_video.mjs . --preview 30 ``` -If all of that completes and `out/preview.mp4` plays with synchronised audio, your -environment is good. +如果这些全部跑完、`out/preview.mp4` 播放时音画同步,你的环境就没问题。 -## 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 - many networks and time out silently. Use system font stacks only, and reference GSAP - from the bundled `../assets/gsap.min.js`. -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. **No wall-clock logic.** `setInterval` and `requestAnimationFrame` counters cannot - work under deterministic seeking. Animate with GSAP or CSS `@keyframes`. -4. **Animate with GSAP timelines, registered on `window.__tl`.** The renderer seeks this - timeline once per frame; without it, nothing animates. -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, - not silently reuse a stale number. -6. **Keep the subtitle safe zone clear.** Nothing may render in the bottom 80–170px, and - 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. +1. **任何画面帧里都不许引外部字体或 CDN 资源。** 网络字体在很多网络下不可达,而且超时是 + 静默的。只用系统字体栈,GSAP 一律从随包的 `../assets/gsap.min.js` 引用。 +2. **颜色只能取 `theme.css` 的 CSS 变量。** 画面代码里不许出现十六进制字面量 —— 这正是 + 「换主题只改一个文件」得以成立的原因。 +3. **不许有墙钟逻辑。** `setInterval` 与 `requestAnimationFrame` 计数在确定性 seek 下不可能 + 工作。动画用 GSAP 或 CSS `@keyframes`。 +4. **动画用 GSAP 时间轴,并注册到 `window.__tl`。** 渲染器每帧 seek 这个时间轴;没有它, + 什么都不动。 +5. **动画定位用 `B('块文本')`,绝不用写死的时间。** 兜底写法 `B('x') || 3.2` 是被禁止的 —— + 块查不到就应该在构建时大声失败,而不是静默复用一具过期的数字。 +6. **保持字幕禁区干净。** 底部 80–170px 不许有任何内容渲染,入场路径也不许穿过它。 +7. **项目目录与输出文件一律用 ASCII 路径。** 非 ASCII 路径会让 Windows 上的 ffmpeg 出错。 + 项目名与输出名保持 ASCII,要展示用的名字以后再改。 -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 -this kind of pipeline fails *silently* — a bug produces a plausible-looking video rather -than an error. +`references/lessons.md` 是这个仓库里最值钱的文件。它之所以存在,是因为这条流水线的失败方式 +是**静默**的 —— 出问题不会报错,只会产出一条看着挺像样的视频。 -A good entry contains, in this order: +一条好的记录按这个顺序写: -1. **The symptom**, as you first observed it — including how it misled you. -2. **The diagnosis**, and the evidence that proved it (a probe, a diff, a measurement). -3. **The fix**, and what to check afterwards to confirm it. -4. **The general rule** this implies, if it implies one. +1. **症状**,就按你最初观察到的样子 —— 包括它是怎么把你带偏的。 +2. **定位过程**,以及证明病因的证据(探针、diff、测量值)。 +3. **修法**,以及修完该检查什么来确认。 +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 - structure, colour discipline) and `references/style-catalog.json`. -2. Classify it as `rich` (single file, CSS `@keyframes` timeline — drivable by the seek - renderer with no changes) or `gsap` (multi-composition — must be re-expressed, not - copied). -3. Add an adaptation recipe to `references/template-guide.md` if the style needs anything - beyond the standard three steps (swap the font stack, lift bottom elements out of the - subtitle zone, fill in real content). -4. Verify it renders: `node scripts/peek_frame.mjs --at 25,50,85`. +1. 在 `references/style-catalog.md` 与 `references/style-catalog.json` 里记录规范 + (画布、字阶、时间轴结构、配色纪律)。 +2. 分类为 `rich`(单文件,CSS `@keyframes` 时间轴 —— seek 渲染器零改动可驱动)或 `gsap` + (多 composition —— 必须重写表达,不能照搬)。 +3. 如果这种风格需要超出标准三步(换字体栈、把底部元素抬出字幕带、填真实内容)之外的 + 处理,在 `references/template-guide.md` 里补一条改编配方。 +4. 验证它能渲出来:`node scripts/peek_frame.mjs --at 25,50,85`。 -Style identifiers are `kebab-case` and match the pattern already in use (`frame-*`, -`vfx-*`). +风格标识符用 `kebab-case`,并沿用已有的命名模式(`frame-*`、`vfx-*`)。 -## Pull request checklist +## PR 检查清单 -Before opening a PR, please confirm: +提 PR 之前请确认: -- [ ] `python scripts/lint_frames.py --project ` reports no FAILs for any frame - you touched -- [ ] You have actually rendered your change. Frame code that "looks right" frequently - is not — seek-based rendering has its own failure modes -- [ ] Any new lesson is added to `references/lessons.md` with a number -- [ ] No external fonts, CDN references, or hardcoded colours were introduced -- [ ] 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 +- [ ] `python scripts/lint_frames.py --project ` 对你动过的每一帧都没有 FAIL +- [ ] 你真的渲染过你的改动。画面代码「看着对」经常是不对的 —— seek 渲染有自己的失败模式 +- [ ] 新增的踩坑记录已带编号写进 `references/lessons.md` +- [ ] 没有引入外部字体、CDN 引用或硬编码颜色 +- [ ] 如果有再分发或再衍生的第三方内容,`THIRD_PARTY_NOTICES.md` 已更新 +- [ ] 涉及使用者可见行为的改动,`README.md` 与 `README.en.md` 已同步更新 -## Commit messages +## 提交信息 -Conventional Commits are preferred (`fix:`, `feat:`, `docs:`, `chore:`). A body is -encouraged when the change fixes a silent failure — explain what was misbehaving and how -you verified the fix. +建议使用 Conventional Commits(`fix:`、`feat:`、`docs:`、`chore:`)。如果这次改动修的是一个 +静默失败,请写正文说明它原来错在哪、你是怎么验证修好了的。 -## Scope note +## 范围说明 -This project intentionally does **not** aim to be a framework. It is a pipeline with a -small, boring surface: HTML frames in, MP4 out, with audio-video sync that holds. Features -that add a build system, a plugin API, or a runtime dependency on a cloud service are -generally out of scope — say hello in an issue if you think there's an important exception. +本项目刻意**不**打算成为一个框架。它是一条流水线,接口很小、很无聊:HTML 帧进,MP4 出, +音画同步稳得住。会增加构建系统、插件 API,或对云服务产生运行时依赖的功能,一般都在范围之外 +—— 如果你认为有重要的例外,开个 issue 聊聊。 -## License +## 许可 -By contributing, you agree that your contributions are licensed under the MIT License -(see [LICENSE](LICENSE)), and you confirm you have the right to submit them. If you are -porting content from another project, add it to `THIRD_PARTY_NOTICES.md` in the same PR. +提交贡献即表示你同意你的贡献以 MIT 许可授权(见 [LICENSE](LICENSE)),并确认你有权提交它们。 +如果你从别的项目移植了内容,请在同一个 PR 里把它写进 `THIRD_PARTY_NOTICES.md`。 diff --git a/README.en.md b/README.en.md index 0c784d0..851ff95 100644 --- a/README.en.md +++ b/README.en.md @@ -2,17 +2,19 @@ # 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. -No Remotion. No cloud render queue. No per-render fees. No API keys for the core path. +Write scenes in HTML → deterministic frame-by-frame rendering → a real MP4. Everything runs +locally; the core pipeline needs no API key and charges no per-render fee. [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Version](https://img.shields.io/badge/version-1.2.2-blue.svg)](CHANGELOG.md) -[![Python](https://img.shields.io/badge/Python-3.9%2B-3776AB.svg?logo=python&logoColor=white)](https://python.org) -[![Node](https://img.shields.io/badge/Node-18%2B-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org) -[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-6e6e6e.svg)](#requirements) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) +[![Agent Skill](https://img.shields.io/badge/Agent%20Skill-SKILL.md-8A2BE2.svg)](SKILL.md) +[![Claude Code](https://img.shields.io/badge/Claude%20Code-%E2%9C%93-D97757.svg)](#install) +[![Codex](https://img.shields.io/badge/Codex-%E2%9C%93-000000.svg)](#install) +[![WorkBuddy](https://img.shields.io/badge/WorkBuddy-%E2%9C%93-1f6feb.svg)](#install) +[![Python](https://img.shields.io/badge/Python-3.9%2B-3776AB.svg?logo=python&logoColor=white)](#requirements) +[![Node](https://img.shields.io/badge/Node-18%2B-339933.svg?logo=node.js&logoColor=white)](#requirements) [简体中文](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 -`html-explainer` produces explainer and knowledge videos — the kind with a voiceover, hard -subtitles, a progress bar, and motion-graphics scenes built in HTML/CSS/GSAP. +`html-explainer` turns a topic into a **narrated explainer video with burned-in subtitles and a +progress bar**, with the visuals written in HTML/CSS/GSAP. -You write a narration script. An agent (or you) picks a visual style from a library of 23, -writes one self-contained HTML file per beat, and this pipeline renders it to a real MP4 -with frame-accurate subtitle timing and two platform-ready covers. +**It is an Agent Skill** (`SKILL.md` + `scripts/` + `references/`), following the +[Agent Skills](https://code.claude.com/docs/en/skills) convention, so Claude Code, OpenAI Codex, +WorkBuddy, Cursor, Gemini CLI and others can load it directly. Underneath it is plain Python and +Node scripts, so you can also run it by hand. -Everything runs on your machine. The only network calls are the TTS request and, optionally, -web research for the script. +You say one sentence: -It is packaged as an **Agent Skill** (`SKILL.md` + `scripts/` + `references/`), so it drops -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. +> Make a 3-minute explainer video about why the sky is blue. + +The skill then walks the agent through: research → script → voiceover → subtitles and beats → +scene authoring in one of 23 styles → render → QC → covers. --- -## 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 -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 -so half the video uses the wrong typeface, the first few frames capture a pre-animation +### One-line install (recommended) + +Send this to your agent: + +```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\`. + +### Skills directory by agent + +| Agent | Personal (global) | Project (in-repo) | +|---|---|---| +| **WorkBuddy** | `~/.workbuddy/skills/` | `/.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= # expand to an absolute path when briefing a sub-agent +SKILL= + +# 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/.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; .beats.js is generated, never hand-edited +├── audio/ render/ research/ script/ +└── out/ # MP4, covers, SRT/VTT, QC report +``` + +The order matters: `beats.js` is derived from the TTS output. That is why, after you edit the +narration, everything downstream re-times automatically while no scene code changes. + +> **If you plan to reuse the rendered PNGs, always pass `--keep-frames`.** Preview mode clears the +> frame directory after muxing by design. + +--- + +## Why not screen recording + +Most "HTML to video" tools launch a browser, play the animation and record the screen. That breeds +a whole class of **intermittent** bugs — the animation played before recording started, a web font +loaded late so half the video used the wrong typeface, the first frames captured the pre-animation state. -`html-explainer` does not record. It **seeks**. +`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: - -1. sets `window.__MG_RENDER__` so the frame knows not to autoplay, -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. +The trade-off: **every animation must be seekable.** Wall-clock animation (`setInterval`, +`requestAnimationFrame` counters, CSS `transition` entrances) cannot work and is rejected by +`lint_frames.py`. --- @@ -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.** | -| **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. | -| **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. | -| **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. | -| **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. | -| **Pre-render linting** | `lint_frames.py` audits all eight frame-contract rules before you spend 15 minutes rendering. | -| **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. | -| **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. +| **Beats anchored to narration** | Scenes position animation by **matching subtitle text** (`B('block text')`), never by hardcoded frame numbers. Edit the script and the whole video re-times with zero scene-code changes. | +| **Word-boundary subtitles** | Timing comes from edge-tts `WordBoundary` events, never interpolated by character count — in Chinese two phrases of equal length can differ 3× in duration. | +| **Two-level clock** | Frame length uses MP3 **container duration** (keeps A/V in sync); the final subtitle block ends on **actual speech end**. | +| **23 visual styles** | 8 categories, each with its canvas, type scale, timeline and colour discipline recorded. No designing from a blank page. | +| **Dual covers** | Every video ships a 16:9 cover plus an **independently re-laid-out** 3:4 cover — not a crop. Cropping loses 57.8% of the width. | +| **Pre-render audit + QC** | `lint_frames.py` reports contract violations before you spend render time; `qc_check.py` checks loudness, drift and samples frames. | +| **Structurally offline** | GSAP vendored; browser auto-detected; ffmpeg falls back to the `imageio-ffmpeg` static binary; web fonts are banned by contract. | --- @@ -107,339 +182,110 @@ determinism, and it is enforced rather than merely advised. ```mermaid flowchart LR - A["Topic"] --> B["Research
facts with sources"] - B --> C["narration.json
text, | splits subtitle blocks"] - C --> D["tts_build.py
edge-tts → MP3 + word boundaries"] + A["Topic"] --> B["Research
every figure sourced"] + B --> C["narration.json
pipes split subtitle blocks"] + C --> D["tts_build.py
edge-tts to MP3 + word boundaries"] D --> E["timeline_build.py
global axis + stitched track"] - E --> F["subs.py
subs.json · srt/vtt · beats.js"] - F --> G["frames/*.html
one scene per beat, style from library"] - G --> H{"lint_frames.py
8 contract rules"} + E --> F["subs.py
subs.json, srt/vtt, beats.js"] + F --> G["frames/*.html
one scene per line, styled from the library"] + G --> H{"lint_frames.py
eight contract rules"} H -->|fail| G - H -->|pass| I["render_video.mjs
seek → PNG per frame"] + H -->|pass| I["render_video.mjs
seek per frame to PNG"] I --> J["ffmpeg
H.264 + AAC mux"] J --> K[("out/slug.mp4")] - I --> L["qc_check.py
loudness · drift · contact sheet"] - K --> M["cover_build.mjs
cover_169.png · cover_34.png"] + I --> L["qc_check.py
loudness, drift, contact sheet"] + K --> M["cover_build.mjs
cover_169.png, cover_34.png"] style K fill:#1f6feb,color:#fff style H fill:#8957e5,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= # 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/.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 -### Pipeline - -| Command | Does | +| Command | Purpose | |---|---| -| `python scripts/new_project.py [--topic T] [--preset P] [--fps 30] [--width 1920] [--height 1080] [--lang zh]` | Scaffolds a project: config, theme, templates, and both cover HTML files | -| `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 | -| `python scripts/timeline_build.py --project . [--gap SEC]` | Builds the global timeline and stitches `narration-full.mp3` | -| `python scripts/subs.py --project . [--fps N]` | Emits `subs.json`, `srt`/`vtt` sidecars, and 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 | -| `node scripts/render_video.mjs [--out PATH] [--preview SEC] [--fps N] [--concurrency N] [--jpeg] [--scale N] [--keep-frames] [--browser PATH]` | Renders and muxes to MP4 | -| `python scripts/qc_check.py --project . [--samples N]` | Loudness / duration / stream audit plus a contact sheet | -| `node scripts/cover_build.mjs [--at last\|SEC] [--only 169\|34] [--scale N] [--jpg] [--keep-frames]` | Renders both covers at 2× by default | +| `new_project.py --topic "..."` | Scaffold a project | +| `tts_build.py --project .` | Synthesise voiceover (caching, timeouts, backoff retries, silence trim) | +| `timeline_build.py --project .` | Global timeline + stitched `narration-full.mp3` | +| `subs.py --project .` | Three subtitle outputs + one `beats.js` per scene | +| `lint_frames.py --project .` | Audit frames against the eight contract rules | +| `render_video.mjs ` | Render and mux the MP4 | +| `qc_check.py --project .` | Loudness / duration / stream checks + contact sheet | +| `cover_build.mjs ` | 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 | -|---|---| -| `node scripts/peek_frame.mjs [--at 40,80]` | Screenshots one scene at percentage points — seconds, not minutes. `--all` for every scene | -| `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) | -| `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) | +A few handy extras: `peek_frame.mjs ` (screenshot one scene in seconds — the one you +reach for while designing), `make_theme.py --topic "medical" --use` (derive a palette from a topic +word), `package_skill.py` (build a portable zip). + +**Full flag reference is in [`SKILL.md`](SKILL.md), or pass `--help` to any script.** --- ## The `B()` beat system -This is the part that changes how the work feels. - -A generated `beats.js` per scene exposes every subtitle block by its text: +`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: ```js -B('右边卡片') // the second the phrase "右边卡片" starts being spoken -Be('证据') // the second that phrase finishes +tl.fromTo('.card', { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('right-hand card')); +tl.fromTo('.verdict', { scale: 0.8 }, { scale: 1, duration: 0.6 }, B('the verdict') + 0.2); ``` -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 -var tl = gsap.timeline({ paused: true }); -window.__tl = tl; +This is this project's own design, and it is the part that changes how the work feels: edit the +narration once, re-run three commands, and every scene re-times itself. Compare with hardcoded +frame numbers — one edited sentence means re-aligning the entire video by hand. -tl.fromTo('.card', { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('右边卡片')); -tl.fromTo('.verdict',{ scale: 0.8 }, { scale: 1, duration: 0.6, ease: 'back.out(2)' }, B('结论句') + 0.2); -``` - -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. +**Fallbacks are forbidden.** `B('x') || 3.2` hides "beat not found" behind a stale number. A miss +should fail the build — a mis-timed video is far worse than a failed build. --- ## Visual style library -**Do not design scenes from a blank page.** 23 styles across 8 categories, each documented -with its canvas, type scale, timeline structure, and colour discipline -([`references/style-catalog.md`](references/style-catalog.md)). +**Do not design scenes from a blank page.** 23 styles across 8 categories, each recording its +canvas, type scale, timeline structure and colour discipline — full catalogue in +[`references/style-catalog.md`](references/style-catalog.md) (Chinese). -| Category | Styles | -|---|---| -| Presentation / title cards | Bold Poster, Bold Signal, Build Minimal, Creative Voltage, Electric Studio, Glitch Title, Kinetic Type, Swiss Grid, Warm Grain | -| 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 | +It covers bold signal cards, luxurious minimal whitespace, NYT-grade data charts, Swiss grid, +glitch art, film light leaks, fluid hero backgrounds, brand logo outros, VFX text cursors, 9:16 +social formats, product promos and more. -Two classes, 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 - these **with zero code changes**; adapting one is three steps: swap the web font for a - system stack, lift bottom elements out of the subtitle zone, fill in real content. -- **`gsap` (11)** — multi-composition, CDN-loaded. **Do not port the code.** A multi-scene - composition has no playback hook inside a single seekable frame, so importing it yields a - static first frame and no error whatsoever. Take the visual DNA and re-express it as CSS - keyframes. +- **`rich` (12)** — single file, pure CSS `@keyframes`. The seek renderer drives them **with no + changes**; adaptation is three steps: swap web fonts for a system stack, lift bottom elements out + of the subtitle band, drop in real content. +- **`gsap` (11)** — multiple compositions loaded from a CDN. **Do not port the code.** Take only + the visual DNA and re-express it in CSS keyframes. -Rotate two to four styles across a video. Eight scenes sharing one template looks stale; -mixing an aggressive opener, a spare mid-section, and a data-driven conclusion does not. +Rotate 2–4 styles per project. Eight scenes sharing one look reads as monotonous. --- -## Dual-cover system +## Dual covers -A finished video produces **two covers that are siblings, not a crop pair**. - -Cropping 3:4 out of 16:9 yields 810px from 1920px — discarding **57.8% of the frame width**. -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: +Every video ships **two covers that are siblings, not crops**. Cropping 16:9 to 3:4 leaves 810px of +1920px — **57.8% of the width is gone**, and any full-width headline gets sliced in half. So both +share one visual DNA, but each is **laid out again from scratch**: | Element | 16:9 (`1920×1080`) | 3:4 (`1440×1080`) | |---|---|---| -| Paradox visual | Right side, side-by-side | Top, vertically stacked | -| Hook | Bottom-left, 2 lines, ≥96px | Bottom, 3 lines, ≥120px | -| Corner label | Top-left | Top-centre or dropped | +| Paradox visual | Right side, side by side | Top, stacked vertically | +| Hook | Bottom-left, two lines, ≥96px | Bottom, three lines, ≥120px | | Max characters per line | 7 | 5 | -Three elements are mandatory, and a missing one means rework: a large hook with genuine -tension, a central visual that *argues a contradiction* (two figures moving opposite ways, -a split line, light against dark — not a decorative shape), and margin to spare -(≥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 -│ ├── .html # One scene per file -│ ├── .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. +`new_project.py` generates the templates for both sizes directly; output is 2× by default. Full +rules and the pre-upload checklist: [`references/cover-guide.md`](references/cover-guide.md) +(Chinese). --- @@ -447,99 +293,90 @@ your target duration, rewrite sentences — do not distort the speech rate. | 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 | -| 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 is 1280×720 instead of 1920×1080 | `viewport` passed to `browser.launch()` — it is a *context* option | Pass it to `newPage()`. Lessons #18 | -| 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 | -| `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 | -| Node can't find `/tmp/...` | Git Bash's `/tmp` ≠ Node's `/tmp` on Windows | Pass `C:/...` forward-slash absolute paths. Lessons #12 | -| 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 | +| Every frame is a frozen first frame, no error | Timeline not registered on `window.__tl`; or `page.evaluate` received a **string** instead of a function literal (Playwright evaluates it once, so the body never runs) | Pass a real function literal | +| Video comes out 1280×720 | `viewport` was passed to `browser.launch()` — it is a *context*-level option | Pass it to `newPage()` | +| Cover renders at 1× | Same trap, `deviceScaleFactor` twin | Pass to `newPage()` + `screenshot({ scale: 'device' })` | +| Numbers render but never move | The seek suppressed `onUpdate` callbacks | Renderer fixed (`pause(t, false)`); use a transform-based number reel in scenes | +| `No such file or directory` on Windows | Non-ASCII path — Windows ffmpeg reads UTF-8 as ANSI | Keep paths ASCII | +| Preview finished, frame PNGs are gone | Preview mode clears the frame directory by design | Pass `--keep-frames` | -**`references/lessons.md` is the most valuable file here.** 34 numbered entries, each -recording a bug that produced a plausible-looking video while being wrong — including how -it was misdiagnosed first. Read it before debugging from scratch. +**`references/lessons.md` is the most valuable file in this repository.** 34 numbered entries, each +one a bug where "the video looked fine but was wrong" — including how it was misdiagnosed at first. +Read it before debugging from scratch. --- ## Who this is for -**Good fit** +**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. -- You already work with an AI coding agent and would rather express scenes as HTML than - learn a motion-graphics suite. -- You need reproducible output — re-render the same project and get the same frames. -- 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:** live-action editing, talking-head footage, re-creating an existing video; wanting a +drag-and-drop editor (scenes are code, deliberately); wanting React component animation as the +authoring model — that is the domain of the sibling project +[`anything2explainer`](https://github.com/Vincentwei1021/anything2explainer). -**Poor fit** +--- -- Real-footage editing, talking-head video, or recreating an existing video. -- React component animation as the authoring model (that is the job of the Remotion-based - `anything2explainer`, this project's sibling). -- A graphical drag-and-drop editor. Scenes are code, deliberately. +## Prior work and sibling projects + +`html-explainer` did not invent its methodology. It has two clear sources of ideas. Both are +**independent projects by different authors — not the same author as this one**, and this +repository neither bundles nor depends on any of their code at runtime. What was carried over is +design specification and engineering experience. + +- [**Vincentwei1021/anything2explainer**](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 - [ ] Per-scene render switch (`--only `) — 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 -- [ ] Additional cover aspect ratios via the spec table in `cover_build.mjs` -- [ ] English-first style notes in the catalog (currently Chinese descriptions throughout) -- [ ] Optional background-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) +- [ ] Cover layout measurement tool (line count / margins / overlap / hook font size in one pass) +- [ ] More cover aspect ratios +- [ ] English descriptions in the style catalogue +- [ ] Optional music bed with automatic ducking under narration --- ## Contributing -Issues and PRs are welcome. The most valuable contribution is a **diagnosed silent -failure** added to `references/lessons.md` — the symptom as it first misled you, the -evidence that proved the cause, the fix, and the resulting rule. - -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. +Issues and PRs are welcome. The most valuable contribution is a **diagnosed silent failure** added +to `references/lessons.md` — write down how it first misled you, the evidence that proved the cause, +the fix, and the general rule that follows. Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) first +(Chinese). --- -## 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 +## Licence and acknowledgements [MIT](LICENSE) © 2026 Moh -The MIT licence covers the original code and documentation. Third-party components and the -derived style specification remain under their own terms — see -[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md). +MIT covers the original code and documentation. Bundled third-party components and the derived +style specifications remain under their own terms; GSAP is bundled under GreenSock's +[standard "no charge" licence](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** -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. diff --git a/README.md b/README.md index cab35fd..546b201 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,18 @@ # html-explainer -**把一个主题做成有配音、有硬字幕、有封面的讲解视频 —— 全本地、用 HTML 写画面,音画同步稳得住。** +**一个 Agent Skill:给任意主题,产出一条带配音、硬字幕、封面的讲解视频。** -面向 AI 编程智能体的确定性 HTML → MP4 渲染流水线。 -不需要 Remotion,不需要云端渲染队列,没有按次计费,核心链路不需要任何 API key。 +HTML 写画面 → 确定性逐帧渲染 → 真 MP4。全本地跑,核心链路零 API key、零按次计费。 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Version](https://img.shields.io/badge/version-1.2.2-blue.svg)](CHANGELOG.md) -[![Python](https://img.shields.io/badge/Python-3.9%2B-3776AB.svg?logo=python&logoColor=white)](https://python.org) -[![Node](https://img.shields.io/badge/Node-18%2B-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org) -[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-6e6e6e.svg)](#环境要求) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) +[![Agent Skill](https://img.shields.io/badge/Agent%20Skill-SKILL.md-8A2BE2.svg)](SKILL.md) +[![Claude Code](https://img.shields.io/badge/Claude%20Code-%E2%9C%93-D97757.svg)](#安装) +[![Codex](https://img.shields.io/badge/Codex-%E2%9C%93-000000.svg)](#安装) +[![WorkBuddy](https://img.shields.io/badge/WorkBuddy-%E2%9C%93-1f6feb.svg)](#安装) +[![Python](https://img.shields.io/badge/Python-3.9%2B-3776AB.svg?logo=python&logoColor=white)](#环境要求) +[![Node](https://img.shields.io/badge/Node-18%2B-339933.svg?logo=node.js&logoColor=white)](#环境要求) [简体中文](README.md) · [English](README.en.md) @@ -20,43 +21,138 @@ --- -## 这是什么 +## 它是什么 -`html-explainer` 用来生产科普 / 讲解 / 知识类视频 —— 就是那种带配音、带硬字幕、带进度条, -画面用 HTML/CSS/GSAP 做的动效视频。 +`html-explainer` 把一个主题做成**带配音、带硬字幕、带进度条**的讲解视频,画面用 +HTML/CSS/GSAP 写。 -你写一份解说词。智能体(或者你自己)从 23 种画面风格里挑一个,为每个节拍写一个自包含的 -HTML 文件,然后这条流水线把它渲染成真正的 MP4:字幕逐字对齐,外加两张可直接上传的封面。 +它**是一个 Agent Skill**(`SKILL.md` + `scripts/` + `references/`),遵循 +[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/`),可以直接放进 -Claude Code、WorkBuddy、Cursor,或任何能读技能文件的智能体 —— 但脚本本身只是普通的 -Python 和 Node,手工跑完全没问题。 +> 把「为什么天空是蓝色的」做成一条 3 分钟的讲解视频。 + +技能会带着智能体走完:调研 → 解说词 → 配音 → 字幕与节拍 → 挑风格写画面 → 渲染 → 体检 → 封面。 --- -## 核心思路:确定性 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__`,让画面知道自己不该自动起播; -2. 用 `tl.pause(t, false)` 把 GSAP 时间轴定位到那一瞬间; -3. 用 `document.getAnimations().currentTime = t * 1000` 同步所有 CSS 动画; -4. 等两个动画帧后截图。 +它会自己 clone 到对应目录、跑环境自检、把缺的东西装上。装完**新开一个会话**,让智能体重新 +扫描技能目录。 -渲染出来的第 1204 帧,和下一次渲染的第 1204 帧,逐像素一致。正是这个性质让后续的一切成为 -可能 —— QC 可以定点抽查某一帧、单个场景可以独立重渲,而一整类时序 bug 从根本上无法发生。 +### 手动安装 -> 第二个参数不是可选项。`pause(atTime, suppressEvents)` 的 `suppressEvents` 默认为 `true`, -> 它会**静默丢弃**这次 seek 触发的所有回调 —— 用 `onUpdate` 写的数字滚动会永远停在初值, -> 且零报错。这个坑花掉了真实的调试时间,见 [`references/lessons.md`](references/lessons.md) #27。 +仓库根目录**就是**技能目录(`SKILL.md` 在根),直接 clone 进技能目录即可,不用再拷子目录: + +```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/.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;.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('块文本')`),绝不硬编码帧号。改解说词,全片自动重排时间 —— **画面代码一行都不用改。** | -| **词边界字幕** | 时序来自 edge-tts 的 `WordBoundary` 事件,不按字数插值。中文里两个同字数的短语时长可以差 3 倍,插值会明显错位。 | -| **两级时钟** | 帧长用 MP3 **容器时长**(保证音画同步);末块字幕按**语音真实结束**收尾。两者混用会让每场景字幕多停约 0.8 秒。 | -| **23 种画面风格** | 8 个类别 —— 大胆信号卡、奢华极简、NYT 编辑级数据图表、瑞士网格、故障艺术、胶片漏光、流体 Hero、品牌收尾等,每种都记录了画布、字阶、时间轴与配色纪律。 | -| **封面双方案** | 每条成片都附带 16:9 信息流封面与**独立排版**的 3:4 主页封面 —— 不是裁切,裁切会丢掉 57.8% 的画面宽度。 | -| **渲染前静态体检** | `lint_frames.py` 在你花 15 分钟渲染之前,先把八条画面契约的违规逐条报出来。 | -| **自适应 QC** | 响度、时长漂移、流完整性、抽帧速览图 —— 抽帧体积阈值取本片自身中位数推导,暗色主题不会产生假阳性。 | -| **结构性离线** | 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 直接拒绝。 -这个约束换来了确定性 —— 而且它是被强制执行的,不只是写在文档里的建议。 +| **节拍锚定解说词** | 画面按**匹配字幕文本**定位动画(`B('块文本')`),绝不硬编码帧号。改解说词,全片自动重排时间,画面代码零改动。 | +| **词边界字幕** | 时序来自 edge-tts 的 `WordBoundary` 事件,不按字数插值 —— 中文里两个同字数的短语时长可以差 3 倍。 | +| **两级时钟** | 帧长用 MP3 **容器时长**(保音画同步);末块字幕按**语音真实结束**收尾。 | +| **23 种画面风格** | 8 个类别,每种都记录了画布、字阶、时间轴与配色纪律。不用对着空白页从零设计。 | +| **封面双方案** | 每条成片附带 16:9 与**独立重排**的 3:4 封面 —— 不是裁切,裁切会丢掉 57.8% 的画面宽度。 | +| **渲染前体检 + QC** | `lint_frames.py` 在渲染前报出契约违规;`qc_check.py` 查响度、时长漂移、抽帧速览。 | +| **结构性离线** | GSAP 内置;浏览器自动探测;ffmpeg 缺失时回退到 `imageio-ffmpeg` 静态二进制;网络字体被契约禁止。 | --- @@ -119,309 +193,84 @@ flowchart LR 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/.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 [--topic T] [--preset P] [--fps 30] [--width 1920] [--height 1080] [--lang zh]` | 建项目脚手架:配置、主题、模板,以及两份封面 HTML | -| `python scripts/tts_build.py --project . [--voice V] [--rate +8%] [--no-trim] [--no-cache]` | 合成配音;带缓存、超时、退避重试、裁静音;写两种时长口径 | -| `python scripts/timeline_build.py --project . [--gap SEC]` | 生成全局时间轴并拼接 `narration-full.mp3` | -| `python scripts/subs.py --project . [--fps N]` | 产出 `subs.json`、`srt`/`vtt` 外挂字幕,以及每场景一份 `beats.js` | -| `python scripts/lint_frames.py --project . [--only id,id] [--include-covers]` | 按八条契约静态体检所有画面帧 | -| `node scripts/render_video.mjs [--out PATH] [--preview SEC] [--fps N] [--concurrency N] [--jpeg] [--scale N] [--keep-frames] [--browser PATH]` | 渲染并合成 MP4 | -| `python scripts/qc_check.py --project . [--samples N]` | 响度 / 时长 / 流体检 + 抽帧速览图 | -| `node scripts/cover_build.mjs [--at last\|SEC] [--only 169\|34] [--scale N] [--jpg] [--keep-frames]` | 渲两张封面,默认 2 倍图 | +| `new_project.py --topic "主题"` | 建项目脚手架 | +| `tts_build.py --project .` | 合成配音(带缓存、超时、退避重试、裁静音) | +| `timeline_build.py --project .` | 全局时间轴 + 拼接 `narration-full.mp3` | +| `subs.py --project .` | 字幕三出口 + 每场景 `beats.js` | +| `lint_frames.py --project .` | 按八条契约静态体检画面帧 | +| `render_video.mjs ` | 渲染并合成 MP4 | +| `qc_check.py --project .` | 响度 / 时长 / 流体检 + 抽帧速览图 | +| `cover_build.mjs ` | 渲染封面双方案,默认 2 倍图 | -### 辅助工具 +常用可选参数:`--preview N` 只渲前 N 秒样片、`--keep-frames` 保留帧 PNG、 +`--only id,id` 只体检指定场景、`--scale N` 输出倍率。 -| 命令 | 作用 | -|---|---| -| `node scripts/peek_frame.mjs [--at 40,80]` | 给单个场景在若干百分比时点截图 —— 是「秒级」而不是「分钟级」。`--all` 可覆盖全部场景 | -| `python scripts/make_theme.py --topic "医疗" --use` | 按主题词推导配色,或用 `--preset violet\|cyan\|amber\|mono` | -| `python scripts/check_integrity.py` | 版本号 / 风格目录 / 计数一致性 + 外链扫描(CI 也会跑) | -| `bash setup_env.sh --install` | 环境自检与依赖安装 | -| `python scripts/package_skill.py [--with-deps]` | 打成可移植 zip(不加 `--with-deps` 约 110KB) | +另有几个顺手的小工具:`peek_frame.mjs `(单场景秒级截图,改画面时最常用)、 +`make_theme.py --topic "医疗" --use`(按主题词推配色)、`package_skill.py`(打成可移植 zip)。 + +**完整参数表见 [`SKILL.md`](SKILL.md),或对任一脚本加 `--help`。** --- ## `B()` 节拍系统 -这是让整个工作方式变掉的那一块。 - -`subs.py` 为每个场景生成一份 `beats.js`,把每块字幕按**原文**暴露出来: +`subs.py` 为每个场景生成一份 `beats.js`,把每块字幕按**原文**暴露出来,于是画面动画可以直接 +锚在吐字时刻上: ```js -B('右边卡片') // 「右边卡片」这几个字开始被念出的那一秒 -Be('证据') // 这句话念完的那一秒 +tl.fromTo('.card', { y: 60, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.7 }, B('右边卡片')); +tl.fromTo('.verdict', { scale: 0.8 }, { scale: 1, duration: 0.6 }, B('结论句') + 0.2); ``` -于是画面动画可以这样写: +`B('块文本')` 是「这几个字开始被念出」的时刻,`Be('块文本')` 是念完的时刻。 -```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('右边卡片')); -tl.fromTo('.verdict',{ scale: 0.8 }, { scale: 1, duration: 0.6, ease: 'back.out(2)' }, B('结论句') + 0.2); -``` - -改一次解说词,重跑三条命令。节拍随之平移,所有场景自行重排对时,没有一个画面文件被改动。 -对比帧号硬编码的写法:改一句话意味着整片手工重新对位。 - -**兜底写法是被禁止的。** `B('x') || 3.2` 会把「节拍查不到」这件事藏在一个过期的数字后面。 -查不到就应该让构建直接失败 —— 一条对错了时间的视频,比一次构建失败糟糕得多。 +**兜底写法是被禁止的。** `B('x') || 3.2` 会把「节拍查不到」藏在一个过期的数字后面。查不到 +就应该让构建直接失败 —— 一条对错了时间的视频,比一次构建失败糟糕得多。 --- ## 画面风格库 **不要对着空白页从零设计画面。** 23 种风格、8 个类别,每种都记录了画布、字阶、时间轴结构与 -配色纪律([`references/style-catalog.md`](references/style-catalog.md))。 +配色纪律 —— 完整目录见 [`references/style-catalog.md`](references/style-catalog.md)。 -| 类别 | 风格 | -|---|---| -| 演示 / 标题卡 | 大胆海报帧、大胆信号卡帧、奢华极简留白帧、创意电压分屏帧、电光工作室分屏帧、故障艺术标题帧、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 文字光标 | +涵盖:大胆信号卡 / 奢华极简留白 / NYT 编辑级数据图表 / 瑞士网格 / 故障艺术 / 胶片漏光 / +流体 Hero / 品牌 Logo 收尾 / VFX 文字光标 / 社媒竖版(9:16)/ 产品演示等。 分成两类,改编成本差别很大: -- **`rich`(12 个)** —— 单文件 + 纯 CSS `@keyframes` 驱动。seek 渲染器**零改动即可驱动**; - 改编只要三步:把网络字体换成系统字体栈、把底部元素抬出字幕带、填进真实内容。 -- **`gsap`(11 个)** —— 多 composition + CDN 加载。**不要搬代码。** 多场景组合在单个可 - seek 的帧里没有起播钩子,整段搬进来只会得到静止首帧,而且零报错。只取它的视觉 DNA, - 用 CSS keyframes 重新表达。 +- **`rich`(12 个)** —— 单文件 + 纯 CSS `@keyframes`。seek 渲染器**零改动即可驱动**;改编 + 只要三步:换系统字体栈、把底部元素抬出字幕带、填进真实内容。 +- **`gsap`(11 个)** —— 多 composition + CDN 加载。**不要搬代码。** 只取它的视觉 DNA,用 + 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`) | |---|---|---| | 悖论视觉 | 右侧,左右并置 | 上方,竖排堆叠 | | 钩子 | 左下,两行,≥96px | 下方,三行,≥120px | -| 角标 | 左上 | 顶部居中或省略 | | 每行字数上限 | 7 字 | 5 字 | -三个要素缺一即返工:一句**真正有张力**的大字钩子、一个**在讲矛盾**的主视觉(两个数字反向 -运动、一道分裂线、一明一暗 —— 不是装饰图形)、以及四边都留有余量(≥96px / ≥110px)。 - -默认输出 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 -│ ├── .html # 一个场景一个文件 -│ ├── .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%,改句子 —— 不要扭曲语速去硬凑。 +两个尺寸的输出都由 `new_project.py` 直接生成模板,默认出 2 倍图。完整规则与上传前自检清单: +[`references/cover-guide.md`](references/cover-guide.md)。 --- @@ -429,18 +278,12 @@ my-video/ | 症状 | 原因 | 处理 | |---|---|---| -| 每一帧都是静止首帧,且不报错 | 时间轴没注册到 `window.__tl`;或 `page.evaluate` 传了**字符串**而不是函数字面量(Playwright 只把字符串求值一次,返回的是函数对象,函数体根本不执行) | 注册时间轴;传真正的函数字面量。lessons #17 | -| 数字能渲染,但永远不动 | seek 抑制了 `onUpdate` 回调 | 渲染器已修(`pause(t, false)`)。帧里改用 transform 数字卷轴。lessons #27 | -| 视频变成 1280×720 而不是 1920×1080 | `viewport` 传给了 `browser.launch()` —— 它是 *context* 级选项 | 传给 `newPage()`。lessons #18 | -| 封面出成 1 倍图,但日志写着 2 倍 | 同上,`deviceScaleFactor` 的孪生坑 | 传给 `newPage()` + `screenshot({ scale: 'device' })`;用 PIL 核尺寸。lessons #28 | -| Windows 上 `No such file or directory` | 非 ASCII 路径 —— Windows 的 ffmpeg 把 UTF-8 当 ANSI 读 | 项目、输出、抽帧路径全部保持 ASCII。lessons #11 | -| Node 找不到 `/tmp/...` | Git Bash 的 `/tmp` ≠ Node 的 `/tmp` | 传 `C:/...` 正斜杠绝对路径。lessons #12 | -| 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 | +| 每一帧都是静止首帧,且不报错 | 时间轴没注册到 `window.__tl`;或 `page.evaluate` 传了**字符串**而非函数字面量(Playwright 只求值一次,函数体根本不执行) | 传真正的函数字面量 | +| 视频变成 1280×720 | `viewport` 传给了 `browser.launch()` —— 它是 *context* 级选项 | 传给 `newPage()` | +| 封面出成 1 倍图 | 同上,`deviceScaleFactor` 的孪生坑 | 传 `newPage()` + `screenshot({ scale: 'device' })` | +| 数字能渲染但永远不动 | seek 抑制了 `onUpdate` 回调 | 渲染器已修(`pause(t, false)`);帧里改用 transform 数字卷轴 | +| Windows 上 `No such file or directory` | 非 ASCII 路径 —— Windows 的 ffmpeg 把 UTF-8 当 ANSI 读 | 路径保持 ASCII | +| 预览渲完,帧 PNG 不见了 | 预览模式按设计会清空帧目录 | 加 `--keep-frames` | **`references/lessons.md` 是这个仓库里最值钱的文件。** 34 条编号记录,每一条都是一个 「成片看着挺正常、其实是错的」的 bug —— 包括它一开始是怎么被误判的。从零开始 debug 之前, @@ -450,71 +293,64 @@ my-video/ ## 适合谁 -**适合** +**适合**:想成规模地把文章 / 文档 / 某个主题做成带解说的视频;已经在用 AI 编程智能体, +且宁愿用 HTML 描述画面也不想学动效软件;需要可复现的输出;需要离线跑或拒绝按次计费。 -- 想成规模地把文章、文档或某个主题做成带解说的视频。 -- 已经在用 AI 编程智能体,并且宁愿用 HTML 描述画面,也不想学一套动效软件。 -- 需要可复现的输出 —— 同一个项目重渲一次,帧应当一致。 -- 需要离线跑,或者不接受按次计费。 -- 想把字幕、音画同步、封面、QC 这些基础工程直接拿走用。 +**不适合**:实拍剪辑、真人口播、复刻已有视频;想要图形化拖拽编辑器(画面是代码,这是刻意 +选择);想把 React 组件动画作为创作模型 —— 那是姊妹项目 +[`anything2explainer`](https://github.com/Vincentwei1021/anything2explainer) 的领域。 -**不适合** +--- -- 实拍剪辑、真人口播,或者复刻一条已有的视频。 -- 想把「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 `)—— 目前需要临时项目法绕过 -- [ ] 封面排版终态测量器(行数 / 四边边距 / 元素重叠 / 钩子字号一次判完)—— 目前需自行写十几行 -- [ ] 通过 `cover_build.mjs` 里的规格表支持更多封面比例 -- [ ] 风格目录补英文说明(目前描述以中文为主) +- [ ] 封面排版终态测量器(行数 / 四边边距 / 元素重叠 / 钩子字号一次判完) +- [ ] 更多封面比例 +- [ ] 风格目录补英文说明 - [ ] 可选背景音乐轨,并对解说自动做闪避(ducking) -- [ ] 「焦点 / 发光纪律」的 lint 规则(目前只能靠肉眼) -- [ ] 常见视频形态的项目模板(60 秒短片、3 分钟讲解) --- ## 参与贡献 欢迎提 issue 和 PR。最有价值的贡献,是一条**已被定位的静默失败**,加进 -`references/lessons.md` —— 写下它一开始是怎么误导你的、证明病因的证据、修法,以及由此 -得出的通用规则。 - -请先读 [`CONTRIBUTING.md`](CONTRIBUTING.md),里面有本项目的硬性规则、端到端自检方法, -以及 PR 检查清单。 - -CI 会在 Linux 与 Windows 上、Python 3.9 与 3.13 上检查版本号漂移、风格目录漂移和外链引用。 +`references/lessons.md` —— 写下它一开始是怎么误导你的、证明病因的证据、修法,以及由此得出的 +通用规则。请先读 [`CONTRIBUTING.md`](CONTRIBUTING.md)。 --- -## 致谢 - -本项目的解说与音画同步方法论 —— 词边界字幕、两级时钟、语速标定、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 许可覆盖原创代码与文档。第三方组件与衍生出的风格规范仍适用其各自的条款 —— 见 +MIT 覆盖原创代码与文档。第三方组件与衍生出的风格规范仍适用其各自的条款;GSAP 按 GreenSock +的 [standard "no charge" 许可](https://gsap.com/standard-license) 内置。完整声明见 [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。 --- diff --git a/SKILL.md b/SKILL.md index 50bfa79..5f7ae68 100644 --- a/SKILL.md +++ b/SKILL.md @@ -218,13 +218,49 @@ GSAP 用 `../assets/gsap.min.js`(本地内置)→ 主体动画压在 speech_ | `assets/frame-template.html` | 场景模板(契约注释在文件头,B() 用法示例) | | `assets/cover-template.html` | **封面模板**(封面三要素注释在文件头,可改尺寸复用为两份) | | `assets/gsap.min.js` | GSAP 3.13 本地内置(离线渲染;License 见同目录 `gsap-README.md`) | -| `README.md` / `README.en.md` | 对外项目说明(中文为主,英文副) | +| `README.md` / `README.en.md` | 对外项目说明(**README.md 中文为默认**,`README.en.md` 英文;含跨 Agent 安装指引) | +| `CONTRIBUTING.md` | 贡献指南:硬性规则、端到端自检、PR 清单 | | `CHANGELOG.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) + +装完**新开一个会话**,让智能体重新扫描技能目录。同理,调用本技能时应把 +`` 替换成实际安装路径 —— 派子 agent 时要展开成**绝对路径**写进 prompt。 + ## 打包移植 技能目录自包含,不引用本机任何绝对路径(脚本按自身位置定位 `SKILL_ROOT`)。 +仅有两处**兜底探测**会去探 WorkBuddy 托管的运行时目录(`setup_env.sh` 的 Python/Node +候选、`peek_frame.mjs` 的技能根候选)—— 探不到就自动跳过,不影响其它平台。 **零依赖声明(重要)**:本技能**不需要** html-video 或 anything2explainer 存在。 两个来源项目的名字只出现在:① 代码注释的出处署名 ② `scripts/import_styles.py` @@ -237,7 +273,7 @@ qc → cover)**完全不碰这两个项目**。风格库是抄成纯文本的 python package_skill.py # → dist/html-explainer-v<版本>.zip python package_skill.py --with-deps # 含 playwright-core,~14MB(目标机全程离线) -# ② 新机器:解压到任意目录(推荐 ~/.workbuddy/skills/) +# ② 新机器:解压到任意目录(放进上表任一「个人级」技能目录即可被该智能体识别) bash setup_env.sh --install # 自检 + 装缺项(先装后判定,装成功即算就绪) # ③ 跑一遍端到端(可选,验证链路) @@ -249,8 +285,8 @@ python scripts/qc_check.py --project . && node scripts/cover_build.mjs . ``` **依赖探测顺序**(都尽量用系统已有的,避免下载): -- Python:先找托管 venv,再 `python3` / `python` -- Node:`node -v` ≥18;没有则扫 `~/.workbuddy/binaries/node/versions/*` +- Python:先找 WorkBuddy 托管 venv,再 `python3` / `python` +- Node:`node -v` ≥18;没有则扫 `~/.workbuddy/binaries/node/versions/*`(WorkBuddy 托管路径) - 浏览器:Chrome → Edge → playwright chromium → `BROWSER_PATH` 环境变量 - ffmpeg:PATH → `imageio_ffmpeg.get_ffmpeg_exe()`(pip 装依赖时自带静态二进制) - GSAP:包内 `assets/gsap.min.js`(离线,不外链) @@ -263,6 +299,9 @@ python scripts/qc_check.py --project . && node scripts/cover_build.mjs . - 本技能**原创代码与文档**:MIT © 2026 **Moh**(见 `LICENSE`)。 - **画面风格目录**(`references/style-catalog.md` / `.json`)由 [nexu-io/html-video](https://github.com/nexu-io/html-video) (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`)、 playwright-core(Apache-2.0)。 - 完整清单与逐条授权见 **`THIRD_PARTY_NOTICES.md`**。发布/再分发前请连同该文件一起带上。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 28a7133..16a4460 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,123 +1,110 @@ -# Third-Party Notices +# 第三方声明 -`html-explainer` is MIT-licensed (see [LICENSE](LICENSE)). That license covers the -**original code and documentation** authored for this project. +`html-explainer` 以 MIT 许可发布(见 [LICENSE](LICENSE))。该许可覆盖本项目原创的 +**代码与文档**。 -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 -> product), you must carry this file along with it.** +> **如果你要再分发本项目(fork、打包 zip、发布为包、镜像,或放进某个产品里),必须把本文件 +> 一起带上。** --- -## 1. Bundled in this repository +## 1. 随仓库打包的组件 ### GSAP 3.13.0 — `assets/gsap.min.js` -- **Copyright**: © 2025 GreenSock. All rights reserved. Author: Jack Doyle. -- **License**: GreenSock standard "no charge" license — -- **Status**: Bundled verbatim for offline rendering. Not modified. -- **Note**: This is **not** an OSI license. GSAP is free to use, including commercially, - under its standard license terms. The copyright header inside `assets/gsap.min.js` - must not be removed. Upstream README is kept alongside it at - `assets/gsap-README.md`. +- **版权**:© 2025 GreenSock. All rights reserved. 作者 Jack Doyle。 +- **许可**:GreenSock standard "no charge" 许可 —— +- **状态**:为离线渲染逐字内置,未做修改。 +- **说明**:这**不是**一个 OSI 许可。GSAP 在其标准许可条款下可免费使用(含商业用途)。 + `assets/gsap.min.js` 里的版权头不得移除。上游 README 一并保留在 `assets/gsap-README.md`。 --- -## 2. Runtime dependencies (installed, not vendored) +## 2. 运行时依赖(安装,而非随包内置) -These are declared as dependencies and fetched by `bash setup_env.sh --install`. -They are not committed to this repository. +这些在依赖清单中声明,由 `bash setup_env.sh --install` 拉取,**不**提交进本仓库。 -| Component | Version (tested) | License | Upstream | +| 组件 | 版本(已测) | 许可 | 上游 | |---|---|---|---| -| `edge-tts` | 7.2.8 (pinned) | LGPL-3.0 | | +| `edge-tts` | 7.2.8(钉死) | LGPL-3.0 | | | `playwright-core` | 1.63.0 | Apache-2.0 | | | `numpy` | 2.5.3 | BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0 | | | `pillow` | 12.3.0 | MIT-CMU | | | `imageio-ffmpeg` | 0.6.0 | BSD-2-Clause | | -| `ffmpeg` (binary) | via `imageio-ffmpeg` | LGPL-2.1+ / GPL-2.0+ (varies by build) | | +| `ffmpeg`(二进制) | 来自 `imageio-ffmpeg` | LGPL-2.1+ / GPL-2.0+(随构建而异) | | -**A note on `edge-tts` (LGPL-3.0) and `ffmpeg`:** these are invoked as **separate -processes / imported as unmodified libraries**; this project does not statically link -them or ship modified copies. If you plan to redistribute a *bundled* binary build of -ffmpeg, review FFmpeg's own licensing page — build flags determine whether a given -binary is LGPL or GPL. +**关于 `edge-tts`(LGPL-3.0)与 `ffmpeg`**:本项目以**独立进程调用 / 作为未修改的库导入** +的方式使用它们,不做静态链接,也不分发修改过的副本。如果你打算再分发一个**打包的** ffmpeg +二进制构建,请先看 FFmpeg 自己的许可页面 —— 编译选项决定该二进制属于 LGPL 还是 GPL。 -`edge-tts` is pinned to `7.2.8` intentionally: the 7.x series has had breaking changes -in its speech-boundary API, and this project depends on `WordBoundary` events. +`edge-tts` 是**刻意**钉在 `7.2.8` 的:7.x 系列在其语音边界 API 上有过破坏性变更,而本项目 +依赖 `WordBoundary` 事件。 --- -## 3. Derived content — the style catalog +## 3. 衍生内容 —— 风格目录 -`references/style-catalog.md` and `references/style-catalog.json` are a **transcription -of third-party template design specifications** — canvas size, typography, timeline -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. +`references/style-catalog.md` 与 `references/style-catalog.json` 是**对第三方模板设计规范的 +转写** —— 画布尺寸、字体排印、时间轴结构、配色纪律 —— 覆盖 23 种具名风格。其中**不包含**任何 +上游的模板源码、标记或素材。记录的只有事实性的设计参数与风格名称,目的是让人能从零写出一个 +兼容的画面帧。 -This catalog was produced by `scripts/import_styles.py`, which reads an installed copy of -html-video and writes the specification out as plain text. **It is a one-time porting -tool.** Running an html-explainer video never reads html-video, and no html-video code is -executed or linked at any point. +该目录由 `scripts/import_styles.py` 产出:它读取一份**已安装的** html-video,把规范转写成纯 +文本。**它是一个移植期的一次性工具。** 跑一条 html-explainer 视频从不读取 html-video, +任何环节都不会执行或链接 html-video 的代码。 -### 3.1 `nexu-io/html-video` — 23 templates +### 3.1 `nexu-io/html-video` —— 23 个模板 -- **Copyright**: the html-video authors (Open Design / nexu-io) -- **License**: Apache-2.0 -- **Source**: -- **What was derived**: design specifications and style identifiers for all 23 templates - (`frame-bold-poster` … `vfx-text-cursor`), transcribed into `references/style-catalog.*`. -- **Changes made**: specifications reformatted into a flat Markdown/JSON catalog; each - template classified as `rich` (single-file CSS `@keyframes`) or `gsap` (multi-composition); - annotated with adaptation cost and subtitle-safe-zone constraints. Original template - code, compositions, and assets were **not** copied. -- Apache-2.0 requires that recipients of a derivative work receive a copy of the license. - A verbatim copy is included at [`licenses/Apache-2.0.txt`](licenses/Apache-2.0.txt). - The upstream repository ships no `NOTICE` file, so no `NOTICE` propagation applies - (Apache-2.0 §4(d)). +- **版权**:html-video 的作者(Open Design / nexu-io) +- **许可**:Apache-2.0 +- **来源**: +- **衍生了什么**:全部 23 个模板的设计规范与风格标识符(`frame-bold-poster` … + `vfx-text-cursor`),转写进 `references/style-catalog.*`。 +- **做了哪些改动**:规范重排为扁平的 Markdown / JSON 目录;每个模板分类为 `rich` + (单文件 CSS `@keyframes`)或 `gsap`(多 composition);补充了改编成本与字幕禁区约束的 + 标注。原始模板代码、composition 与素材**没有**被复制。 +- Apache-2.0 要求衍生作品的接收者获得一份许可副本。逐字副本见 + [`licenses/Apache-2.0.txt`](licenses/Apache-2.0.txt)。上游仓库不含 `NOTICE` 文件, + 因此不涉及 `NOTICE` 传递(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 -provenance that html-video attaches to each of its templates. Seven of the 23 styles -trace back to an earlier MIT-licensed project. Those notices are reproduced here because -this project's catalog describes the same styles. +`references/style-catalog.json` 为每种风格记录了 `via` 字段,保留了 html-video 为其模板附带的 +来源信息。23 种风格中有 7 种可追溯到更早的 MIT 许可项目。由于本项目的目录描述了同样的风格, +这些声明在此一并复现。 -Exact mapping, as recorded in the catalog: +按目录中记录的精确对应关系: -#### `frontend-slides` — 4 styles +#### `frontend-slides` —— 4 种风格 -- **Copyright**: Zara Zhang © 2025 -- **License**: MIT -- **Source**: -- **Styles**: `frame-bold-poster`, `frame-bold-signal`, `frame-creative-voltage`, +- **版权**:Zara Zhang © 2025 +- **许可**:MIT +- **来源**: +- **风格**:`frame-bold-poster`、`frame-bold-signal`、`frame-creative-voltage`、 `frame-electric-studio` -#### `huashu-design` — 3 styles +#### `huashu-design` —— 3 种风格 -- **Copyright**: alchaincyf (花叔 · 花生) © 2026 -- **License**: MIT -- **Source**: -- **Styles**: `frame-build-minimal`, `frame-pentagram-stat`, `frame-takram-organic` +- **版权**:alchaincyf(花叔 · 花生)© 2026 +- **许可**:MIT +- **来源**: +- **风格**:`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 -- **License**: MIT (as recorded in the catalog's `license` field for this entry) -- **Styles**: `frame-product-promo-30s` — recorded upstream as a fork of - `linear-promo-30s`, with brand-specific copy and assets replaced by generic - placeholders. +- **版权**:Nate Herk +- **许可**:MIT(按目录中该条目的 `license` 字段记录) +- **风格**:`frame-product-promo-30s` —— 上游记录为 `linear-promo-30s` 的 fork, + 品牌相关的文案与素材已替换为通用占位内容。 -html-video's own `ATTRIBUTIONS.md` documents the first two upstreams and states its rule -for all such templates: static slide designs were re-expressed as original CSS/SVG -keyframe timelines, with new sample data and no verbatim source copying, and the studio -names attached to some styles (Pentagram, Build, Takram) are recorded as **inspiration -only, never as affiliation or endorsement**. This project inherits that position. +html-video 自己的 `ATTRIBUTIONS.md` 记录了前两个上游,并给出了它对此类模板的处理原则:静态 +幻灯片设计被重新表达为原创的 CSS/SVG 关键帧时间轴,配以新的示例数据,不做逐字源码复制;部分 +风格上挂着的设计事务所名字(Pentagram、Build、Takram)记录为**仅表灵感来源,绝不表示隶属或 +背书**。本项目沿用这一立场。 -MIT License notice for all three upstreams above: +上述三个上游的 MIT 许可声明: ``` 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. ``` -### 3.3 Studio names — inspiration only, no affiliation +### 3.3 设计事务所名称 —— 仅表灵感,无隶属关系 -Some style names reference real design studios or publications as a record of stylistic -inspiration (Pentagram, Build, Takram, NYT, Vignelli, Swiss Grid). These are **factual -statements of inspiration only**. This project — and html-video before it — is **not -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. +部分风格名引用了真实的设计事务所或出版物,作为风格灵感来源的记录(Pentagram、Build、 +Takram、NYT、Vignelli、Swiss Grid)。这些**仅为事实性的灵感说明**。本项目 —— 以及它之前的 +html-video —— **与其中任何一方均无隶属、背书或赞助关系**。项目中不含任何第三方的 logo、 +字标、字体文件或品牌素材;所有画面都用系统字体从零创作。 --- -## 4. Design lineage of the workflow +## 4. 方法论上的思路来源 -The narration/audio-sync methodology (word-boundary subtitles, two-level clock, speech-rate -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 -and historical notes. +解说与音画同步方法论(词边界字幕、两级时钟、语速标定、QC 判据)参考了下列项目,它们 +**由不同作者独立开发,与本项目不是同一作者**: + +- **[Vincentwei1021/anything2explainer](https://github.com/Vincentwei1021/anything2explainer)** + —— 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 | -| `THIRD_PARTY_NOTICES.md` (this file) | Apache-2.0 §4 + MIT notice retention | -| `licenses/Apache-2.0.txt` | Apache-2.0 §4(a) requires a copy of the license | -| `assets/gsap-README.md` | GSAP license reference; keep the header in `gsap.min.js` intact | +| `LICENSE` | 原创代码的 MIT 条款 | +| `THIRD_PARTY_NOTICES.md`(本文件) | Apache-2.0 §4 + MIT 通知保留要求 | +| `licenses/Apache-2.0.txt` | Apache-2.0 §4(a) 要求附带一份许可副本 | +| `assets/gsap-README.md` | GSAP 许可参考;并保持 `gsap.min.js` 内的版权头完整 | -*Last reviewed: 2026-09-21.* +*最后审阅:2026-09-21。* diff --git a/scripts/peek_frame.mjs b/scripts/peek_frame.mjs index 0ae3baf..8a46bcb 100644 --- a/scripts/peek_frame.mjs +++ b/scripts/peek_frame.mjs @@ -24,7 +24,16 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // 本脚本放在**项目**的 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() { const cands = []; if (process.env.HTML_EXPLAINER_ROOT) cands.push(process.env.HTML_EXPLAINER_ROOT); @@ -35,8 +44,12 @@ function resolveSkillRoot() { if (p === d) break; d = p; } - if (process.env.USERPROFILE) cands.push(path.join(process.env.USERPROFILE, '.workbuddy', 'skills', 'html-explainer')); - if (process.env.HOME) cands.push(path.join(process.env.HOME, '.workbuddy', 'skills', 'html-explainer')); + const homes = [process.env.USERPROFILE, process.env.HOME].filter(Boolean); + for (const home of homes) { + for (const seg of SKILL_SUBDIRS) { + cands.push(path.join(home, ...seg, 'html-explainer')); + } + } for (const c of cands) { try { if (fs.existsSync(path.join(c, 'node', 'node_modules', 'playwright-core', 'package.json'))) return c; diff --git a/setup_env.sh b/setup_env.sh index 7abc2cc..d61e5f8 100644 --- a/setup_env.sh +++ b/setup_env.sh @@ -29,6 +29,8 @@ info() { echo " → $1"; } echo "== html-explainer 环境自检 ==" # ---------- Python ---------- +# 候选顺序:WorkBuddy 托管 venv(本机既有依赖,优先复用)→ 系统 python3 / python。 +# 前两个是 WorkBuddy 专属路径,其它平台探不到就自动跳过,不影响使用。 PY="" for c in "$HOME/.workbuddy/binaries/python/envs/default/Scripts/python.exe" \ "$HOME/.workbuddy/binaries/python/envs/default/bin/python" \ @@ -63,6 +65,7 @@ if command -v node >/dev/null 2>&1; then NV="$(node_major)" if [ "${NV:-0}" -ge 18 ]; then ok "Node: $(node -v)"; else miss "Node ≥18 需要,当前 $(node -v)"; fi else + # 兜底:WorkBuddy 托管的 Node(非标准安装,路径带版本号)。仅本机可用,其它平台跳过。 for d in "$HOME/.workbuddy/binaries/node/versions"/*/; do if [ -x "$d/node.exe" ] || [ -x "$d/node" ]; then export PATH="$d:$PATH"; ok "Node(托管): $(node -v)"; break