Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/skills/new-scene/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: 在 explainer-kit 里新写一场讲解(旁白 + 画面)。用

# 写一场

动手前先过一遍 `docs/principles.md` 的六条原理;遇到组件里没有现成答案的需求,从原理推(比如「要和某个词同步」就是原理 1:用 `w()` 取帧)。

## 1. 先写旁白,再想画面

旁白就是时间轴。在纸面上把文稿切成「一句话一个画面」,每个画面变化前放一个 `[[cue]]`:
Expand Down Expand Up @@ -53,4 +55,4 @@ node tools/new-scene.mjs s10 --look paper --chapter "第十场 · 标题" --text

## 5. 交付前

`npx tsc --noEmit` 通过,然后按 `review` skill 审一遍动态、声音和竖屏。
`npm run verify` 通过,然后按 `review` skill 审一遍动态、声音和竖屏。
2 changes: 1 addition & 1 deletion .claude/skills/new-style-pack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ node tools/new-scene.mjs s10 --look chalk --chapter "第十场 · 黑板风" --t

`new-pack` 生成 `src/chalk/index.tsx`:经过 `themed()` 的调色板、带缓慢光斑的背景、章节卡、`Title`/`Card` 组件、`Look`。

## 3. 每个包都要满足
## 3. 每个包都要满足(背后是 `docs/principles.md` 的原理 2、3、4)

- 调色板必须经过 `themed('<pack>', {...}, {accent: [...], accent2: [...]})`,把主色挂到 accent 槽位,品牌色才能一键替换;颜色写 `#rrggbb`。
- 背景永远有一点动作(漂移光斑、滚动纹理、闪烁),不能整屏静止。
Expand Down
2 changes: 2 additions & 0 deletions .claude/skills/publish/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ node tools/publish.mjs --skip-tts # 只改了画面,沿用现有配
node tools/publish.mjs --mock # 离线跑通整条流水线(静音旁白)
```

publish 会在渲染前先跑 `tools/verify.mjs`(验收不过就停,不会白渲染几十分钟);确实要跳过用 `--no-verify`。

成品:`out/<id>-mixed.mp4`、`out/<id>-9x16-mixed.mp4`……

## 出片前确认
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ node tools/listen.mjs s05:block s05:out:30 # 一段

## 交付前的检查清单

- `npx tsc --noEmit`、`pytest` 通过
- `npm run verify` 全部通过(类型、源文件一致、每场每种画幅可渲染、两次渲染逐字节一致)
- 改过的每场都拼帧看过,主画幅和 9x16 各一遍
- 音效位置用 `listen` 核对过
- 真配音(Windows 上 `python tts/gen.py`)后再看一遍:cue 位置会变,写死的帧数会错位
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ Remotion 讲解视频模板:旁白里写 `[[cue]]` 标记,edge-tts 的逐词

- 面向用户的文档用简体中文;代码、命令、标识符用英文。
- 本机是 Windows(渲染、真 TTS 验证在那里做);云端沙箱只做重构、类型检查、`--mock` 时间轴和单测。沙箱里也能看画面和听音效:`tools/` 的渲染脚本会自动用 `/opt/pw-browsers` 预装的 headless shell(`node tools/strip.mjs s01:quote`、`node tools/overview.mjs`、`node tools/listen.mjs s05`),只看,不提交 `out/`。
- 做事的原则:往原理去理解,随后一通百通;可交付、可重复、稳定输出,才是 AI 赋能的前提。项目的六条原理见 `docs/principles.md`,动手前先对照它想清楚,新需求优先从原理推,不要只套组件。交付前跑 `npm run verify`(`--quick` 只做类型和一致性检查),它不过就不算完成。
- 常做的事有项目 skill(`.claude/skills/`):`new-scene` 写一场、`review` 审片、`new-style-pack` 新风格包、`publish` 出片。反复手动做的步骤,优先收成 `tools/` 脚本或补进这些 skill。
- 不在仓库里提交字体文件和渲染产物(`out/`、`public/fonts/`)。
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

仓库自带一支约 3 分钟的演示片(9 场),每场换一套风格包:`film`(电影)、`slides`(幻灯片 + KaTeX 公式)、`paper`(手绘笔记)、`neon`(霓虹终端)、`editorial`(杂志排版)、`math`(数学推导)、`keynote`(发布会)、`pixel`(像素游戏)、`ink`(水墨)。演示片的时间轴和音频是用 `--mock` 生成的静音占位,克隆下来不用联网合成就能预览。

先读 [`docs/principles.md`](docs/principles.md):六条原理讲清楚这个模板为什么这样设计,看懂它,后面的组件和工具都能推出来。

## 快速开始

```bash
Expand Down Expand Up @@ -175,6 +177,8 @@ const {p, q} = life(f, c('cards'), c('formula')); // 卡片在 formula 时
6. **配乐**:`python tools/music.py` → `out/music.wav`。按 timings 每场一段和声,每个章节开头一声轻击,重配音后重跑即可对齐。
7. **混音**:`python tools/mix.py` → `out/<id>-mixed.mp4`(竖屏 `python tools/mix.py 9x16`)。配乐低通后按旁白做 sidechain 压缩,再整体 `loudnorm=I=-16:TP=-1.5:LRA=11`。

交付前:`npm run verify`。依次检查类型、源文件一致(旁白稿 ↔ 时间轴 ↔ 音频 ↔ 登记表 ↔ 音效库)、每场在每种画幅下都能渲染、同一批帧渲染两次逐字节一致。`--quick` 只跑前两项;示意图在 `out/verify/`。

第 2、5、6、7 步可以一条命令跑完:`node tools/publish.mjs`(`--formats 9x16,1x1` 或 `all` 同时出其他画幅,`--skip-tts` 沿用现有配音,`--mock` 离线跑通流程)。

## 画幅和品牌
Expand All @@ -191,7 +195,8 @@ const {p, q} = life(f, c('cards'), c('formula')); // 卡片在 formula 时
|---|---|
| `node tools/new-scene.mjs s10 --look paper --chapter "…" --text "[[a]]……"` | 追加旁白、生成能直接跑的场景文件(每个 cue 一段跟着旁白出现的字)、登记、跑 mock 时间轴 |
| `node tools/new-pack.mjs chalk --base "#1F2B26" --accent "#F2C14E"` | 生成一个新风格包骨架:接好品牌色的调色板、会动的背景、章节卡、组件、Look |
| `node tools/publish.mjs --formats all` | 配音 → 各画幅渲染 → 配乐 → 混音 |
| `node tools/publish.mjs --formats all` | 配音 → 验收 → 各画幅渲染 → 配乐 → 混音 |
| `npm run verify` | 交付前的验收:类型、源文件一致、每场可渲染、渲染结果可复现 |
| `node tools/strip.mjs` / `overview.mjs` / `listen.mjs` | 审动态 / 看全片 / 听音效 |

`.claude/skills/` 里有四个项目 skill,在 Claude Code 里打开这个仓库就能用:`new-scene`(从文稿到一场)、`review`(审片清单)、`new-style-pack`(新风格包的要求)、`publish`(出片和踩过的坑)。
Expand Down Expand Up @@ -266,7 +271,7 @@ const {p, q} = life(f, c('cards'), c('formula')); // 卡片在 formula 时

explainer-kit is a Remotion template for narrated explainer videos. Narration lives in `tts/script.json` with inline markers: `[[cue]]` marks an animation/shot trigger, `||` inserts a dramatic pause, and `<<key|...>>` reads a quote with an alternate voice. `tts/gen.py` synthesizes each scene with edge-tts, records word boundaries and writes `public/audio/<scene>.mp3` plus `src/timings.json`. Scenes read frames through `useScene()` — `c('cue')`, `w('word')`, `rel('word', 'cue')` — so durations and animations follow the audio after any rewrite.

Quick start: `npm i && npm run studio` shows the bundled ~3 min, nine-scene demo (one scene per style pack), whose timings and silent audio were produced by `python tts/gen.py --mock` (no network; word times estimated from character count). `npm run fonts` downloads the Noto Sans SC / Noto Serif SC variable fonts (OFL) into `public/fonts/`; without them the stacks fall back to system fonts. All size, fps, voice, pace and font settings live in `kit.config.json`. Nine optional style packs are included: `film` (letterbox, grain, grade, cue-driven shots, paper props), `slides` (panels, chips, karaoke subtitles), `paper` (hand-drawn strokes that draw on and boil, sticky notes, highlighter), `neon` (scrolling grid floor, scanlines, glowing type, terminal, node graph with flowing pulses, glitch), `editorial` (masked kinetic headlines, slammed numbers, colour-block wipes, ticker), `math` (Manim-style axes, traced plots, sliding tangent, step-by-step TeX), `keynote` (one morphing shape, cursor clicks, liquid glass, screen-studio zoom), `pixel` (320×180 canvas upscaled with hard pixels, walking hero, coins, RPG dialog) and `ink` (ink-wash mountains bleeding through mist, brush strokes, vertical calligraphy, red seal). `src/core/motion.ts` holds a shared motion vocabulary (enter/exit with `life` + `move`, springs, idle `drift`, `punch`, `shake`), `<Spoken>` reveals on-screen text exactly as the narrator says it, and `Shots` cuts on cues with `fade`/`push`/`up`/`zoom`/`wipe`/`whip`/`iris`/`black`/`flash` transitions. Sound effects are synthesized by `tools/sfx.py` into `public/sfx/` and placed with `<Sfx at={frame} name="pop" />` (moving `Shots` transitions add their own); the score pulses on a `music.bpm` grid that scenes can follow with `useBeat()`. `brand.accent`/`accent2` in `kit.config.json` recolour every pack, `theme.<pack>` overrides single keys, and `formats` adds portrait (9:16) and square (1:1) compositions that lay the 16:9 picture out with a title band, large captions and a progress bar. `tools/publish.mjs` runs TTS → render (all formats) → score → mix in one go; `tools/new-scene.mjs` and `tools/new-pack.mjs` scaffold scenes and style packs; `.claude/skills/` holds Claude Code skills for writing a scene, reviewing, building a pack and publishing. Review with `tools/stills.mjs`, `tools/pick.mjs`, `tools/sheet.py`, `tools/strip.mjs` (a filmstrip of frames around a cue, to judge motion rather than end states), `tools/overview.mjs` (one frame per scene) and `tools/listen.mjs` (renders a range's audio and lists sound events by cue); render with `npm run render` (uses `--gl=angle`); add a synthesized score with `tools/music.py` and mix/normalize to -16 LUFS with `tools/mix.py`.
Quick start: `npm i && npm run studio` shows the bundled ~3 min, nine-scene demo (one scene per style pack), whose timings and silent audio were produced by `python tts/gen.py --mock` (no network; word times estimated from character count). `npm run fonts` downloads the Noto Sans SC / Noto Serif SC variable fonts (OFL) into `public/fonts/`; without them the stacks fall back to system fonts. All size, fps, voice, pace and font settings live in `kit.config.json`. Nine optional style packs are included: `film` (letterbox, grain, grade, cue-driven shots, paper props), `slides` (panels, chips, karaoke subtitles), `paper` (hand-drawn strokes that draw on and boil, sticky notes, highlighter), `neon` (scrolling grid floor, scanlines, glowing type, terminal, node graph with flowing pulses, glitch), `editorial` (masked kinetic headlines, slammed numbers, colour-block wipes, ticker), `math` (Manim-style axes, traced plots, sliding tangent, step-by-step TeX), `keynote` (one morphing shape, cursor clicks, liquid glass, screen-studio zoom), `pixel` (320×180 canvas upscaled with hard pixels, walking hero, coins, RPG dialog) and `ink` (ink-wash mountains bleeding through mist, brush strokes, vertical calligraphy, red seal). `src/core/motion.ts` holds a shared motion vocabulary (enter/exit with `life` + `move`, springs, idle `drift`, `punch`, `shake`), `<Spoken>` reveals on-screen text exactly as the narrator says it, and `Shots` cuts on cues with `fade`/`push`/`up`/`zoom`/`wipe`/`whip`/`iris`/`black`/`flash` transitions. `docs/principles.md` explains the six principles behind the design (one time source, frames as pure functions, enter/breathe/exit, looks as clothing, one canvas for all formats, nothing derived is hand-edited); `npm run verify` is the delivery gate (types, source consistency, every scene renders in every format, two renders are byte-identical) and `publish` runs it before rendering. Sound effects are synthesized by `tools/sfx.py` into `public/sfx/` and placed with `<Sfx at={frame} name="pop" />` (moving `Shots` transitions add their own); the score pulses on a `music.bpm` grid that scenes can follow with `useBeat()`. `brand.accent`/`accent2` in `kit.config.json` recolour every pack, `theme.<pack>` overrides single keys, and `formats` adds portrait (9:16) and square (1:1) compositions that lay the 16:9 picture out with a title band, large captions and a progress bar. `tools/publish.mjs` runs TTS → render (all formats) → score → mix in one go; `tools/new-scene.mjs` and `tools/new-pack.mjs` scaffold scenes and style packs; `.claude/skills/` holds Claude Code skills for writing a scene, reviewing, building a pack and publishing. Review with `tools/stills.mjs`, `tools/pick.mjs`, `tools/sheet.py`, `tools/strip.mjs` (a filmstrip of frames around a cue, to judge motion rather than end states), `tools/overview.mjs` (one frame per scene) and `tools/listen.mjs` (renders a range's audio and lists sound events by cue); render with `npm run render` (uses `--gl=angle`); add a synthesized score with `tools/music.py` and mix/normalize to -16 LUFS with `tools/mix.py`.

## License

Expand Down
80 changes: 80 additions & 0 deletions docs/principles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 原理

> 往原理去理解,随后一通百通;可交付、可重复、稳定输出,才是 AI 赋能的前提。

这份文档讲 explainer-kit 为什么是现在这个样子。只记组件用法,换一个需求就得重新问;把下面几条原理弄明白,新需求多半能自己推出来,也能判断 AI 写的代码对不对。

## 一条主线

```
旁白稿 ──gen.py──▶ 逐词时间戳 ──timeline.ts──▶ 帧号 ──场景(纯函数)──▶ 画面
[[cue]] timings.json c() w() rel() f ↦ 像素
```

整个项目只做一件事:**把「说到哪」翻译成「第几帧该是什么样」**。下面每条原理,都是这条主线在某个方面的要求。

## 六条原理

### 1. 时间只有一个来源:旁白

画面不自己计时,只问「这个 cue 在第几帧」「这个词在第几帧」。秒数只在 `gen.py` 产生,经 `timings.json` 进入画面。

- **所以**:改一句旁白、换一个音色,重跑配音,所有动画和字幕自动对上。写死的帧数会错位,按 cue/词取帧的不会。
- **推论**:屏幕上的大字也该跟着声音走,而不是按固定速度打字,所以有 `<Spoken>`。音效、配乐节拍也挂在同一条时间线上(`<Sfx at>`、`useBeat()`)。

### 2. 每一帧是帧号的纯函数

同一个帧号,永远画出同一张图。不能用 `Math.random()`、当前时间,也不能把状态留到下一帧;需要随机就用 `rng(seed)`、`noise(seed, x)`。

- **所以**:渲染可以多进程乱序并行;审片工具可以直接跳到任意一帧(`strip`、`overview` 都靠 `<Freeze>` 冻结帧);两次出片结果一致。
- **验证**:`npm run verify` 的第 4 关,把同一批帧渲染两次,逐字节比较。

### 3. 动 = 进场 + 停留时的呼吸 + 退场

所有动效都是「一个 0→1 的进度」映射成样式:`tween` 算进度,`life(f, at, out)` 给出进场 `p` 和退场 `q`,`move(kind, p, q)` 变成 CSS。

- **所以**:画面问题都能归到这三段。死帧是停留阶段没有动作,加 `drift` 或让背景自己动;重影和重叠是旧内容没退场,补上 `out`;生硬是缺进度曲线,换 `EASE` 或弹簧。
- **推论**:能变形就别切(keynote 的 `Morph`)、容器先动内容后进、一个镜头最多停 1 秒,这些动效规则都是这一条的具体写法。

### 4. 风格是外面那层衣服,核心不认识风格

场景只描述「什么时候出现什么」;底色、背景、叠加层、章节卡、字幕样式由 `Look` 统一套上;颜色经 `themed()` 暴露主色槽位。`src/core` 从不 import 任何风格包。

- **所以**:一场戏换一套风格,只改登记表里的 `look`;换品牌色只改 `kit.config.json`;删掉不用的风格包,其他代码不受影响。
- **推论**:新风格包一定要有 `themed()` 调色板、会动的背景、章节卡和 `Look`,`new-pack` 生成的骨架就是这四样。

### 5. 只在一张画布上设计,画幅交给外壳

场景永远按 1920×1080 写。竖屏和方屏由 `Video` 外壳排版:中间放画面,上面是标题,下面是大字幕。

- **所以**:多一种画幅不用改任何场景;某场想在竖屏里放大,只在登记表里加 `portrait: {zoom}`。
- **代价**:竖屏里画面偏小。要做原生竖屏,才需要按画幅单独写场景,这是有意的取舍。

### 6. 能由源文件推出来的,都不手改

`timings.json` 由 `gen.py` 生成,`public/sfx` 由 `sfx.py` 生成,配乐由 `music.py` 生成,成片由 `publish.mjs` 一条命令产出。配置只在 `kit.config.json` 一处。

- **所以**:任何人、任何一台机器,拿到同一份仓库,就能出同一支片子。
- **验证**:`tests/test_sync.py` 检查旁白稿、时间轴、音频、登记表是否一致;`tests/test_sfx.py` 检查音效名、合成配方和文件是否一致。

## 可交付、可重复、稳定输出

| | 在这个项目里的意思 | 靠什么保证 |
|---|---|---|
| **可交付** | 一条命令从仓库出成片,不依赖记忆和手工步骤 | `node tools/publish.mjs`;`new-scene` / `new-pack` 生成能直接跑的代码 |
| **可重复** | 同样的输入,得到同样的输出 | 原理 2(纯函数)+ 原理 6(生成物有源);素材全部由代码合成,依赖版本锁定 |
| **稳定输出** | 每次交付前都过同一道关,不靠运气 | `npm run verify`:类型 → 源文件一致 → 每场在每种画幅都能渲染 → 两次渲染逐字节一致;`publish` 渲染前会自动跑一遍 |

AI 写代码很快,但「快」只有落在这三条上才算数。这三条立住了,AI 加的每个功能都能被验收、能复现、能交给别人用。立不住,它每次都要从头猜一遍。

## 遇到新需求,先用原理推一遍

| 需求 | 从原理推 |
|---|---|
| 「这个图标要在说到某个词时亮起来」 | 原理 1:`w('那个词')` 拿帧号,`tween(f, 那一帧)` 做进度 |
| 「这段太平了」 | 原理 3:哪一段是空的?停留阶段加呼吸,切换时换一种进场方式或转场 |
| 「出一版抖音竖屏」 | 原理 5:场景不用改,`npm run render -- 9x16`;内容太小就加 `portrait.zoom` |
| 「客户要用他们的品牌色」 | 原理 4:`brand.accent`,个别颜色用 `theme.<pack>` 微调 |
| 「渲染出来偶尔不一样」 | 原理 2:找随机数、时间、跨帧状态;`verify` 第 4 关会告诉你 |
| 「换了配音后动画错位」 | 原理 1:某处写死了帧数,改成 `c()` / `w()` / `rel()` |
| 「又在手动重复同一串操作」 | 原理 6:收成 `tools/` 脚本或补进 `.claude/skills/` |
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"publish": "node tools/publish.mjs",
"new:scene": "node tools/new-scene.mjs",
"new:pack": "node tools/new-pack.mjs",
"verify": "node tools/verify.mjs",
"typecheck": "tsc --noEmit",
"test": "pytest"
},
Expand Down
Loading
Loading