[teamai] Push 87 resource(s) from XingfenD

This commit is contained in:
2026-09-10 16:10:45 +08:00
parent 425c9c078a
commit 65c04def51
1314 changed files with 211681 additions and 0 deletions
@@ -0,0 +1,276 @@
# 音频合成
把每个章节 `narrations.ts` 里的口播文字按 **step 颗粒度**合成 mp3,
落到 `presentation/public/audio/<chapter-id>/<step-N>.mp3`。运行时
Auto 模式会自动按 step 播放并自动推进——录屏可以一镜到底。
> **真相源**:每个章节的 `src/chapters/<NN>-<id>/narrations.ts` 是 step
> 数 + 口播文本的**唯一来源**。`outline.md` 不再参与音频合成,章节代码
> 也不再手写 `totalSteps`。这一改根除了"网页 step 和音频文件数对不上"
> 这个老问题。
合成器是 **provider-agnostic** 的:runner 本身不绑定任何 TTS 后端,每个
后端是 `scripts/tts-providers/<name>.sh` 一个文件。**内置 2 个 provider**:
| Provider | 默认 | 何时用 |
|---|---|---|
| `minimax` | ✓ | 中文口播首选(用 `mmx-cli`,要 MiniMax API key) |
| `openai` | —— | 多数 agent 已有 `OPENAI_API_KEY`;curl-based、响应快 |
换 / 加 provider 见
[`scripts/tts-providers/README.md`](../templates/scripts/tts-providers/README.md)
(脚手架跑完后路径是 `presentation/scripts/tts-providers/README.md`)。
README 里还附了 5 套**可粘贴**的现成片段(ElevenLabs / edge-tts / macOS say /
Azure / Google Cloud)和写自定义 provider 的三函数契约。
---
## 文件命名约定
```
presentation/public/audio/
├── coldopen/
│ ├── 1.mp3
│ ├── 2.mp3
│ └── ...
├── hook/
│ └── ...
└── ...
```
- 章节子目录名 = `chapters.ts` 里的 `id`
- 文件名 = `<step-N>.mp3`(**1-indexed**,对齐 narrations 数组的 index + 1)
- 格式默认 mp3。如果你写的 provider 只能出 wav,在函数里加一步 `ffmpeg`
转 mp3(参见 `tts-providers/README.md` 的 `say.sh` 示例)
---
## 标准流程
### 1. 抽取 segments
```bash
cd presentation
npm run extract-narrations
```
这会扫所有章节的 `narrations.ts`,按 `chapters.ts` 注册顺序生成
`audio-segments.json`:
```json
[
{ "chapter": "coldopen", "step": 1, "text": "...", "audio": "coldopen/1.mp3" },
{ "chapter": "coldopen", "step": 2, "text": "...", "audio": "coldopen/2.mp3" },
...
]
```
让用户**先扫一眼这个 json**,确认文本和切分都对,再开始烧 token 合成。
> 空字符串的 narration 会被自动跳过(不烧 TTS token)——运行时 Auto 模式
> 按字数估时撑过这种"无声过场"step。
### 2. 选 provider
```bash
ls scripts/tts-providers/ # 看本项目带了哪些
```
- 用默认 `minimax` → 走 [2.A](#2a-用内置-minimax-合成)
- 用内置 `openai` → 走 [2.B](#2b-用内置-openai-合成)
- 想用别的 TTS / 自带 TTS → 走 [2.C](#2c-换-provider--加自定义-provider)
- 一个都没装好 → 走 [2.D](#2d-退化路径)
#### 2.A 用内置 minimax 合成
```bash
npm run synthesize-audio # 增量:跳过已存在的 mp3
npm run synthesize-audio -- --force # 全部重合成
npm run synthesize-audio -- --voice=<voice-id> # 指定音色
```
启动时 runner 会先调 provider 的 `tts_check`:
- mmx 未安装 → 报 `mmx CLI not found in PATH`,并打印安装说明
- mmx 未登录 → 报 `mmx is not authenticated`,并提示登录命令
修完再跑。每条段打印进度:
```
[ 3/24] coldopen/3.mp3 ✓ 4s
[ 4/24] coldopen/4.mp3 skip (exists)
```
合成串行(避免 rate limit),**自动跳过已存在文件**(断点续合,不烧
重复 token)。
#### 2.B 用内置 openai 合成
```bash
export OPENAI_API_KEY=sk-... # 在 platform.openai.com 拿
PRESENTATION_TTS=openai npm run synthesize-audio
# 换音色 + HD 模型
OPENAI_TTS_MODEL=tts-1-hd PRESENTATION_TTS=openai \
npm run synthesize-audio -- --voice=nova
```
可选 env:
| 变量 | 默认 | 作用 |
|---|---|---|
| `OPENAI_API_KEY` | —— **必须** | API key |
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | 切代理 / Azure-OpenAI |
| `OPENAI_TTS_MODEL` | `tts-1` | `tts-1` 快 / `tts-1-hd` 高质量约 2× 价 |
| `--voice=` / `PRESENTATION_TTS_VOICE` | `alloy` | 可选 alloy / echo / fable / onyx / nova / shimmer |
`tts_check` 会检查 curl / jq / `OPENAI_API_KEY` 三件套,缺哪个报哪个。
#### 2.C 换 provider / 加自定义 provider
内置之外的常见后端在 `scripts/tts-providers/README.md` 里有 5 段
**可粘贴**代码片段(ElevenLabs / edge-tts / macOS `say` / Azure / Google
Cloud)。
挑一个 → 复制 README 里的代码块 → 保存为
`scripts/tts-providers/<name>.sh` → 设好环境变量 → 切换 provider 跑:
```bash
PRESENTATION_TTS=elevenlabs npm run synthesize-audio
# 或
npm run synthesize-audio -- --provider=edge-tts
```
如果用户的 TTS 完全自研,**按三函数契约**写一个 `<name>.sh` 即可:
| 函数 | 必需 | 作用 |
|---|---|---|
| `tts_synthesize <text> <out_path> [<voice>]` | ✓ | 把一段文字写成 mp3 到指定路径 |
| `tts_check` | 可选 | 启动时校验环境(CLI / key / auth),未就绪 return 非零 |
| `tts_install_help` | 可选 | `tts_check` 失败时打印怎么修 |
抄 `openai.sh`(HTTP-based)或 `minimax.sh`(CLI-based)起手最快。
详细规范在 `scripts/tts-providers/README.md`。
#### 2.D 退化路径
如果两个内置 provider 都没就绪(没装 mmx 也没有 OpenAI key)告诉用户:
```
我可以:
1. 用内置 openai provider(如果你已有 OpenAI key)
export OPENAI_API_KEY=sk-...
PRESENTATION_TTS=openai npm run synthesize-audio
2. 帮你装 MiniMax CLI(默认 provider,中文音色更稳)
npm install -g mmx-cli && mmx auth login --api-key sk-xxxxx
API key 在 https://platform.minimaxi.com 获取
3. 换其它 provider
scripts/tts-providers/README.md 里有 5 种现成代码片段:
• ElevenLabs (要 ELEVENLABS_API_KEY,英文音色最佳)
• edge-tts (免费 / 无 key / pip install edge-tts)
• macOS say (零依赖离线,质量一般,适合预览)
• Azure (要 AZURE_SPEECH_KEY)
• Google (要 gcloud auth)
复制一段保存成 tts-providers/<name>.sh,
再 PRESENTATION_TTS=<name> npm run synthesize-audio
4. 暂时跳过
稿子和 narrations 都在,你自己用任意 TTS 录制即可——文件
按 audio-segments.json 的 audio 字段命名就行。
```
不要假装合成成功。
---
## 校验时长
合成完后跑:
```bash
for f in public/audio/*/*.mp3; do
d=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f")
echo "$f ${d}s"
done
```
把每条的实际秒数汇总告诉用户。**重点关注 ≥ 15s 的条目**——口播太长意味
着该 step 的 narration 写得过密,或者 step 没拆够。让用户决定**改稿子
重合**还是**回章节代码拆 step**。
---
## 运行时如何使用合成的音频
合成完成后,**不需要任何额外配置**——脚手架的 `App.tsx` 已经接好:
| 模式 | 触发方式 | 行为 |
|---|---|---|
| **Manual**(默认) | 直接打开页面 | 不播音频,点击 / 方向键推进 |
| **Audio**(半自动) | URL `?audio=1` 或按 `M` 键 | 进入 step 自动播音频,但你手动推进(点鼠标) |
| **Auto**(全自动) | URL `?auto=1` 或按两次 `M` 键 | 进入 step 播音频 → 播完自动 next() → 进下个 step → ... |
Auto 模式首次需要按一次 `Space` 启动(绕过浏览器自动播放限制),之后
全自动跑。**录屏时打开屏幕录制 → 按 Space → 整片自动跑完 → stop**。
> **Auto 模式的推进规则就一句话**:每段音频播完 + 200ms 缓冲 → 自动 next。
> **没有"等动画跑完"的兜底**——如果你写的视觉动画比口播长,会被当场切。
> 解决办法:写更长口播 / 拆 step / 调动画速度(详见
> [`CHAPTER-CRAFT.md`](CHAPTER-CRAFT.md) 「代码层最小约束」)。
>
> 音频文件缺失(还没合成 / 404)或 narration 是空串 → 退化到字数估时
> (`max(1500ms, 字数 × 250ms)`),保证预览也能整片跑通。
---
## 故障排查
通用:
| 现象 | 原因 / 修法 |
|---|---|
| `chapter id "X" registered but no matching folder found` | 章节文件夹应命名为 `NN-<id>`;id 必须等于 chapters.ts 里注册的 |
| `narrations.ts in X must export an array named "narrations"` | 该章节的 narrations.ts 没 export 名为 narrations 的数组 |
| `TTS provider 'X' not found` | `scripts/tts-providers/X.sh` 不存在;列出来看哪些可用,或抄 README 加一个 |
| `provider 'X' does not define tts_synthesize` | 你的 `<X>.sh` 没定义必需的函数。看 README 的契约部分 |
| 中间断了几条没合成 | `npm run synthesize-audio` 重跑 —— 已存在文件会跳过 |
| 浏览器没播音频 | Auto / Audio 模式下首次需要用户手势——确认你按了 SPACE 启动 Auto,或者点过页面 |
| 音频 404 但 Auto 模式还能跑 | 找不到 mp3 时 useAudioPlayer 退化到字数估时(4 字/秒),保证预览不中断 |
minimax 专属:
| 现象 | 原因 / 修法 |
|---|---|
| `mmx: command not found` | `npm install -g mmx-cli`;npm 全局 bin 不在 PATH 时 `npm config get prefix` 看一下 |
| `mmx is not authenticated` | `mmx auth login --api-key sk-xxxxx` 重新登录 |
| 中文音色不自然 | mmx 默认音色未必最佳;查 `mmx speech --help` 看 `--voice` 可选项,传 `--voice=<id>` |
| 整段合成被截断 | 单段过长(mmx 默认上限约 5000 字符)。在 narrations.ts 里把这条拆成两条(也意味着该 step 应该拆成两个 step) |
openai 专属:
| 现象 | 原因 / 修法 |
|---|---|
| `OPENAI_API_KEY is not set` | `export OPENAI_API_KEY=sk-...`,或者把它加到 shell rc / `.env` |
| 全部段 FAILED + key 是对的 | 多半 model / voice 名字错。`--voice=alloy` 试默认值;`OPENAI_TTS_MODEL=tts-1` 试默认模型;用 `bash -x scripts/synthesize-audio.sh` 看请求体 |
| 走代理 / 走 Azure-OpenAI | `export OPENAI_BASE_URL=https://your-proxy/v1` |
| HD 太慢 | 改成 `OPENAI_TTS_MODEL=tts-1`(默认);HD 大约慢 2 倍 |
| 中文音色不像真人 | OpenAI 6 种音色都是英语偏向;中文角色用 `minimax` 更合适 |
换其它(自定义)provider 之后:
| 现象 | 原因 / 修法 |
|---|---|
| `<X>_API_KEY not set` | 你的 provider 需要 API key,但 env 里没设。`export <X>_API_KEY=...` 或写到 `.env` 再 `set -a; source .env; set +a` |
| 合成的 mp3 浏览器播不了 | 检查 provider 是否真的出了 mp3(不是 wav / opus / aac)。`file public/audio/*/*.mp3` 看 magic header |
| 一切看起来都对,但全部 FAILED | `bash -x scripts/synthesize-audio.sh` 看每段实际调了什么 |
---
## 相关链接
- Provider 契约 + 现成片段:[`scripts/tts-providers/README.md`](../templates/scripts/tts-providers/README.md)
- mmx-cli 仓库:<https://github.com/MiniMax-AI/cli>
- mmx 官方文档:<https://platform.minimaxi.com/docs/token-plan/minimax-cli>
- mmx 参数 / 音色查询:`mmx speech --help`
@@ -0,0 +1,224 @@
# 章节开发指引(每章开发必读)
---
## 这是视频,不是 PPT
正在做的是**视频网页** —— 讲者点击 + 口播 + 录屏发出去给观众看。
判断每一步做对没有,标准非常朴素:
- **不像 PPT** —— 观众感觉是在看视频,不是在看翻页幻灯(页面中不得包含页眉页脚,突出主视觉元素)
- **看起来舒服** —— 配色、字体、节奏都让人放松,不得出现大量的纯文字、不得出现字体太小的文字
- **有视觉冲击** —— 画面在演事情,不只是文字堆砌,不得一次性全部罗列所有元素,关键元素随进度逐步推进展现
---
## 必须用 CSS / SVG / Canvas / JS 大胆绘制视觉演示
> **这是底线。**
>
> 每一章都至少要有 1~2 处"动起来的图 / 演示元素"。
> **整章只有纯文字 = 验收不过 = 回去重做。**
视频感最强的来源 —— 用户**看见**了被讲解的东西在屏幕上演给他看:
- 数字在递增 / 横条在生长 / 排名在交换
- 流程节点依次点亮 / 连线自绘
- 对比被一刀切开 / 聚光灯扫过 / 形状在变形
- 粒子聚拢成形 / 噪声背景流动 / 字符雨下落
- 模拟终端交互
- 模拟 AI 对话窗口
- 模拟文件目录树
**怎么组合发挥都行 —— 但每章必须用,不允许整章纯文字。**
---
## 逐步揭示,禁止一次全展示
整页内容由**全局 `step` 计数器**驱动 —— 点击空白处或按 → 键推进
一步。设计每一步时心里要默念:**这一步演什么,下一步演什么**。
**最重要的一条**:
> 当口播在说"第一是 X、第二是 Y、第三是 Z"这种**清单 / 列表**时,
> **严禁**一个 step 把 X / Y / Z 全部 stagger 上来。
正确做法:
- 一项 = 一个 step
- X 只在它自己的 step 里独自亮起
- 讲到 Y 时,X 灰化保留作上下文 + Y 亮起
- 讲到 Z 时,X / Y 都灰化 + Z 亮起
**判断标准**:讲者会一个一个念出来吗?会 → 必须逐个揭示。
---
## 内容取舍:抓重点,不要原文搬运
视频是**音 + 画**:
- **口播**负责把信息线性讲清楚
- **画面**负责把节拍重点放大、节奏感拉出来
每个 step 屏幕上只挂这个节拍**最值得放大的 1~3 个东西** —— 一个
hero 标语 / 一个数字 / 一组对比 + 必要的视觉演示。
不要试图把原文每个字都搬上去。那是论文阅读,不是视频。
---
## 双源:节奏跟口播稿,细节回原文章
> **节奏 / 顺序 / 节拍切分** 跟 **`script.md` 口播稿** —— **关键顺序不能乱**。
> **画面细节 / 数据 / 引用 / 案例** 回 **`article.md` 原文章**抽。
`outline.md` 已经在每章首段抽了「信息池」做参考。但**实现章节时
也必须回去翻 `article.md` 本章对应段落** —— 那里有比口播稿多得多的
细节(具体数字、引用原话、案例维度、出处时间)。把这些挂到画面上,
让**画面信息密度 > 口播信息密度**。
> **如果你只用了口播稿的内容做章节** —— 屏幕等于把口播打字打了一遍
> —— 那就是 PPT,不是视频。
>
> 章节实现一定要回原始文章抽细节,不要嫌麻烦。
---
## 字体 / 配色 / 动画 / 留白 —— 视频演示基本审美
视频观众离屏幕远、注意力浮动,所以:
- **字号要大** —— hero 文字至少 80px 起,远观也能看清
- **留白要多** —— 舞台四边都要让出大留白,画面不要塞满
- **配色要舒服** —— **颜色和字体家族必须用主题 token**(保证换主题不破);
字号 / 间距 / 时长这些章节按内容自由发挥(详见下方「代码层最小约束」)
- **动画要舒服 + 炫酷** —— 出现得干净利落,停下来不抢戏;炫酷靠
**设计巧思**(内容驱动的演示动画),不靠**速度暴力**或**密集闪烁**
---
## 避免 AI 味
AI 生成的网页有几种共有的"视觉指纹",**全部不要**:
- 紫粉 / 蓝紫对角渐变背景
- 圆角卡片 + 彩色左边框装饰
- 渐变按钮 + 大圆角药丸
- emoji 当图标用
- 假数据 / 假 logo / 假"X 万用户"
- 整章 N 步用同一种入场动画(全场 fade / 全场 blur)
- 每步都挂 ken burns / 光晕呼吸 / 持续闪烁
- 每屏右下角都挂 mono 角标 / 序号
缺的东西**承认缺** —— 用 placeholder 占位卡(一张写着"image · 16:9
描述"的卡片,按真实比例留位)。**不要**用 emoji 凑、不要找无关图凑、
不要编数字。**没有就承认没有**,比 fake 强一百倍。
---
## 框架已经搭好的部分(理解就好,不需重写)
- **16:9 固定舞台**:内容设计在 1920×1080 上,外层 transform scale
缩到任何视口,外围 letterbox 留黑 —— **没有响应式断点**
- **舞台居中 + 大留白**:上下左右四边都让出至少 80px 的安全区
- **隐形进度条**:屏幕底部默认完全透明,鼠标悬到底部边缘才出现,
支持点击跳转章节(录屏时摄像头看不到任何 chrome 控件)
- **全局 step 驱动**:点击舞台空白处 / 键盘 ←/→ 推进;章节是 `step`
的纯函数,没有定时器、没有命令式状态
---
## 代码层最小约束
不能踩的红线,其它怎么写都行:
### 必须用 token(换主题不破的底线)
- **颜色**:`--shell` / `--surface` / `--surface-2` / `--surface-3` /
`--text` / `--text-2` / `--text-mute` / `--text-faint` / `--rule` /
`--accent` / `--accent-soft` / `--accent-glow` ——
**禁硬编码 hex / rgb / 颜色名**
- **字体家族**:`--font-display-cn` / `--font-display-en` / `--font-body`
/ `--font-mono` —— **禁硬编码字体名**
- **主题性格签名**通过 primitive class 自动接入,**不要在章节 CSS 里
重定义它们**:
- `.hero-num`(hero 数字风格 —— 主题决定衬线 / 等宽 / 粗黑)
- `.rule`(分割线 —— 主题决定 1px 实线 / 4px 实线 / 2px 虚线)
- `.card`(卡片 —— 主题决定圆角 + 阴影性格)
- `.stage-frame`(舞台底色 / 圆角 / 阴影 / 装饰图案 / vignette
全自动,章节什么都不用做)
### 可硬编码 / 可 token,按内容自由(解锁章节自由设计)
- **字号**:想要 80px 就写 80px,想用 `var(--t-h1)` 也行
- **间距 / padding / margin**:按画面节奏写具体值
- **动画时长 / 缓动 / keyframe**:按动画意图写具体值
(**节奏气质**参考 `theme.json` 的 `mood` —— 慢主题别写 200ms 的快动画)
- **边框宽度 / 非性格圆角 / 字距**:随手写
- **gap / grid 布局尺寸**:按画面构图写
### 其它工程红线
- 不用 `setTimeout` / `setInterval` 驱动动画 —— 用 CSS keyframes
- 章节内的可交互元素(按钮 / 自定义控件)加 `data-no-advance`,
否则点了会被舞台误推进 step
- 章节代码物理隔离:每章独立文件夹、独立 CSS 类前缀,不跨章 import
- **每章必须有 `narrations.ts`**(与 `<Chapter>.tsx` 同目录):
- 数组长度 **=** 章节代码里 `if (step === N)` 出现的最大 N + 1
- 每个元素 = 一个 string,该 step 要播的口播文本(来自 `script.md`
对应段,**语义一致**——可微调标点 / 断句以适配 TTS,但不能漏关键短语)
- 完全无音频的过场 step 用空串 `""`,Auto 模式会按字数估时撑过
- 这是**音频合成 + Auto 模式自动推进的唯一真相源**,写错或漏写
会让录屏对不上嘴
- **动画时长必须 ≤ 该 step 的口播时长**——Auto 模式严格按音频结束推进,
没有"等动画跑完"的兜底。动画太长 → 三选一:**写更长口播 / 拆 step
/ 调动画速度**。详细机制见 [`AUDIO.md`](AUDIO.md)
---
## 完工自检(写完每章**强制**执行,不可跳过)
> ⚠️ **硬性流程**:章节实现完成后**必须**走完下面的自检 → 修复 → 汇报
> 三步。**禁止**"实现完成 → 直接汇报给用户"。
>
> **执行方式**(按能力降级):
>
> 1. **优先 Agent Teams**:开一个独立的 reviewer agent,传入本章代码路径
> + 本文件 Part「完工自检」清单,让它**逐项核查 + 出结论**(哪几条
> pass / 哪几条 fail + 证据)。
> 2. **其次 subAgent**:当前 agent 没有 Teams 能力但能开 subagent,用 subagent
> 走同样的流程。
> 3. **都没有**:当前 agent 自己**严格逐项**核查,不允许目测一遍就放行。
>
> **拿到自检结论后**:先按 fail 项**改完代码**,然后再向用户汇报"做完
> 了 + 自检结论 + 改了什么"。**直接拿原始结论汇报但不修复 = 违规**。
写完一章 + 在浏览器点完一遍后逐项过:
- [ ] **每章至少 1~2 处 CSS / SVG / Canvas / JS 视觉演示** —— 没有 = 回去补
- [ ] **不同 step 的主导动作不一样** —— 全章一种动画 = 回去重做
- [ ] 字号大、留白舒服、配色舒服
- [ ] 清单 / 列表逐个揭示,**1 项 = 1 step**
- [ ] 画面信息比口播稿多(回了原文章抽细节挂上来)
- [ ] 没有紫粉渐变 / 圆角彩色边框 / emoji / 假数据 / 假 logo
- [ ] 缺的素材用 placeholder,不是 fake
- [ ] **颜色和字体家族全部走 token**(无硬编码 hex / 字体名);hero 数字
/ 卡片 / 分割线 / 舞台用 primitive class 接入主题性格 —— 这两条不
达标 = 换主题就破
- [ ] 章节交付时**主动告诉用户**:"本章还缺这些素材"
- [ ] 禁止出现小号字体,大量纯文字(出现后必须回去改)
- [ ] 禁止出现任何形式的页眉页脚,仅展示关键内容(出现后必须回去改)
- [ ] **`npx tsc --noEmit` 通过** —— 不通过禁止汇报"做完了"
- [ ] 章节代码物理隔离:独立 CSS 类前缀(`.cd-` / `.mg-` / ...),
未跨章 import,未修改 `chapters.ts` 之外的共享文件
- [ ] **`narrations.ts` 存在**且 `narrations.length` === 章节代码里
`if (step === N)` 用到的最大 N + 1(不一致 = Auto 模式录屏会错位)
- [ ] **每条 narration 文本与 `script.md` 对应段落语义一致**(关键短语 /
数字 / 引用全部保留,可为 TTS 微调标点断句)—— 录屏画外音应当能被
观众听成同一段稿子
- [ ] **每个 step 的视觉动画时长 ≤ 口播时长**(口播 `字数 ÷ 4` ≈ 秒数)——
超出会被 Auto 模式当场切断,动画演到一半就跳下一步
任一未过 → 回去改。**不要**"先放着以后修"。
@@ -0,0 +1,107 @@
# EXAMPLES —— 完整章节 / 题材 anchor
> ## ⚠️ 这是**结构示意**,不是抄袭模板
>
> 这些 example **不是给你照抄的**。它们的角色是"看一个完整章节大概
> 什么形状、动画怎么分层、CSS 用了哪些 token、outline 长什么样"。
>
> **正确使用流程**:
>
> 1. 走完 [`../CHAPTER-CRAFT.md`](../CHAPTER-CRAFT.md) Part 0 五问
> 2. 实在卡壳"我这一章的整体结构应该是什么"才翻 EXAMPLES
> 3. **保留它的"形"**(step 切分逻辑、字号关系、布局原则),**按本
> 项目的主题 + 内容换动作选型**
>
> 倒过来——先翻 EXAMPLES 选一个照搬到底 = [`../CHAPTER-CRAFT.md`](../CHAPTER-CRAFT.md)
> Part 5 第 8 条「整章只用一种入场动画」同质化反模式(每个用户的视频
> 看起来像同一个模板的 N 个变奏)。
两类参考资源,让 agent 在写章节时**有具体形状可参考**,不用从零设计。
> **不是必须按这个写**。卡壳时翻一翻;用力发挥时大胆偏离。
## 目录
### A. 章节结构 anchor(与题材无关)
| 例子 | 适用场景 | 文件 |
|---|---|---|
| [`hook-chapter/`](hook-chapter/) | **钩子型开场** —— 多张图片逐张揭示后 hero takeover | `chapter.tsx` + `chapter.css` |
| [`list-reveal/`](list-reveal/) | **列举型** —— 口播说"三件事 / N 个特性",每项 1 step | `chapter.tsx` + `chapter.css` |
每个 example 都是**完整章节**:**内容驱动主导动作** + 必要的伴随动作
(**不强求挂持续微动**,按 [`../CHAPTER-CRAFT.md`](../CHAPTER-CRAFT.md)
Part 0 原则 7 节制使用)、真素材(不是占位卡)、字号狠对比、绑了
`newsroom` 主题作为示范。
### B. 题材 case anchor(与题材相关)
| 例子 | 题材 | 文件 |
|---|---|---|
| [`case-tech-review/`](case-tech-review/) | 科技测评 / 实测对比 / 跑分类视频 | README + outline 节选 |
> 题材 case 展示**真实 outline 的样子**(含 article 补字段如何填、
> 章节切分如何决策)。拿到与某个 case 题材相似的需求时,先翻它再
> 写自己的 outline。
## 怎么用
### 写章节卡壳时
1. 看哪个 anchor 跟你这一章**结构最像**(钩子型 vs 列举型 vs 其它)
2. 翻 `README.md` 看这个例子的设计思路 + 节奏
3. 翻 `chapter.tsx` 看实现:JSX 结构、`step` 切分、用了哪些组件 / 类名
4. 翻 `chapter.css` 看动画用了哪些 keyframes、token、`infinite` 持续
微动写在哪
5. 写自己这一章时**保留 anchor 的"形",按本章内容 + 本主题气质换动画选型**
### 切换主题时
每个 example 的 README 末尾有"切到其它主题怎么换"的提示 —— 通常只需要
**换主导动作的形式**(newsroom 印章砸下 → terminal 打字机 → chalk
粉笔自绘),**结构、step 切分、字号关系不动**。
---
## ⚠️ 这两个 anchor 是"地板",不是"天花板"
这两个例子已经引入印章砸下、stagger、accent 红条 —— 但**仍然是相对克
制的版本**。**鼓励你做得更狂、更"视频感"**:
### 进阶玩法(任选搭配)
| 维度 | 这俩 anchor 给的(地板) | 可以升级到(无上限) |
|---|---|---|
| 背景层 | 纯色 surface | + SVG turbulence filter 纸纹永不停斜向漂移 |
| 主导动作 | mask reveal + 印章砸下 | + Canvas 粒子从屏幕外汇聚成 hero 字 |
| 伴随动作 | accent 红条 scaleX | + SVG path stroke-dashoffset 自绘下划线 / 装饰花纹 |
| 持续微动 | accent 光晕呼吸 | + 多层粒子漂移 / scanline / ken burns 缓推 |
| 数字 hero | 直接显示 | + JS 数字滚动(`requestAnimationFrame` + easeOutQuart) |
| 流程 / 架构 | 仅文字列 | + SVG path 自绘流程图(每条线 stroke-dashoffset 错峰) |
| 对比图 | 两段文字 | + SVG 双柱图自绘 + 差值数字滚动 |
| 转场 | 章节边界硬切 | + clip-path inset 横向擦除转场 |
→ 详细工具箱见 [`../CHAPTER-CRAFT.md`](../CHAPTER-CRAFT.md) Part 2
"视觉手段全栈工具箱"(CSS / SVG / Canvas / JS 四层)。
### 实测原则
写章节时,**先实现 anchor 同等的地板版本**(按 [`../CHAPTER-CRAFT.md`](../CHAPTER-CRAFT.md)
Part 0 五问选好主导动作),跑起来确认气质对,**再决定要不要加伴随
动作 / 持续微动**。
**判断标准**:
- 如果不同 step 的主导动作够多样(PPT 警报通过 [`../CHAPTER-CRAFT.md`](../CHAPTER-CRAFT.md)
Part 0 原则 7 自检)= 不需要再加持续微动
- 如果整章主导动作太单一 = 不要靠"加持续微动"补救,**回 [`../CHAPTER-CRAFT.md`](../CHAPTER-CRAFT.md)
Part 1 五问换主导动作**才是正解(参 Part 5 第 8 条「整章只用一种入场动画」)
## 不在 EXAMPLES 里出现的章节类型
- **数字型 hero**("+47%" → "几乎快了一倍")
- **对比型**(前后对照 / 双柱图)
- **链接卡片收尾**
这些场景的视觉原语已经在 [`../CHAPTER-CRAFT.md`](../CHAPTER-CRAFT.md)
Part 3 视觉工具箱(CSS / SVG / Canvas / JS 全栈)里覆盖了;按 anchor
的"形"组合即可。
@@ -0,0 +1,56 @@
# Case: 科技测评类(tech review)
一篇 AI / 工具 / 产品**实测对比**类文章 → 7 章 36 步、6 分 30 秒视频
的真实案例。
> ## ⚠️ 这是结构示意 / 历史案例,不是抄袭模板
>
> 这个目录的角色是让 agent 看:"**测评类视频章节怎么切、信息池怎么
> 抽、章长怎么定**"。**它包含的具体动画描述(如"慢速 blur clear
> 1.5s ease-out / 打字机每字 80~100ms")属于历史版本** —— 新版
> outline 已经**不写动画 / 不写时长**(见 [`../../OUTLINE-FORMAT.md`](../../OUTLINE-FORMAT.md))。
> 新写 outline 时只写"屏幕内容 + 关系名前缀 + 章节级信息池",动画
> 选型留给章节实现阶段按 [`../../CHAPTER-CRAFT.md`](../../CHAPTER-CRAFT.md)
> Part 0 五问决定。
>
> **看这个 case 学的应该是**:
> 1. 测评类怎么切 7 章(钩子 → 优点 → 场景 → 进阶 → 收束)
> 2. 章长怎么定(每章 4~6 step 防疲劳)
> 3. 双源原则怎么落地(hero 来自 script / 数据角标来自 article)
>
> **不应该学**:动画选型、CSS 实现、时长数值(这些已下放到 chapter
> 阶段)。
## 适用场景
- AI 模型 / 产品 / 工具的实测体验文
- 多家产品对比(A vs B vs C)
- 跑分 / benchmark / 用户投票数据驱动的内容
- "强在哪 / 怎么用 / 怎么用得好"型结构
## 关键决策
| 维度 | 这个案例的选择 | 通用启发 |
|---|---|---|
| 主题 | `midnight-press`(电影感慢镜、blur clear、暖橙 accent、scanline) | 科技测评类适合"克制、有重量"的暗色调;避开俏皮 / 糖果色 |
| 章节切分 | 7 章:开场悬念 / 强在哪 / 哪能用 / 怎么用好 / Skill 介绍 / Skill 模式 / 收尾 | 测评类的标准结构:钩子 → 优点 → 场景 → 进阶 → 收束 |
| 章长 | 每章 4~6 step | 测评类信息密度高,每章不超过 6 step 防止观众疲劳 |
| 双源应用 | hero 标语来自 script、画面密度(具体分数 / 投票数 / 时间戳)来自 article | 测评类 article 数据极多 —— 用 mono cue / 角标 / 数据浮层挂出来 |
| 动画风格 | 慢速 blur clear / 打字机 / ken burns 缓推 | midnight-press 暗色印刷气质,章节实现时按主题氛围自由发挥 |
## 文件
- [`outline-snippet.md`](outline-snippet.md) —— 前 2 章完整节选(5 + 5 step),
展示双源原则在 outline 里怎么落地
> 完整 7 章 outline 在调用此 Skill 的具体项目里(`gpt-image2-video/outline.md`),
> 不放进 Skill 仓库 —— 避免 Skill spec 被某一个项目内容污染。
## 不在这个 case 出现的情形
测评类**通常不需要**:
- 慢节奏长镜头(电影感片头 / 旅行 vlog 才需要)
- 手写温暖感(教育 / 亲子 / 食谱才需要)
- 大量插画(设计稿 / 工艺品类才需要)
→ 选别的 case anchor 或自由发挥。
@@ -0,0 +1,71 @@
# Outline 节选 · 科技测评类 case
> **节选**:前 2 章(10 step),用来展示 outline 在科技测评题材里的
> 形状。完整版 7 章 36 步在调用此 Skill 的具体项目里,不进 spec。
> **主题**:`midnight-press`(电影感慢镜、blur clear、暖橙 accent、
> scanline;克制有重量。**禁**砸下 shake / 弹簧 / emoji)
>
> **总时长**:约 6 分 30 秒
---
## 1. coldopen — 登顶悬念(5 steps · ~30s)
- **step 1** (~5s) — 暗场远景粒子云 + 钩子字幕"我刷到一张图,愣了三秒"
· 动画:屏幕从纯黑慢速 fade 到暗暖底(1.5s ease-out)→ 远景光尘粒子云慢漂浮入(1.0s 错峰)→ 字幕 mono 打字机逐字打出(每字 100ms);持续微动:粒子云永不停 brownian 漂移 + 暖橙暗角光晕 6s 周期慢呼吸 + ken burns 缓推
· 手段:CSS background 慢 fade + Canvas 粒子云慢漂 + JS typewriter + filter: drop-shadow 暖橙呼吸 + transform: scale 永动 ken burns
- **step 2** (~7s) — 排行榜慢镜景深聚焦 + 主分数 hero 数字 blur clear 浮出
· 动画:截图从 blur(12px) + scale(1.05) 慢速景深聚焦(1.5s ease-out)→ 主分数从 blur(20px) 慢慢锐化(1.2s 错峰 400ms)→ 王冠 SVG 沿数字外圈慢速 mask reveal(1.5s);持续微动:主分数暖橙光晕 5s 呼吸 + ken burns 缓推
· 手段:filter: blur 反向 + transform scale 慢推 + clip-path 沿 path mask reveal + filter: drop-shadow 呼吸
· article 补:主分数(来自 article §1,具体数字)+ 测评窗口("X 月 N 日 ~ Y 日")+ 投票数(mono cue 角标)—— 口播只说"换榜首",画面把"领先多少 / 多少票投出来的"全挂上
- **step 3** (~6s) — 第 2 名对比横条 + "+差距分"慢浮锐化
· 动画:第一条从左向右慢速 mask reveal 拉到 100%(1.2s ease-out)→ 第二条同向 mask reveal 但只到 ~70%(错峰 600ms)→ 中间空缺区差距分从 blur(15px) 慢慢锐化进场(错峰);持续微动:差距区暖橙光晕慢呼吸 + accent 横线 8s 缓延展永动 + scanline 极淡 overlay 慢移
· 手段:clip-path inset 慢 reveal + filter: blur 反向 + linear-gradient 暖光晕 + linear-gradient scanline 永动
· article 补:第 2 名具体名字(article §1)+ 差距分(具体数字 vs 模糊"低很多")+ 趋势注释("过去 N 周首次反超")
- **step 4** (~6s) — 官方原话 pull-quote 慢镜入场(电影感引文)
· 动画:左右两枚巨大引号 SVG 从 opacity 0 + blur(15px) 慢速锐化进场(1.0s 错峰 200ms,**无砸下**)→ 引文文字 mono 打字机逐字打出(每字 80ms)→ 落款慢速 blur clear 浮出(0.8s);持续微动:引号暖橙慢光晕呼吸 + 镜头 ken burns 缓推
· 手段:filter: blur 反向锐化 + JS typewriter + transform translateY 慢推 + filter: drop-shadow 呼吸
· article 补:原话直引(来自 article §1,1~2 句)+ 落款来源("— 出处.AI")—— 引文是 article 里口播完全省略的"权威背书"
- **step 5** (~5s) — 主持人介绍 + 4 件事预告速览
· 动画:第一行自我介绍 blur clear 慢入场(1.2s ease-out)→ 4 张占位卡分别从 blur(15px) 慢速景深聚焦 stagger 出现(每张 250ms 错峰,每张 1.0s 慢镜),卡内 mono 数字 01/02/03/04 + 关键词;持续微动:每张卡暖橙边线慢光晕呼吸(错峰 400ms)+ 远景粒子永漂
· 手段:filter: blur 反向 + opacity 慢 fade + transform scale 慢推 + filter: drop-shadow 多 instance 错峰呼吸
· article 补:4 件事的关键词(来自 article 章节标题,简化)
口播节选:
> 我刷到一张图,愣了三秒……今天讲清楚四件事。
---
## 2. why-strong — 强在哪(5 steps · ~80s)
- **step 1** (~6s) — hero"强在哪 · 四个方向" + 4 个 ghost 占位卡
· 动画:hero 字符整体从 blur(20px) + opacity 0 慢速景深聚焦(1.5s ease-out)→ 下方暖橙长横线从中心向两侧慢延展(0.8s)→ 4 张 ghost 卡片同步从 blur 慢镜出现(保持 opacity 0.3 占位状态);持续微动:暖橙下划线 8s 周期慢光晕脉冲 + ghost 卡片暖暗边线慢闪
· 手段:filter: blur 反向 + transform scaleX 慢延展 + opacity 阶梯填充 + filter: drop-shadow 永动呼吸
· article 补:4 个方向各自的关键词(mono cue 标签,"01 X / 02 Y / 03 Z / 04 W")
- **step 2** (~16s) — 第 1/4 项填实 + 大图慢镜 takeover
· 动画:卡片 1 从 ghost 状态慢速 mask 填实(0.8s 暖暗底色 + 边线慢光晕亮起)→ 中央 hero 大图从 blur(15px) 慢速景深聚焦(1.5s ease-out)→ mono cue 标签从暗角慢速 blur clear 入场(0.8s)→ 副标打字机逐字打出(每字 80ms);持续微动:暖橙 accent 高亮条永动呼吸 + 大图 ken burns 缓推(0.5% scale 12s 周期)+ scanline 慢移
· 手段:filter: blur 反向 + clip-path 慢 reveal + JS typewriter + transform scale 永动 ken burns + linear-gradient scanline 永动
· article 补:本项的具体表现(article §2 抽 1~2 个数据点 / 案例标签)—— 口播只说"它强在 X",画面挂"具体强到 N% / 跑赢 M / 测评分数 K"
- **step 3** (~16s) — 第 2/4 项填实 + 列表/演示
· 动画:卡片 2 慢速 mask 填实(0.8s)→ mono cue 标签 blur clear 慢入场(0.8s)→ 4 行具体细则 typewriter 逐行打出(每行 0.8s 错峰 350ms)→ 每行末尾 mono 光标闪烁后追加暖橙对勾 SVG path stroke 慢绘制;持续微动:mono 光标永闪烁(800ms blink)+ scanline 慢移
· 手段:JS typewriter + opacity blink 光标 + SVG path stroke-dashoffset 慢绘 + linear-gradient scanline overlay 永动
· article 补:4 行细则的具体内容(来自 article §2 第 N 段子列表)—— 口播只说"指令遵循好",画面把 article 列出来的 4 个具体维度"主体放哪 / 背景怎么搭 / ..."逐行打出来
- **step 4** (~16s) — 第 3/4 项填实 + before/after 慢镜对照 + 永动 cross-fade
- **step 5** (~16s) — 第 4/4 项填实 + 多参数预览 + redacted 注释
口播节选:
> 实测下来强在四个方向 ……
---
> **观察**:每个 step 的画面都做到"口播说一件事,画面挂多件事"。比如
> step 2 口播只是"第一项很强",画面同时呈现:本项关键词 / 大图实例 /
> 具体数据点 / 副标补充 —— 这是双源原则的具象落地。
@@ -0,0 +1,86 @@
# Anchor: hook-chapter(钩子型开场)
> ⚠️ **这是结构示意,不是抄袭模板**。先走 [`../../CHAPTER-CRAFT.md`](../../CHAPTER-CRAFT.md)
> Part 0 五问。本 anchor 给的是"钩子型开场的结构骨架"——你要保留它的
> step 切分逻辑、字号关系、布局原则,**按本项目的主题 + 内容换动作
> 选型**。倒过来照抄 = [`../../CHAPTER-CRAFT.md`](../../CHAPTER-CRAFT.md)
> Part 5 第 8 条「整章只用一种入场动画」同质化反模式。
## 定位
视频开头最常用的章节类型:**抛 N 张可疑图 / 反例 / 截图 → 引出主题 →
切大字 hero takeover**。
## 适用场景
- 悬念型开头:先甩 3~4 张让人怀疑 / 困惑的图,再揭示原因
- "今天聊聊 X 的几个翻车现场":先看翻车,再切主题
- 产品发布的"问题感"开场:先看痛点截图,再揭示新功能
## 假设的 outline.md 章节段(抽象)
```markdown
## 2. hook — <章节标题>(6 steps)
- **step 1** (~4s) — N 张可疑图片占位(虚线 ghost 卡片)
- **step 2** (~5s) — 第 1 张露出:<反例 1 描述>(独占视觉)
- **step 3** (~5s) — 第 2 张露出:<反例 2 描述>(独占视觉)
- **step 4** (~5s) — 第 3 张露出:<反例 3 描述>(独占视觉)
- **step 5** (~4s) — 三张图同时缩入侧栏,中间出 <主题大字> takeover
- **step 6** (~3s) — 切到下一句钩子(被 brush 划掉)
```
## 关键节奏决策
| step | 节奏意图 | 视觉 |
|---|---|---|
| 1 | 抛悬念 —— N 张未知 | 虚线 ghost 卡片,1/3 屏一张 |
| 2-N | **每张图独占视觉** —— 重点不是"凑数",是让观众盯着每张图想"这是真的吗" | 大图占据 ~70% 屏幕,旁边小字标注图源 |
| N+1 | takeover —— 揭示主题 | 三张缩成左侧迷你卡,中间巨字 |
| 末 | 钩子收束 | brush 划掉旧概念,引下一章 |
## 为什么 2-N 不能 stagger 同时上
口播会**逐个念出来** —— 必须 1 项 = 1 step([CHAPTER-CRAFT.md Part 0 原则 8](../../CHAPTER-CRAFT.md#8-多点内容必须逐个揭示绝不同时上))。
同时 stagger 上 = 观众扫一眼看完,讲者还在念第一张 = PPT 直觉。
## 文件结构
```
hook-chapter/
├── README.md ← 本文件
├── chapter.tsx ← 完整章节示例 —— 默认绑 newsroom 主题
└── chapter.css
```
## 关键手段(地板线)
| 维度 | 这个 anchor 怎么实现 |
|---|---|
| 素材 | `<img src="/hook/<asset>.png" />` 真截图 |
| 字号 | hero = 144px serif (`var(--t-display-1)`) |
| 主导动作 | brush-stroke + 印章砸下(newsroom 气质) |
| 伴随动作 | accent 红条 scaleX + 副标 stagger 200ms |
| 持续微动 | accent 红条光晕 `infinite` 呼吸;图片 ken burns 缓推 |
| 卡片样式 | drop-shadow + 微旋转 1deg |
| takeover | 三张图缩入 + hero 巨字爆出 + accent 红条贯穿 |
> **新写章节时**:抄结构和字号关系,按本章内容 + 本主题气质自由
> 设计动画形式。**持续微动按需挂**,不强求 —— 详见
> [`../../CHAPTER-CRAFT.md`](../../CHAPTER-CRAFT.md)「避免 AI 味」一节
> 关于「每步都挂 ken burns / 持续闪烁」的反模式。
## 切到其它主题时
- `bauhaus-bold` → brush 划掉换 hard-cut 大色块;hero 字体换 Archivo Black
- `terminal-green` → 三张图换"FILE_001/002/003"占位框;hero 用打字机
- `chalk-garden` → 粉笔感虚线 + 慢速 wiggle 入场
- `midnight-press` → blur clear 慢镜入场 + ken burns + scanline;
takeover 改"主标 blur 锐化 + 暖橙光晕呼吸"
**结构(N+2 步、独占节奏、takeover、收束)保持不变。**
## 想看具象题材应用
- 科技测评 / 实测对比类视频用这个 anchor 开场长什么样 →
[`../case-tech-review/`](../case-tech-review/)
@@ -0,0 +1,197 @@
/* ─────────────────────────────────────────────────────────────────
* hook-chapter · 完整章节示例样式
* 默认绑 newsroom 主题。所有视觉属性走语义 token,零硬编码。
* ───────────────────────────────────────────────────────────────── */
.hk-scene {
color: var(--text);
display: flex;
flex-direction: column;
justify-content: center;
}
/* ── kicker ── */
.hk-kicker {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-6);
font-family: var(--font-mono);
font-size: var(--t-cue);
color: var(--accent);
letter-spacing: 0.12em;
text-transform: uppercase;
}
.hk-kicker-line {
width: 64px;
height: 2px;
background: var(--accent);
}
/* ── step 1 三 ghost ── */
.hk-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-5);
align-items: center;
}
.hk-ghost {
aspect-ratio: 16 / 9;
border: var(--rule-w) dashed var(--rule);
border-radius: var(--r-card);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
padding: var(--space-4);
background: var(--surface);
}
.hk-ghost-num {
font-family: var(--font-mono);
font-size: var(--t-h2);
color: var(--text-mute);
letter-spacing: 0.08em;
}
.hk-ghost-label {
font-family: var(--font-mono);
font-size: var(--t-cue);
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.2em;
align-self: end;
}
/* ── step 2-4 单图独占 ── */
.hk-solo-frame {
width: 78%;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.hk-solo-img-wrap {
position: relative;
aspect-ratio: 16 / 9;
border-radius: var(--r-card);
overflow: hidden;
box-shadow: var(--shadow-card);
background: var(--surface-2);
}
.hk-solo-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.hk-stamp {
position: absolute;
top: var(--space-4);
right: var(--space-4);
padding: var(--space-2) var(--space-3);
border: 3px solid var(--accent);
color: var(--accent);
font-family: var(--font-display-en);
font-size: var(--t-h3);
font-weight: 900;
letter-spacing: 0.1em;
transform: rotate(-8deg);
background: color-mix(in oklch, var(--surface) 60%, transparent);
animation: hk-stamp-drop var(--dur-base) var(--ease-quart) backwards;
animation-delay: 600ms;
}
@keyframes hk-stamp-drop {
0% { transform: rotate(-8deg) scale(2.4); opacity: 0; }
60% { transform: rotate(-8deg) scale(0.92); opacity: 1; }
100% { transform: rotate(-8deg) scale(1); }
}
.hk-solo-meta {
display: flex;
justify-content: space-between;
align-items: baseline;
font-family: var(--font-mono);
font-size: var(--t-cue);
color: var(--text-2);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hk-solo-label { color: var(--accent); }
.hk-solo-caption {
font-family: var(--font-display-cn);
font-size: var(--t-body);
text-transform: none;
letter-spacing: 0;
color: var(--text);
}
/* ── step 5 takeover ── */
.hk-takeover {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-6);
justify-content: center;
}
.hk-mini-row {
display: flex;
gap: var(--space-3);
}
.hk-mini {
width: 140px;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: calc(var(--r-card) * 0.5);
box-shadow: var(--shadow-card);
animation: hk-mini-shrink var(--dur-base) var(--ease-quart) backwards;
}
@keyframes hk-mini-shrink {
from { transform: scale(2.4); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.hk-accent-bar {
width: 60%;
height: 4px;
background: var(--accent);
animation: hk-bar-grow 700ms var(--ease-quart) 250ms backwards;
}
@keyframes hk-bar-grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.hk-hero {
margin: 0;
font-family: var(--font-display-en);
font-size: var(--t-display-1);
letter-spacing: -0.025em;
color: var(--text);
line-height: 0.95;
}
/* ── step 6 close + brush ── */
.hk-close {
display: grid;
place-items: center;
}
.hk-quote-wrap {
position: relative;
display: inline-block;
}
.hk-quote {
margin: 0;
font-family: var(--font-display-cn);
font-size: var(--t-display-2);
color: var(--text);
}
.hk-brush {
position: absolute;
left: -4%;
right: -4%;
top: 50%;
height: 0.18em;
background: var(--accent);
transform-origin: left center;
transform: scaleX(0) translateY(-50%);
animation: hk-brush-strike 700ms var(--ease-expo) 500ms forwards;
}
@keyframes hk-brush-strike {
to { transform: scaleX(1) translateY(-50%); }
}
@@ -0,0 +1,125 @@
// ⚠️ 这是 anchor 参考代码,不会被任何项目编译。
// 抄到真实项目时(presentation/src/chapters/NN-hook/),
// 把下面两个 import 改成:
// import { MaskReveal } from "../../components/MaskReveal";
// import type { ChapterStepProps } from "../../registry/types";
import { MaskReveal } from "../../../templates/src/components/MaskReveal";
import type { ChapterStepProps } from "../../../templates/src/registry/types";
import "./chapter.css";
/**
* hook-chapter · 完整章节示例
* ─────────────────────────────────────────
* 默认绑 newsroom 主题(serif + 报头红 + 印刷盖章 motion)。
*
* 关键手段:
* - 真素材:<img src="/hook/{name}.png" /> 而不是 placeholder
* - 字号狠对比:hero 用 --t-display-1(≥ 144px)+ 微微负字距
* - 主导动作:mask reveal + 印章砸下(贴 newsroom 印刷气质)
* - takeover:三张图缩入 + 巨字爆出 + accent 红条贯穿
* - 收束:brush 划掉旧概念
*
* 切其它主题时按那个主题的气质自由换"印章砸下 / brush"等效动作,
* 结构和字号节奏保持。
*/
export default function HookChapter({ step }: ChapterStepProps) {
// step 1 — 三张 ghost(精修:加 kicker 引子 + accent 红条)
if (step === 0) {
return (
<div className="hk-scene scene-pad">
<div className="hk-kicker">
<span className="hk-kicker-line" />
<span className="hk-kicker-text">这几天</span>
</div>
<div className="hk-grid" key={step}>
{["01", "02", "03"].map((i, idx) => (
<MaskReveal show key={i} delay={idx * 200} duration={900}>
<div className="hk-ghost">
<span className="hk-ghost-num">{i}</span>
<span className="hk-ghost-label">image</span>
</div>
</MaskReveal>
))}
</div>
</div>
);
}
// step 2-4 — 每张图独占(真素材 + 角章 + 旁白)
// ⚠️ 这是结构示例。具体反例 caption / src 应该来自 outline.md 本章
// article 补字段(双源原则)—— 别照抄下面这些占位字符串。
const reveals: Array<{ src: string; label: string; caption: string }> = [
{
src: "/hook/<asset-1>.png",
label: "01 / 03",
caption: "<反例 1 caption,来自 article §X>",
},
{
src: "/hook/<asset-2>.png",
label: "02 / 03",
caption: "<反例 2 caption>",
},
{
src: "/hook/<asset-3>.png",
label: "03 / 03",
caption: "<反例 3 caption>",
},
];
if (step >= 1 && step <= 3) {
const r = reveals[step - 1];
return (
<div className="hk-scene scene-pad" key={step}>
<div className="hk-solo-frame">
<MaskReveal show duration={1100}>
<div className="hk-solo-img-wrap">
<img className="hk-solo-img" src={r.src} alt={r.caption} />
<div className="hk-stamp">FAKE?</div>
</div>
</MaskReveal>
<MaskReveal show delay={400} duration={900}>
<div className="hk-solo-meta">
<span className="hk-solo-label">{r.label}</span>
<span className="hk-solo-caption">{r.caption}</span>
</div>
</MaskReveal>
</div>
</div>
);
}
// step 5 — takeover:三张缩入 + 巨字爆出 + accent 红条
if (step === 4) {
return (
<div className="hk-scene scene-pad hk-takeover" key={step}>
<div className="hk-mini-row">
{reveals.map((r, idx) => (
<img
key={r.src}
className="hk-mini"
src={r.src}
alt={r.caption}
style={{ animationDelay: `${idx * 80}ms` }}
/>
))}
</div>
<span className="hk-accent-bar" />
<h1 className="hk-hero">
<MaskReveal show duration={1100}>
{/* hero 文案来自 outline 本章 step 5;这里只是占位 */}
&lt;主题大字 takeover&gt;
</MaskReveal>
</h1>
</div>
);
}
// step 6 — 钩子收束:brush 划掉
return (
<div className="hk-scene scene-pad hk-close" key={step}>
<div className="hk-quote-wrap">
<h2 className="hk-quote">&lt;下一句钩子&gt;</h2>
<span className="hk-brush" aria-hidden />
</div>
</div>
);
}
@@ -0,0 +1,91 @@
# Anchor: list-reveal(列举型逐个揭示)
> ⚠️ **这是结构示意,不是抄袭模板**。先走 [`../../CHAPTER-CRAFT.md`](../../CHAPTER-CRAFT.md)
> Part 0 五问。本 anchor 给的是"列举型章节的结构骨架"(单网格 N 槽位 +
> 每 step 只填一个槽位 + 位置不重排)——保留这个**结构**,**按本项目的
> 主题 + 内容换动作选型**。倒过来照抄 = [`../../CHAPTER-CRAFT.md`](../../CHAPTER-CRAFT.md)
> Part 5 第 8 条「整章只用一种入场动画」同质化反模式。
## 定位
口播说"三件事 / 四个原因 / N 个特性"时,**每项 1 step 逐个揭示**。
视频中段最常用的章节类型,**最容易翻车成 PPT** —— 这是为什么需要 anchor。
## 适用场景
- "<主体> 强在哪 → 三件事"
- "选购 <X> → 四个角度"
- "为什么我喜欢 <X> → 五个理由"
- 任何"主题 + N 个并列子项"的结构
## 假设的 outline.md 章节段(抽象)
```markdown
## 4. <chapter-id> — <主题 N 件事>(N+1 steps)
- **step 1** (~3s) — masthead 引子"<N 件事>"
- **step 2** (~6s) — 第 1 件:<标题> + <article 抽来的细节>
- **step 3** (~6s) — 第 2 件:<标题>
- ...
- **step N+1** (~6s) — 第 N 件:<标题>
```
## 关键节奏决策
| step | 视觉布局 |
|---|---|
| 1 | 中心引子大字 + 序号 01/02/.../N 占位(**不显示内容**,纯占位) |
| 2 | "01" 槽位填充:标题 + 简短说明 + accent 编号;其余仍是 ghost |
| 3 | "02" 填充;01 已激活变次级;其余仍 ghost |
| ... | 当前槽位填充;之前的激活降级;之后的 ghost |
## CHAPTER-CRAFT.md Part 0 原则 8 的核心实现
> "布局不重排,只是单元格内容变化"
整个章节只有**一个网格布局**,N 个槽位的 React 节点位置完全不变。
变的只是每个槽位的内容状态(ghost / active / past)。这样:
- 单元格不会重排 → 视觉稳
- 每点一次只有"一个槽位变化" → 观众视线明确锁定新揭示的项
**反模式**:每点一次重新渲染整个布局 → 已揭示的项也跟着抖动 / 重新
入场 → 观众不知道该看哪。
## 文件结构
```
list-reveal/
├── README.md
├── chapter.tsx ← 完整章节示例 —— 默认绑 newsroom 主题
└── chapter.css
```
## 关键手段(地板线)
| 维度 | 这个 anchor 怎么实现 |
|---|---|
| 字号 | 标题 64px / 巨号 144px serif |
| 槽位状态 | dashed → 巨号红色高亮 → 灰化数字(**位置不重排**) |
| 序号 | hero-num 字体(衬线大数字) |
| 主导动作 | mask reveal(标题)+ 数字砸下(accent 红) |
| 伴随动作 | 副标 stagger 200ms + accent 横线 scaleX |
| 持续微动 | active 槽位的数字 accent 光晕 `infinite` 呼吸 |
| 引子 | masthead 双线规则 + serif 大字 |
> **新写章节时**:抄结构(单网格 N 槽位、每 step 只填一个槽位、位置
> 不重排),按本章内容 + 本主题气质自由设计主导动作的形式。
## 切到其它主题时
- `bauhaus-bold` → 序号换 Archivo Black + 大色块;用 hard-cut 砸下
- `terminal-green` → 序号 `[01]` `[02]` `[03]` 风格;打字机入场
- `chalk-garden` → 粉笔下划线手绘 + wiggle 入场
- `midnight-press` → 数字 blur clear 慢锐化 + 暖橙光晕慢呼吸
**结构不变**:N+1 step、单网格 N 槽位、每 step 只填一个槽位。
## 想看具象题材应用
- 科技测评 / 实测对比类视频用这个 anchor 长什么样 →
[`../case-tech-review/outline-snippet.md`](../case-tech-review/outline-snippet.md)
里 `## 2. why-strong` 章节
@@ -0,0 +1,137 @@
/* ─────────────────────────────────────────────────────────────────
* list-reveal · 完整章节示例样式
* 默认绑 newsroom 主题。零硬编码视觉属性。
* ───────────────────────────────────────────────────────────────── */
.lr-scene {
color: var(--text);
display: flex;
flex-direction: column;
gap: var(--space-6);
}
/* ── masthead ── */
.lr-masthead {
display: flex;
align-items: center;
gap: var(--space-3);
}
.lr-rule {
flex: 1;
height: 1px;
background: var(--rule);
}
.lr-kicker {
font-family: var(--font-mono);
font-size: var(--t-cue);
letter-spacing: 0.18em;
color: var(--accent);
text-transform: uppercase;
white-space: nowrap;
}
/* ── intro step ── */
.lr-intro {
justify-content: center;
align-items: stretch;
gap: var(--space-5);
}
.lr-intro-h {
margin: 0;
font-family: var(--font-display-cn);
font-size: var(--t-display-2);
text-align: center;
letter-spacing: -0.01em;
}
.lr-em { color: var(--accent); }
.lr-intro-sub {
font-family: var(--font-body);
font-size: var(--t-h3);
color: var(--text-2);
text-align: center;
margin-bottom: var(--space-5);
}
/* ── grid (3 slots, fixed layout) ── */
.lr-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-5);
}
/* ── slot base ── */
.lr-slot {
border-radius: var(--r-card);
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-3);
min-height: 360px;
transition: opacity var(--dur-base) var(--ease-quart),
filter var(--dur-base) var(--ease-quart),
border-color var(--dur-base) var(--ease-quart);
}
/* ghost: 虚线边 + 大灰序号 */
.lr-slot-ghost {
border: var(--rule-w) dashed var(--rule);
background: transparent;
opacity: 0.55;
}
.lr-slot-ghost .lr-slot-num {
color: var(--text-faint);
}
/* active: 红框 + 实心面板 + 巨号砸下 */
.lr-slot-active {
border: var(--rule-w) solid var(--accent);
background: var(--surface);
box-shadow: var(--shadow-card);
}
.lr-slot-active .lr-slot-num {
color: var(--accent);
animation: lr-num-drop var(--dur-base) var(--ease-quart) backwards;
}
@keyframes lr-num-drop {
0% { transform: translateY(-40%) scale(1.6); opacity: 0; }
60% { transform: translateY(0) scale(0.94); opacity: 1; }
100% { transform: translateY(0) scale(1); }
}
/* past: 灰化 */
.lr-slot-past {
border: var(--rule-w) solid var(--rule);
background: transparent;
opacity: 0.7;
filter: grayscale(0.4);
}
.lr-slot-past .lr-slot-num {
color: var(--text-mute);
}
/* ── slot internals ── */
.lr-slot-num {
font-family: var(--hero-num-font, var(--font-display-en));
font-style: var(--hero-num-style, normal);
font-weight: var(--hero-num-weight, 700);
font-size: calc(var(--t-display-2) * 0.85);
letter-spacing: var(--hero-num-track, -0.02em);
line-height: 1;
}
.lr-slot-content {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.lr-slot-title {
font-family: var(--font-display-cn);
font-size: var(--t-h2);
letter-spacing: -0.005em;
}
.lr-slot-body {
font-family: var(--font-body);
font-size: var(--t-body);
color: var(--text-2);
line-height: 1.6;
max-width: 22ch;
}
@@ -0,0 +1,103 @@
// ⚠️ 这是 anchor 参考代码,不会被任何项目编译。
// 抄到真实项目时(presentation/src/chapters/NN-list/),
// 把下面两个 import 改成:
// import { MaskReveal } from "../../components/MaskReveal";
// import type { ChapterStepProps } from "../../registry/types";
import { MaskReveal } from "../../../templates/src/components/MaskReveal";
import type { ChapterStepProps } from "../../../templates/src/registry/types";
import "./chapter.css";
/**
* list-reveal · 完整章节示例
* ─────────────────────────────────────────
* 默认绑 newsroom 主题。
*
* 关键手段:
* - 槽位用 hero-num(serif 巨号)替代普通文字编号
* - 引子用 masthead 双线规则 + serif 大字
* - 槽位状态切换有专属动画:
* ghost → active:mask reveal 标题 + 数字砸下(accent 红)
* active → past :accent 灰化(filter)
* - 关键:所有槽位的 React 节点位置不重排,只切换 className
*/
const ITEMS = [
{ num: "01", title: "文字渲染", body: "图里的文字也能正确写出来" },
{ num: "02", title: "指令遵循", body: "可以给到非常具体的要求" },
{ num: "03", title: "照片真实感", body: "光影 / 材质 / 人物接近真实" },
];
export default function ListRevealChapter({ step }: ChapterStepProps) {
// step 1 — 引子
if (step === 0) {
return (
<div className="lr-scene scene-pad lr-intro">
<header className="lr-masthead">
<span className="lr-rule" />
<span className="lr-kicker">第一部分</span>
<span className="lr-rule" />
</header>
<MaskReveal show duration={1100}>
<h1 className="lr-intro-h">
强在<span className="lr-em">哪</span>
</h1>
</MaskReveal>
<MaskReveal show delay={400} duration={900}>
<div className="lr-intro-sub">三件事 —— 一个个看</div>
</MaskReveal>
<div className="lr-grid">
{ITEMS.map((it) => (
<Slot key={it.num} state="ghost" item={it} />
))}
</div>
</div>
);
}
const activeIdx = step - 1;
return (
<div className="lr-scene scene-pad">
<header className="lr-masthead">
<span className="lr-rule" />
<span className="lr-kicker">第一部分 · 强在哪</span>
<span className="lr-rule" />
</header>
<div className="lr-grid">
{ITEMS.map((it, i) => {
const state =
i < activeIdx ? "past" : i === activeIdx ? "active" : "ghost";
return <Slot key={it.num} state={state} item={it} />;
})}
</div>
</div>
);
}
function Slot({
state,
item,
}: {
state: "ghost" | "active" | "past";
item: { num: string; title: string; body: string };
}) {
return (
<div className={`lr-slot lr-slot-${state}`}>
<div className="lr-slot-num">{item.num}</div>
<div className="lr-slot-content">
{state !== "ghost" && (
<>
<MaskReveal show duration={900} key={`${item.num}-title`}>
<div className="lr-slot-title">{item.title}</div>
</MaskReveal>
{state === "active" && (
<MaskReveal show delay={350} duration={900}>
<div className="lr-slot-body">{item.body}</div>
</MaskReveal>
)}
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,203 @@
# `outline.md` 格式 spec
视频章节规划的产出文件。**用户可以直接编辑**,所以格式必须人类友好
(用 markdown 不用 JSON / YAML)。
!重要:阅读此文件后必须继续阅读 [`CHAPTER-CRAFT.md`](CHAPTER-CRAFT.md) 的全部内容,了解对网页效果的真实需求,然后再开始编写 outline
> ## ⚠️ outline 是开发计划,不是视觉规划
>
> outline 只规划**节奏 + 内容 + 信息密度**:
>
> - 章节切分 / 每章 step 数 / 每步估时
> - 每步屏幕内容(hero / 标语 / 数据 / 列表项)
> - 章节级**信息池**(从 article 抽的数字 / 引用 / 案例 / 标签)
>
> **outline 里的 step 数是初始预估**。最终 step 数以章节实现时的
> `narrations.ts` 为准——后者既是 step 数源,也是音频合成源
> (详见 [`CHAPTER-CRAFT.md`](CHAPTER-CRAFT.md) 「代码层最小约束」+
> [`AUDIO.md`](AUDIO.md))。如果实现时章节 step 数和 outline 不一致,
> 回过来同步 outline 即可,不需要纠结"对得严丝合缝"。
> **写 outline 前必读**(双源原则,[CHAPTER-CRAFT.md Part 0 原则 10](CHAPTER-CRAFT.md#10-双源原则scriptmd-定节拍--articlemd-定画面密度)):
>
> - **`script.md`** —— 决定**节拍**:按 `---` 切节拍,每节拍 1~2 step、估时
> - **`article.md`**(如有)—— 决定**画面信息密度**:每章首段抽**信息池**
---
## 抽象示例(看格式)
````markdown
# Video Outline
> **主题**:`<theme-id>`(Checkpoint Plan 已选定)—— <一句话风格描述>
> **总时长**:约 <T> 分 <S> 秒(口播 ~<X> 字 ÷ 4 字/秒)
> **章节数**:<N> 章 / <M> 步
---
## 1. <chapter-id> — <章节标题>(<S> steps · ~<T>s)
**信息池**(chapter agent 按需挂角标 / 副标 / pull-quote / mono cue):
- <类型:数字 / 引用 / 出处 / 案例 / 词义 / 时间 / 对比 / ...>:<内容> —— <来源 article §X / Lxx>
- ...
**开发计划**:
- step 1 (~Ts) — <屏幕内容>
- ...
口播节选:
> <1~3 句节选,对应到 script.md 完整文本>
---
## 2. <chapter-id> — ...
````
> **关于时长**:outline 里**只**写 step 的 `(~Ts)` 口播估时(音画对齐
> 用),**绝对不写**动画时长 / 错峰量 / keyframe 数值。这些都在章节开发
> 阶段决定([`CHAPTER-CRAFT.md`](CHAPTER-CRAFT.md) Part 3 时长参考)。
> **想看具象示例**:
> - 钩子型开场结构 → [`EXAMPLES/hook-chapter/`](EXAMPLES/hook-chapter/)
> - 列举型章节结构 → [`EXAMPLES/list-reveal/`](EXAMPLES/list-reveal/)
> - 科技测评类(实测 / 对比 / 跑分) → [`EXAMPLES/case-tech-review/`](EXAMPLES/case-tech-review/)
---
## 字段约定
### 顶部 metadata block
用引用块(`>`)形式,方便扫一眼整体规模:
| 字段 | 必填 | 说明 |
|---|---|---|
| **主题** | ✓ | Checkpoint Plan 必须已选定。chapter agent 实现时按主题颜色 / 字体 token 走,动画 / 节奏 / 视觉演示由章节自由发挥 |
| **总时长** | ✓ | 估算口播时长(中文 ~ 250 字 / 分钟) |
| **章节数** | ✓ | `N 章 / M 步` |
### 章节标题:`## N. <id> — <title>(<S> steps · ~<T>s)`
| 部分 | 规则 |
|---|---|
| `N` | 1-indexed 顺序,对齐 `chapters.ts` 的注册顺序 |
| `<id>` | **小写 + 连字符**。会成为 React `key` / 文件夹名 (`src/chapters/0N-<id>/`) / 音频子目录 (`public/audio/<id>/`) |
| `<title>` | 给人看的中文标题。**不会**进 React 代码 |
| `<S> steps` | 该章 step 总数 |
| `~<T>s` | 该章口播总估时(中文 ~ 4 字/秒) |
合法 id:`coldopen`、`hook`、`why-good`、`why-good-text-render`。
不合法:`why_good`(用连字符)、`Hook`(小写)、`第一章`(拉丁字符)。
### 章节首段「信息池」(**双源原则核心落地**)
每章独立列出从 `article.md` 抽的细节集合,**让 chapter agent 实现每步
画面时按需取用**——可能挂成右下角 mono 角标 / 副标小字 /
pull-quote 引用 / 数据浮层。
#### 信息池条目格式
```
- <类型>:<具体内容> —— <来源 article §X / Lxx 或简注>
```
> **没 article(用户直接给 script)**:信息池退化为"主动设计画面信息
> 密度"——靠数字 / 对比 / 元数据等让画面比口播信息密。可以列"画面
> 装饰元素池"而非"article 抽取池"。
### Step 列表:每步 **1 行**
```
- step N (~Ts) — <屏幕内容>
```
| 规则 | 原因 |
|---|---|
| `step N` 1-indexed | agent 实现时 `if (step === N - 1) ...`(注意零基偏移) |
| **`(~Ts)`** 必填 | 按 script.md 本步对应口播段字数 ÷ 4 估算(中文 ~ 4 字/秒)。范围 3~10s |
| **屏幕内容** | 一句话讲清楚这一步舞台上有什么:hero / 标语 / 数据 / 装饰元素。**≤ 1 行**,再多就该拆 step |
| **不写动画** | 写死 = 翻译机化(详见本文件顶部框) |
| **不写时长数值 / 错峰量** | 这些在章节开发阶段决定 |
| **不写实现手段** | filter / SVG / Canvas 选型留给 chapter agent |
### 口播节选(每章末尾,可选但推荐)
精炼 1~3 句,**不是完整稿子**,仅供章节规划阶段对照"这章在讲什么"。
完整文本回 `script.md`。`outline.md` 章节 = `script.md` 中两个明显
主题切换之间的段落。
> 音频合成([`AUDIO.md`](AUDIO.md))会**回到 `script.md`** 切分完整
> 文本,**不**用 outline 节选。
---
## 命名规则速查
| 对象 | 规则 | 示例 |
|---|---|---|
| 章节 id | 小写 + 连字符 | `coldopen`, `why-good` |
| 章节文件夹 | `0N-<id>` | `src/chapters/01-coldopen/` |
| 章节组件 | PascalCase | `Coldopen.tsx`, `WhyGood.tsx` |
| 章节 CSS 类前缀 | 章节缩写(避免跨章冲突) | `.cd-` / `.wg-` / `.mg-` |
| 音频子目录 | `<id>/` | `public/audio/coldopen/` |
| 音频文件 | `<step-N>.mp3` (1-indexed) | `public/audio/coldopen/1.mp3` |
---
## 章节切分的经验法则
- **每章 3~8 步**。少于 3 步太薄;多于 8 步观众会忘记这章在讲啥
- **总时长 ÷ 30 秒** ≈ 章节数(一章约 30~60 秒讲完)
- **每章 = 一个聚焦主题**。"为什么强 + 怎么用" 是两章,不是一章
- **章节边界 = 口播稿里讲者会换语气 / 换主题的位置**。读 `script.md`
时哪里你下意识想"咳一声接下一段",那里就是章节边界
- **慢节奏 / 长镜头风主题**(midnight-press / 电影感片头)每章可少到
2~3 step;**信息密集型**(科技测评 / 对比表)每章可放宽到 8~10 step
---
## 素材清单(outline.md 末尾)
```markdown
## 素材清单
### 1. coldopen
- ✓ <资源 1 描述> (<已就位路径>)
- ⚠️ <资源 2 描述>(待提供)
- ⚠️ <资源 3 描述>(待提供)
---
## 自检(写完 outline **强制**执行,不可跳过)
> ⚠️ **硬性流程**:outline 写完后**必须**走自检 → 修改 → 提交 三步。
> **禁止**写完直接进入 Checkpoint Plan 让用户对齐。
>
> **执行方式**(按能力降级):
>
> 1. **优先 Agent Teams**:开一个独立 reviewer agent,传入 `outline.md`
> + 本节自检清单 + `script.md` / `article.md` 路径,让它**逐项核查 +
> 出结论**(哪几条 fail + 证据)。
> 2. **其次 subAgent**:当前 agent 没 Teams 但能开 subagent,用 subagent
> 走同样流程。
> 3. **都没有**:自己**严格逐项**核查。
>
> 拿到结论后**先按 fail 项改 outline,再进入 Checkpoint Plan**。
- [ ] 每个 step 都是**单一句屏幕内容描述**,没有"动画"行 / "手段"行
- [ ] 没有任何 step 写了具体毫秒 / 秒数(除 `(~Ts)` 口播估时)
- [ ] 每章首段都有「信息池」block,至少 3 条 article 抽取项,**每条
必带来源标注**(`—— 来源 article §X / Lxx`)—— 没标注 chapter agent
回不到原文
- [ ] **所有 step `(~Ts)` 累加 ≈ 顶部声明的总时长**(误差 < 10%)—— 不
一致说明节奏规划失真
- [ ] 章节切分符合"每章 3~8 步 / 30~60s 一聚焦主题"经验
- [ ] 末尾「素材清单」分章节列出,✓ / ⚠️ 标注清楚
- [ ] 脚本不得包含标题、序号等非口播内容,仅包含人类正常可读的内容
写完看一眼:**outline 是不是干净到 chapter agent 看了能立刻开工 + 还有
设计空间**?是 = 合格。如果你看了都觉得"太空,agent 不知道动画选什么"
@@ -0,0 +1,77 @@
# 录制与后期合成
网页做完 + 音频合成完之后,**Auto 模式 + 屏幕录制可以一镜到底**——
不需要手动点击推进 step、也不需要后期对音频。
如果你不打音频,仍可走"手动点击 + 后期配"的传统路径(在文档末尾)。
---
## 推荐流程:Auto 模式一镜到底
### 前置
- 章节代码做完,每章都有 `narrations.ts`
- 已经跑过 `npm run extract-narrations` + `npm run synthesize-audio`,
`public/audio/<id>/<step>.mp3` 全部就位
- `npm run dev` 跑着,浏览器能打开页面
### 录制步骤
1. **浏览器全屏**(F11 / Ctrl+Cmd+F),URL 改成
`http://localhost:5173/?auto=1`
2. 看到 "Press SPACE to start" 蒙层 = Auto 模式就绪
3. **打开屏幕录制**(QuickTime / OBS / Cmd+Shift+5),开始录
4. **按一次 Space** → 蒙层消失 → step 0 出现,1.mp3 自动播 →
播完自动推进到 step 1 → 2.mp3 → … → 最后一个 step 播完 → 停在终态
5. **停止录制** → 后期裁掉头尾(Space 那一下、最后停在终态的尾巴)就是
成品
整个过程**完全不用点鼠标**。音视频天然同步,不需要后期对轨。
> **Auto 模式严格按音频结束推进**(+ 200ms 缓冲),没有"等动画跑完"
> 的兜底。如果你看到某步动画被切了一半 → 说明该 step 动画长于口播,
> 回章节代码改:写更长口播 / 拆 step / 调动画速度。
### 录屏工具
| 平台 | 工具 | 设置 |
|---|---|---|
| macOS | Cmd+Shift+5 → 录制选定窗口 | 选浏览器窗口;浏览器全屏后输出就是 1920×1080 |
| macOS | QuickTime → 文件 → 新建屏幕录制 | 同上 |
| 跨平台 | OBS Studio | 窗口捕获,Canvas 1920×1080,60fps |
### 模式速查
| URL / 快捷键 | 行为 |
|---|---|
| 直接打开(默认) | Manual:点击 / ←→ 推进,不播音频 |
| `?audio=1` 或按 `M` | Audio:进入 step 自动播音频,但**手动点鼠标推进** |
| `?audio=1` + 再按 `M` | Auto:进入 step 自动播 + 自动推进(录制用) |
| Auto 模式下首次按 `Space` | 启动 Auto 播放(绕过浏览器自动播放限制) |
也可以鼠标移到右上角,会出现一个隐藏的模式切换按钮。
---
## 备用流程:没合成音频时手动录屏
如果你跳过了音频合成(`Checkpoint Audio` 选了"不合成"),按老方法:
1. 浏览器全屏 → 打开 `localhost:5173`(默认 Manual 模式)
2. **刷新一次**清空历史 step
3. 开始录屏 → 按口播节奏点击空白推进 step
4. 后期用任何剪辑软件配音 + 调时间线
### 后期工具
| 工具 | 适合 |
|---|---|
| **DaVinci Resolve** | 跨平台免费、能处理多段音频拼接 |
| **iMovie** | macOS 简单场景 |
| **CapCut / 剪映** | B 站 / 抖音风加字幕 |
---
> agent 在 Checkpoint Audio 后**主动告诉用户**上面 Auto 模式录屏的
> 路径,让用户知道下一步怎么把网页变成 mp4。
@@ -0,0 +1,409 @@
# 文章 → 口播稿风格指南
把书面文章转成"说出来不别扭"的口播稿,默认 **B 站风格**。其他平台风格
变体见末尾。
> **三条底线**(任一不过,整稿重写):
>
> 1. **信息保留度 ≥ 60%**(详见下一节)—— 口播稿是"换说法",不是
> "摘要"。删冗余 / 修饰可以,删事实 / 数据 / 案例 / 论证链不行。
> 2. **去 AI 味** —— 即使句子已经短句化、口语化、第二人称,AI 的腔调
> (假共情 / 假深刻 / 自我标榜 / 万能模板 / 排比堆砌)念出来仍然
> 一耳朵是机器。**口播比文字更怕 AI 味**。
> 3. **保持原文语言** —— 除非用户明确要求翻译,`script.md` 必须保持
> 用户输入的主语言。英文文章生成英文口播稿,中文文章生成中文口播稿;
> 中英混排时保留原文中的术语、引用、产品名、代码和专有名词。
> "B 站风格 / YouTube 风格 / 口语化"只调整节奏、结构和表达自然度,
> **不改变语言**。
>
> 8 条原则讲**形式**,「去 AI 味五类」讲**风骨**。两者都建立在
> 「信息保留度」这一最高约束之上。
---
## 最高原则:信息保留度 ≥ 60%(删减不得超过 40%)
> 这条**优先于**下面所有 8 条原则和去 AI 味五类。先保证信息没丢,
> 再调形式 / 风骨。
**判定标准**:
- `len(script.md) ÷ len(article.md) ≥ 0.6`(中文按字符数计;代码块 /
Markdown 标记 / 引用源不计入)
- 关键事实(数字 / 名称 / 时间 / 论断)**逐项对得上**,不能整段消失
- 文章里的**论证链**(前提 → 结论 / 因果 / 对比)必须保留,可以换说
法但不能跳步
**允许大幅压缩的**:套话开场 / 重复总结 / 学术性铺垫 / 引文出处的
完整书目格式 / 客套结语。
**禁止压缩的**:
- 关键数据和数字(哪怕"翻译成感受"也要保留原始数字本身作旁注)
- 具体案例 / 例子 / 故事
- 多步论证里的中间步骤
- 反方观点 / 限制条件 / 例外情况(最容易被 AI 一刀切掉)
**如果原文确实太长**(比如 5000+ 字 article 做 3 分钟视频)→ 不要靠
压缩硬塞,**告诉用户**:"原文 X 字,按 60% 留存最少需要 ~Y 分钟视频,
要么拉长视频要么拆成多集"。**不要**自作主张缩到 30% 然后假装做完了。
---
## B 站风格的 8 条原则(形式层面)
### 1. 口语化:所有书面语换成"说出来不会别扭"的句子
| 书面语 | 口播 |
|---|---|
| 综上所述 | 所以你看 |
| 具有非凡的意义 | 这件事真的挺重要 |
| 在……方面有显著提升 | 在……上明显变强了 |
| 该工具旨在 | 这玩意是用来 |
| 笔者认为 | 我觉得 |
| 由此可见 | 你看吧 / 这就是为啥 |
| 与之相反 | 反过来 |
| 据相关资料显示 | 我查了下 |
| 详情请参阅 | 想看细节自己去翻 |
判断标准:**把它念出来,会不会觉得"这人怎么这么端着"**?会就改。
### 2. 短句:每句 ≤ 20 字
长句拆短句,逗号优先于复合句。
| 原文 | 改写 |
|---|---|
| "尽管它在文字渲染方面有了显著的提升,但在某些复杂场景下仍然会出现错误。" | "文字渲染确实变强了。但复杂场景下,它还是会翻车。" |
| "我们通过对比测试发现该模型在指令遵循能力上明显优于上一代。" | "我做了一组对比测试。指令遵循这一项,比上一代强太多了。" |
讲话的人不会喘不上气。屏幕前的人也不会。
### 3. 第二人称:多用"你 / 我们",少用"用户 / 读者"
| 原文 | 改写 |
|---|---|
| 用户可以通过该接口实现 | 你只要调一下这个接口 |
| 读者将会发现 | 你会发现 |
| 开发者需要注意 | 你写的时候要小心 |
| 本文将介绍 | 我们来看一下 |
让屏幕前的人感觉**你在跟他说话**,而不是宣读公告。
### 4. 开头有钩子(cold open)
第一句必须**抓住人**。三种通用钩子:
- **悬念**:"这是真的吗?" / "你猜怎么着" / "我以为是 bug,结果……"
- **反差**:"这玩意以前要 3 小时,现在 30 秒。" / "上周还人人吹爆,今天就翻车了。"
- **利益**:"看完这条,你就能省下买插件的钱。" / "学会这一招,开会再也不怕被点名。"
**禁止**(前两个是 PPT 感,后两个是 AI 味假共情):
- ❌"大家好今天我们来讲讲" 这种自我介绍开场
- ❌"本期视频我们将探讨" 这种 PPT 标题感
- ❌"在开始之前,让我们先了解一下背景" 这种先讲一堆铺垫
- ❌"我知道你最近肯定很纠结 X / 你是不是也有这种感觉" 假共情钩子
- ❌"接下来我说的,可能会颠覆你的认知" 自我加权钩子
钩子要在 **3 秒内**砸到观众脸上。亲不亲是观众听完判断的,不是你一
开口就宣布的。
### 5. 节奏停顿:用 `---` 分隔每个完整想法
每讲完一个聚焦的想法 = 一个 step 边界 = 一个 `---`。一行一个 idea,
不堆叠。
```markdown
所以呢,这玩意儿真的挺夸张的。
---
举个例子。前几天我让它画一张海报,三秒就出。
---
但你知道最离谱的是啥吗?连里面的中文字都对了。
```
`---` 之间的内容就是后面 `outline.md` 里的一个 step。
### 6. 数字翻译成感受
观众听到具体百分比和大数字会**走神**。除非数字本身就是核心内容
(比如"涨了 20 倍"作为冲击点),否则翻译成感受。
| 原文 | 改写 |
|---|---|
| 准确率提升了 47% | 几乎快了一倍 |
| 覆盖 120 个国家 / 地区 | 全世界基本到处都用 |
| 用户数突破 1.2 亿 | 现在已经是国民级应用了 |
| 推理速度提升 3.4 倍 | 比以前快多了,不用等 |
例外:**核心冲击数字**保留并强调("前后差了 100 倍"、"3 秒 vs 3 小时")。
### 7. 不堆结构词 / 不堆万能模板
口播稿一旦堆"首先 / 其次 / 最后"或"说白了 / 本质上 / 底层逻辑",
机器味就直接溢出。
**禁止**(结构词类):
- ❌"首先 / 其次 / 最后"
- ❌"接下来我们看下面三点"
- ❌"总结一下,本期视频我们讲了什么什么"
- ❌"下面进入第二部分"
**禁止**(万能模板类,AI 高频指纹 —— 详见去 AI 味第 4 类):
- ❌"说白了 ……" / "本质上 ……" / "底层逻辑是 ……"
- ❌"一句话总结 ……" / "归根结底 ……" / "换句话说 ……"
**做法**:把这些词融进自然过渡,**或者直接砍掉**。
- "首先,文字渲染" → "先说文字渲染"
- "接下来看第二点" → "再看一个"
- "总结一下" → "所以你看"
- "说白了它就是 X" → "它就是 X"
- "一句话总结:Y" → 直接说 Y
观众不需要知道你的视频是几段式 outline。**结构感从内容节奏来,不是
从指令来**。
### 8. 具体例子优先
**抽象**会让人走神,**具体**会让人坐住。
| 抽象 | 具体 |
|---|---|
| 它有很强的能力 | 你给它一张猫的照片,让它换成赛博朋克风,3 秒就出 |
| 性能优越 | 我那台 5 年前的旧电脑跑得动 |
| 用户体验良好 | 我妈第一次用,5 分钟就上手了 |
| 应用场景广泛 | 海报、PPT、表情包、PR 套图都能整 |
**举例规则:从"我"出发**。"我做了一个测试"、"我让它画"、"我那台电脑"
比"用户可以"、"通常情况下"亲切 100 倍。
---
## 去 AI 味:五类必清的腔调(风骨层面)
形式做到了 ≠ 没 AI 味。下面五类是 AI 写口播稿时最稳定的"指纹"。
**写完一遍后专门扫一次,逐条砍掉**。
> 唯一判断标准:**这话念出来,一个真人会这么说吗**?不会就改。
### 一、假共情:开口就演"我懂你"
AI 特别喜欢在开头先做一轮情绪按摩,再进入正题。但口播一开口就装亲
密 = 油腻;观众还不认识你,你就宣布"我懂你",听起来更像销售。
❌ 典型句式:
- "我知道你最近肯定很纠结 X"
- "你是不是也有这种感觉"
- "今天这条视频,是给那些一直在……的朋友"
- "我能理解你的心情"
- "你的感受是被看见的"
✓ 怎么改:直接抛事实 / 钩子(见原则 4)。亲不亲是观众听完判断的,
不是你一开口就宣布的。如果原文确实需要表达共情,用具体的、有信息
量的方式 ——"这个坑我也踩过"比"我懂你的感受"真实十倍。
### 二、假深刻:用"恰恰 / 反而 / 正是"包装平庸观点
AI 很擅长用转折词制造"顿悟感"。但仔细一看,去掉这些词,意思一样,
甚至更清楚。
❌ 典型句式:
- "你以为它强是因为模型大,恰恰相反 ……"
- "这反而是它真正可怕的地方"
- "正是因为它够简单,所以才 ……"
- "你想得太对了"
- "不是因为 X,是因为你太 Y 了"
✓ 怎么改:去掉转折包装直接说结论。**自检方法**:"恰恰 / 反而 / 正
是"那句去掉之后,意思变了吗?没变就删。如果去掉后观点变得平淡 ——
**它本来就平淡**,删掉换内容,不要用修辞补救。
### 三、自我标榜:说正事前先给自己加权
AI 在说重点之前喜欢先清嗓子,让观众觉得"接下来这话特别重要"。这
在**口播里更刺耳** —— 观众正在被推销时间。
❌ 典型句式:
- "我必须很认真地说一句"
- "这件事我得展开讲"
- "接下来我说的,可能会颠覆你的认知"
- "这一段你一定要听完"
- "我讲一句可能会让你冷静一点的话"
- "不躲不藏不绕不逃,今天就把 X 说透"
✓ 怎么改:**直接说**。好观点不需要预热。担心观众划走,靠节奏(短句
+ 钩子)把人留住,不靠"加权"。
### 四、万能模板:一听就是 AI 写的口头禅
这些是 AI 生成口播稿的指纹,出现频率高到观众闭着眼都能猜下一句。
| 位置 | ❌ AI 模板 | ✓ 口播版 |
|---|---|---|
| 开头 | "说白了 ……" / "本质上 ……" / "底层逻辑是 ……" | 直接说"它就是 X" |
| 过渡 | "一句话总结 ……" / "换句话说 ……" / "归根结底 ……" | 直接说那句话,不预告 |
| 包装 | "在某种意义上" / "从某种程度上来讲" | 删掉,没意义 |
| 结尾 | "以上就是本期内容" / "希望对你有帮助" / "感谢观看" | 具体 CTA:"链接评论区置顶" / "下期见" |
| 互动 | "你想让我继续讲 X 吗?" / "评论区告诉我" | 给具体动作:"去 GitHub 搜 X" / "评论区聊聊你踩过哪个坑" |
> **注意**:这一类和原则 7「不堆结构词」有重叠 —— 原则 7 在形式层
> 警告,这里在风骨层警告,**两层都要过**。
### 五、排比堆砌:连续三句结构一样
AI 写中文时有强迫症式的整齐感。**口播比文字更怕排比** —— 念三个一
样结构的短句,观众脑子直接关机。
**判断标准**:连续 ≥ 3 个结构相同的短句,且去掉几个意思不变。
❌ 例子:
- "不躲,不藏,不绕,不逃"
- "它会画图,会写字,会做海报,会做 PPT"
- "你的痛我懂,你的累我看到,你的努力我记得"
- "高效,精准,强大"
✓ 怎么改:保留最有信息量的一两个,其余砍掉;或换成一个具体例子。
- "它会画图,会写字,会做海报,会做 PPT" →
"它会画图,重点是 —— 你昨天那张猫的赛博朋克版就是它出的"
> 真人讲话不会追求每段像对联。允许粗糙,过渡句就让它过渡。
---
## 几个**不要做**的细节
- ❌ **emoji**:除非用户明确要求,口播稿里不要 emoji。emoji 是文字
弹幕,不是讲话
- ❌ **括号补充**:"这个模型(一种基于 transformer 的架构)很厉害" ——
括号里的话讲不出来,要么并入正文要么删
- ❌ **罗列书名号 / 引号**:"《XX 报告》指出,XX" —— 改成"我看到一份报告说"
- ❌ **"哈哈哈"等口头语堆砌**:偶尔一次效果好,每段一个就成抽搐
- ❌ **过度网络梗**:梗有半年保鲜期;半年后看就尴尬。除非主题就是网络梗
---
## 平台风格变体
如果用户指定了别的平台,按下表调("去 AI 味五类"对所有平台都适用):
| 平台 | 风格基调 | 单句长度 | 钩子节奏 | 信息密度 |
|---|---|---|---|---|
| **B 站**(默认) | 自来熟、有段子、节奏快 | ≤ 20 字 | 3 秒钩子 | 中(5 秒/idea) |
| **抖音 / 视频号** | 更燃、更短、更冲击 | ≤ 12 字 | 1 秒钩子 | 高(3 秒/idea) |
| **YouTube long-form** | 舒缓、详细、可铺垫 | ≤ 30 字 | 30 秒铺垫可以 | 低(10 秒/idea) |
| **知乎视频** | 严谨一点、有逻辑感 | ≤ 25 字 | 5 秒可铺一点背景 | 中(5~7 秒/idea) |
| **小红书** | 闺蜜分享感、有情绪 | ≤ 15 字 | 直接讲结论 | 中(5 秒/idea) |
> **小红书闺蜜感 ≠ 假共情**。"姐妹们听我说,这个真的踩雷了" 是闺蜜
> 感(具体 + 有情绪);"我知道你最近肯定很纠结" 是 AI 假共情(空泛
> + 上来就装熟)。区别在**有没有具体场景**。
## 主题类型微调
不同主题类型,有不同的语气基线:
- **技术 / 教程**:可以稍微"端着"一点,但仍然口语化。多用"我们试一下"
而不是"假设我们……"
- **测评 / 对比**:要"敢说"。"这个不行"、"那个吹过头了"。中立测评
会让人睡着 —— 但**敢说 ≠ 自我标榜**,"我用了一周觉得不行" 是敢
说,"我必须很认真地说它不行" 是自我标榜
- **新闻 / 资讯**:节奏要快,金句要硬。开头第一句必须给最炸的点
- **产品 / 营销**:忌"吹"。不说"完美"、"颠覆",说"我用了一周,确实
顺手"
- **文化 / 历史 / 科普**:可以放慢节奏,多用"你想象一下"开场
---
## 转写完之后做什么
### 1. 落盘 + 切节拍
落到 `script.md`,原文保留为 `article.md`。每段 ≥ 5 句没用 `---`
切?切一下 ——`---` 是后面 step 边界,多切总比少切好(合并步骤比拆
步骤容易)。
> ⚠️ **自检是硬性流程**:下面的三层自检(形式 / 风骨 / 念出来)写完
> `script.md` 后**必须**全部走完 → 修改 → 再继续。**禁止**写完直接进
> 入"产出 outline"环节。
>
> **执行方式**(按能力降级):
>
> 1. **优先 Agent Teams**:开一个独立 reviewer agent,传入 `script.md`
> + 本节三层清单,让它**逐项核查 + 出结论**(哪几条 fail + 证据 +
> 改写建议)。
> 2. **其次 subAgent**:当前 agent 没 Teams 但能开 subagent,用 subagent
> 走同样流程。
> 3. **都没有**:自己**严格逐项**核查,特别是「念出来」一定要按字面
> 执行。
>
> 拿到结论后**先按 fail 项改稿子,再产出 outline**。
### 2. 形式层自检(8 条原则)
- [ ] **信息保留度 ≥ 60%**(**最高优先级**,不过整稿重写)—— 用
`wc -m script.md` / `wc -m article.md` 算比例;关键数字 / 案例 /
论证链逐项对照原文,不能整段消失
- [ ] 没出现 emoji / 原文书名号《》 / 括号补充 /「据 XX 报告显示」类
引文格式(口播念不出来)
- [ ] 每句 ≤ 20 字(B 站基准)?
- [ ] 第二人称?
- [ ] 开头 3 秒钩子?
- [ ] 数字翻译成感受了?(**核心冲击数字必须保留原值**)
- [ ] 没"首先 / 其次 / 最后"结构词?
- [ ] 都是具体例子,不是抽象描述?
### 3. 风骨层自检(去 AI 味五类)—— **最重要**
逐条扫一次,**任何一条没通过就回去改**:
- [ ] 全文还有 "说白了 / 本质上 / 底层逻辑 / 恰恰 / 反而 / 正是因为
/ 归根结底 / 在某种程度上" 等 **AI 高频词**吗?
- [ ] 开头第一句是钩子,**不是**"我知道你 / 你是不是也"假共情?
- [ ] 有 "我必须认真说 / 我得展开说 / 可能会颠覆你的认知 / 这段你一
定要听完" 等**自我加权**吗?
- [ ] 有连续 ≥ 3 句结构一样的**排比**吗?念到第三句你自己烦不烦?
- [ ] 结尾是具体 CTA,**不是** "希望对你有帮助 / 以上就是本期内容
/ 感谢观看" 套话?
- [ ] 全文是否有用 "恰恰 / 反而 / 正是" 包装的句子?**去掉这些词
意思变了吗**?没变就删。
### 4. 念出来测试(终极标准)
随便挑 3 段念出来 —— 不要在心里默读,**真的张嘴念**。
- 念到哪句你觉得"这话我说不出口" → 那句是 AI 味
- 念到哪句你想跳过 → 那段是排比 / 自我标榜
- 念到哪句你想笑出声(不是因为内容好笑,是因为油腻)→ 那句是假共情
**改完再念。直到三段念下来都自然为止**。
### 5. 在同一次工作里产出 outline
`script.md` 落盘 + 自检通过后,**不要立即停下来等用户**——按
[`OUTLINE-FORMAT.md`](OUTLINE-FORMAT.md) 在**同一次思考**里继续产出
`outline.md`(章节切分 + 每步屏幕内容 + 章节级信息池),然后再进入
Checkpoint Plan 让用户一次对齐 5 件事(稿子 / outline / 主题 / 素材
/ 开发模式)。
> **流程变化提醒**:旧流程把 script / outline 切成两个 checkpoint,
> 新版合并为一个(详见 SKILL.md「Phase 1.2 一次产出」)。理由是 outline
> 不再写动画后,它的依赖只剩"稿子切节拍 + article 抽信息池",可以
> 和稿子一起做完。
---
> **核心提醒**:去 AI 味不是降质。把"AI 在朗诵"换成"人在聊天",信
> 息量一个不少,腔调全部换掉。如果换了之后内容变蠢 —— **它本来就
> 蠢**,不是去 AI 味的锅,是该补内容了。
@@ -0,0 +1,351 @@
# 主题系统
每个演示从头到尾跑**一个主题**。我们**不**在章节间翻转明暗 —— 那会
打断视频的视觉连贯性,录屏时看起来像很硬的剪辑。如果想要"暗一点的氛围"
段落,请在**同一调色板内**降对比、收聚光,而不是翻转表面色。
主题 = 一组 CSS 设计 token + 一个 `theme.json` 元数据。
**章节对 token 的消费分两层**:
1. **必须用 token 的**(换主题不破的底线)—— 颜色 + 字体家族
2. **章节自由发挥的**(按内容设计)—— 字号 / 间距 / 动画时长 / 缓动 /
边框宽度 / 一般圆角 / 字距等都可硬编码
主题**不只管**颜色和字体,但其他维度(hero 数字、分割线、卡片、舞台
装饰)通过 **primitive class**(`.hero-num` / `.rule` / `.card` /
`.stage-frame`)自动接入,章节用 class 即可,不需要手动 `var()`。
主题管的维度:
| 维度 | 主题怎么管 |
| -------------------------- | ----------------------------------------------------------------- |
| **调色板** | shell / surface 阶梯、text 阶梯、accent + 透明度衍生 |
| **字型** | 中文 / 英文 / body / 等宽家族 + OpenType 特性集 |
| **舞台 padding 密度** | `--stage-pad-x/y` —— 精炼主题 140×100,密集主题 80×60 |
| **圆角性格** | `--r-card` —— sharp (0) / refined (4) / soft (16) / keynote (32) |
| **分割线性格** | `--rule-w` + `--rule-style` —— 细/粗 × 实/虚 |
| **hero 数字风格** | `--hero-num-*` —— 编辑级斜体 / 终端等宽 / 粗黑 / 手写 |
| **舞台 / 卡片阴影** | `--shadow-stage` / `--card-shadow` —— 纸浮 / 偏移实色 / 内阴影 |
| **装饰层** | `--surface-pattern*` / `--surface-vignette` / `--text-shadow` |
| **动效基线** | `theme.json` 的 `mood` —— 电影感慢 / 弹簧 / 利落 / 安静 |
> **`mood` 不写时长数值**。具体 ms / 缓动由 chapter agent 看 `mood`
> 自己拍板(慢主题别写 200ms 快动画,仅此而已)。
每个主题约 25~35 个 token。完整契约见下方。
---
## 内置主题
23 套主题,每个都有**独立的设计 DNA** —— 不是简单的换色版。挑一个
匹配你主题情绪的,或者作为你自己主题的起点。
### 深色主题
| id | 性格 |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `midnight-press` | 电影感编辑级深色。暖色 espresso(不是纯黑)+ 火热橙。Instrument Serif italic 英文 vs Noto Serif SC 中文。hero 数字:斜体衬线。慢速电影感节奏(1.6s 揭示)。140×100 padding。只有 vignette,没有颗粒。 |
| `chalk-garden` | 深石板黑板。Patrick Hand 全场手写,粉笔黄 accent。**2px 虚线 rule** 是签名。film grain(overlay)+ vignette。衬线带 chalk text-shadow。手绘节奏。 |
| `terminal-green` | 80 年代磷光终端。纯黑 + JetBrains Mono only + 0px 直角。**CRT 扫描线**贴在舞台上。文字带磷光 text-shadow。利落线性动效(180/400/650ms)。hero 数字:等宽带发光。 |
| `blueprint` | 工程蓝图。深海军蓝 + 绘图青 + IBM Plex Mono。**2px 虚线青色 rule + 60px 制图网格**是签名。hero 数字:等宽青色。等宽配对营造技术 / 蓝图感。 |
| `dark-botanical` | 高级感编辑暗底 —— 时尚刊物 / 博物馆图录。近黑 + 暖陶 / 玫粉 / 鎏金叠层。Cormorant italic + IBM Plex Sans。**柔光晕染(blurred light pool)作为签名**。慢速电影感节奏(1.7s)。140×100 padding。 |
| `neon-cyber` | 赛博朋克未来派。深海军底 + 电光青 + 玫红双霓虹。Clash Display + Satoshi。**青色发光网格 + 双色霓虹描边(cyan + magenta text-shadow)**是签名。snappy 节奏(380/650ms)。 |
| `bold-signal` | hero pitch-deck 暗底。Archivo Black + Space Grotesk。大橙色焦点色卡 + 制表数编号。**对角线深色渐变 + 大字标语**是签名。punchy 节奏(420/680ms)。 |
| `creative-voltage`| 复古朋克创意工作室。饱和电光蓝底 + 霓虹黄强调。Syne + Space Mono。**halftone 网点 + 偏移霓黄阴影**是签名。punchy + 能量节奏(450/720ms)。 |
### 浅色主题
| id | 性格 |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paper-press` | midnight-press 的白天孪生兄弟。暖奶油 + 纸纹(multiply blend)。火热橙。hero 数字:斜体编辑级衬线。慢速电影感节奏。140×100 padding。 |
| `warm-keynote` | 现代 SaaS keynote。奶油 + 棕褐墨 + 青绿 + Inter。**大圆角(32px)glass slab** 配 backdrop blur。**粗黑 font-black hero 数字**。舞台上 40px 暖色网格。弹簧动效。 |
| `newsroom` | NYT 报刊。报纸奶油 + 墨黑衬线 + 旗红。Playfair Display + Noto Serif SC。**0 圆角**(报纸不会圆角)。hero 数字:超大斜体显示衬线。安静的印刷节奏。淡纸纹。 |
| `bauhaus-bold` | 现代主义宣言。米白 + 墨黑 + 原色蓝。Archivo Black + Inter。**0 圆角 + 4px 实色厚边 + 4px 黑色画框包住舞台 + 偏移实色阴影**。hero 数字:font-weight 900 巨字。利落快速动效。无装饰。 |
| `sunset-zine` | 独立 risograph zine。暖桃 + riso 洋红 + Fraunces。**虚线剪贴线 + 偏移桃色阴影**。hero 数字:斜体 Fraunces。粗 riso 纸纹。弹簧 overshoot 动效。 |
| `monochrome-print` | 安静精炼的印刷杂志 —— Monocle / Wallpaper / MIT Press。米白 + 墨黑衬线 + 墨蓝 accent。Source Serif。**只有 1px 实线发丝、4px 精炼圆角**。hero 数字:斜体 tabular figures。**无装饰** —— 极简纯粹。极静节奏(1.7s 揭示)。 |
| `vintage-editorial` | 俏皮编辑奶油底。Fraunces italic + Work Sans + 暖陶 accent。**细线几何叠层(圆 + 线 + 点)**是签名。有性格、会说话,像专栏作家。中速带轻微 overshoot。 |
| `pastel-dream` | 友好柔光。柔粉蓝灰底 + 奶油卡 + 鼠尾草绿。Plus Jakarta Sans。**大圆角(20px)+ 右侧多色 pill 色条**是签名。soft springy(520/820ms)。 |
| `split-canvas` | 双拼画布 —— 蜜桃 + 薰衣草 50/50 硬切分。Outfit + 玫红 accent。**屏幕本身就是双色底**,章节自由在哪一侧落内容。playful 中速(480/780ms)。 |
| `electric-studio` | 企业电光蓝。净白底 + 单一电光蓝 + Manrope。**贴底 4px 电蓝条**作为签名,B2B / 路演 / 财报场景的清晰自信。punchy 节奏(420/700ms)。 |
| `indigo-porcelain` | 靛蓝瓷 —— **靛蓝当墨**(不是 accent,是字色本身)+ 瓷白纸。Playfair Display italic + Noto Serif SC + IBM Plex Sans 正文。学术 / 研究气质,像一本当代思想期刊。无装饰 —— 纯粹。慢速(1.55s)。 |
| `forest-ink` | 森林墨 —— **森林绿当墨** + 象牙暖纸。Source Serif 正文 + Playfair Display。旧版《国家地理》感,沉稳、文献感。faint warm grain。慢速(1.65s)。 |
| `kraft-paper` | 牛皮纸 —— **深棕当墨** + 牛皮米。Fraunces + Source Serif + 紫铜 accent。老笔记本 / 老信封感。**粗暖纸纹**是签名。慢速 tactile(1.55s)。 |
| `dune` | 沙丘 —— **炭褐当墨** + 沙底 + 几乎无 accent(muted clay)。Inter display + Source Serif 正文。**无装饰 + 极宽 padding(140×100)**是签名。建筑手册 / 画廊感。最慢节奏(1.75s)。 |
| `swiss-ikb` | 瑞士国际主义。**极细 200 weight Inter / Helvetica** + 净暖白底 + IKB 克莱因蓝 + **1px 发丝网格 (64px)**。`r-card: 0` 直角。Massimo Vignelli / Helvetica Forever 能量。punchy + linear(400/650ms)。 |
随时列出可用主题:
```bash
bash <path-to-web-video-presentation>/scripts/scaffold.sh --list-themes
```
---
## 脚手架时挑一个主题
```bash
# 默认(midnight-press)
bash scripts/scaffold.sh ./presentation
# 显式指定
bash scripts/scaffold.sh ./talk --theme=newsroom
```
脚手架会把所选主题的 `tokens.css` 拷到 `<project>/src/styles/tokens.css`,
并把主题 id 写到 `<project>/.theme`,方便以后看是从哪个主题开始的。
---
## 之后切换主题
切换 = 一次文件覆盖:
```bash
cp <path-to-web-video-presentation>/themes/newsroom/tokens.css \
presentation/src/styles/tokens.css
```
刷新 dev server。完成。章节代码一行没动。
如果切换后某章节看起来有问题,那是该章节在某处硬编码了颜色 / 字体 /
尺寸,而不是用语义 token。去找出来 —— bug 在章节里,不在主题里。
---
## 完整 token 契约
`base.css` 给**性格 token 都准备了合理的默认值**。主题的 `tokens.css`
只需要覆盖**调色板 + 字体 + 性格旋钮 + 装饰**这四类。
> **base.css 里的字号 / 间距 / 时长尺度只供 primitive class 自己用**
> (`.label-mono` / `.kicker` / `.scene-pad` 等)。**不是**章节必须消费
> 的契约——章节这一层要不要 `var(--t-h1)` 还是直接写 `font-size: 96px`
> 完全自由。
### 必填(主题必须定义)
#### 表面色(4 个)
| token | 作用 |
| ------------- | --------------------------------------------------- |
| `--shell` | letterbox / 舞台外的页面背景 |
| `--surface` | 舞台主背景 |
| `--surface-2` | 凸起 —— 卡片、代码块、嵌入面板 |
| `--surface-3` | 最里层 —— surface-2 里再嵌一层时用 |
#### 文字(4 个)
| token | 作用 |
| -------------- | ------------------------------------- |
| `--text` | 主 |
| `--text-2` | 次(副标题、正文) |
| `--text-mute` | 静音 —— 标签 / 元数据 |
| `--text-faint` | 三级 —— 提示 / 禁用 |
#### 线条(1 个)
| token | 作用 |
| -------- | ----------------- |
| `--rule` | 发丝分割线颜色 |
#### Accent(3 个)
| token | 作用 |
| --------------- | --------------------------------------------- |
| `--accent` | accent 本体(一个品牌强色) |
| `--accent-soft` | 低透明度叠层 —— pill 背景、悬浮光晕 |
| `--accent-glow` | 中透明度叠层 —— text shadow、圆点发光 |
#### 字型家族(4 个)
| token | 作用 |
| ------------------- | ------------------------------------------ |
| `--font-display-cn` | 中文显示家族 |
| `--font-display-en` | 拉丁显示家族(斜体强调声音) |
| `--font-body` | 正文 / 段落家族 |
| `--font-mono` | 等宽家族(终端、mono caps、badge) |
### 可选的性格覆盖(主题应该定义来表达自己的性格)
这些有 base 默认值;主题重新定义来表达性格。
| token | base 默认 | 作用 |
| ------------------ | ------------------- | ----------------------------------------------------- |
| `--font-features` | `"tnum","ss01"` | body 上的 OpenType 特性栈 |
| `--r-card` | `--r-md` | 默认卡片圆角(sharp / refined / keynote) |
| `--r-stage` | `0` | 直接加在舞台本身的圆角 |
| `--rule-w` | `1px` | rule 粗细(1=发丝,2=中等,4=厚重) |
| `--rule-style` | `solid` | rule 样式(`solid` / `dashed` / `dotted`) |
| `--hero-num-font` | `--font-display-en` | `.hero-num` 用什么字体(主题决定性格) |
| `--hero-num-style` | `italic` | `italic` / `normal` |
| `--hero-num-weight`| `400` | 400(编辑级)/ 500(等宽)/ 900(粗黑) |
| `--hero-num-track` | `--track-tight` | hero 数字的字距 |
| `--stage-pad-x` | `96px` | 舞台横向内边距(密度旋钮) |
| `--stage-pad-y` | `80px` | 舞台纵向内边距 |
| `--card-shadow` | none | `.card` 的 box-shadow |
| `--card-glass-bg` | `rgba(255,255,255,0.06)` | `.card-glass` 的背景 |
| `--card-glass-border` | `rgba(255,255,255,0.12)` | `.card-glass` 的边框 |
| `--shadow-stage` | dark drop | 舞台的 box-shadow |
| `--stage-border` | `none` | 舞台的可选边框(Bauhaus 用 `4px solid black`) |
### 可选的装饰层(主题可选用,给质感加签名)
这些默认是 no-op;主题选择性启用。装饰画**在舞台上**(pattern 用
`stage-frame::after`,vignette 用 `stage-frame::before`),所以会被屏幕
录制器捕捉到。
| token | 作用 |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `--surface-pattern` | 叠在舞台上的 `background-image`。SVG 噪声 / 网格 / 扫描线。 |
| `--surface-pattern-size` | 配套的 `background-size`。可平铺渐变必填。 |
| `--surface-pattern-blend` | pattern 层的 `mix-blend-mode`(`normal` / `multiply` / `overlay`)。 |
| `--surface-pattern-opacity` | pattern 层的整体透明度乘子。 |
| `--surface-vignette` | vignette 叠层的 `background`(黑板 / 电影感边角的径向渐变)。 |
| `--text-shadow` | 应用在 `.serif-cn` / `.serif-it` / `.display-en` 上。如粉笔晕 / 磷光辉。 |
如果你需要的装饰找不到对应槽位,那就跨过"主题契约"边界进入"章节自定义
CSS"领域 —— 在那里解决,别扩主题契约。
---
## 创作新主题
### 1. 复制一个最接近的作为起点
挑一个**最接近**你目标气质的:
| 目标情绪 | 起点 |
| --------------------------------------- | --------------------- |
| 阴郁、电影感、编辑级 | `midnight-press` |
| 编辑级 - 浅色 | `paper-press` |
| 现代 keynote / SaaS | `warm-keynote` |
| 教室 / 解说 | `chalk-garden` |
| 终端 / 黑客 / 复古 CRT | `terminal-green` |
| 纪录片 / 严肃 / 新闻 | `newsroom` |
| 工程 / 蓝图 / 技术 | `blueprint` |
| 现代主义 / 布鲁塔利斯特 / 宣言 | `bauhaus-bold` |
| 独立 / 玩味 / zine | `sunset-zine` |
| 精炼 / 安静 / 印刷 | `monochrome-print` |
| 高级感暗底 / 时尚 / 博物馆图录 | `dark-botanical` |
| 赛博朋克 / 未来感 / AI / web3 | `neon-cyber` |
| 俏皮编辑 / 有声音的博主 / 文化随笔 | `vintage-editorial` |
| 柔粉 / 友好 / onboarding / 女性向 | `pastel-dream` |
| 双色分屏 / 对照 / 辩论 | `split-canvas` |
| pitch deck / 大字宣言 / 焦点色卡 | `bold-signal` |
| B2B / 企业 / 投资人路演 | `electric-studio` |
| 复古朋克 / 创意工作室 / 设计周 | `creative-voltage` |
| 学术 / 研究 / 中国当代文化 | `indigo-porcelain` |
| 自然 / 可持续 / 户外 / 纪录 | `forest-ink` |
| 文学 / 怀旧 / 书评 / 手工艺 | `kraft-paper` |
| 建筑 / 艺术展览 / 高端画廊 | `dune` |
| 瑞士国际主义 / Helvetica / 信息驱动设计 | `swiss-ikb` |
```bash
cd <path-to-web-video-presentation>/themes
cp -r monochrome-print my-theme
```
### 2. 改 `my-theme/tokens.css`
按契约自上而下走一遍:调色板 → 字体 → 性格旋钮(`--r-card` /
`--rule-*` / `--hero-num-*` / `--stage-pad-*`)→ 阴影 → 装饰。
**不要**碰字号 / 间距 / 时长尺度 —— 那些是 base.css 给 primitive class
用的内部默认值,不是主题契约的一部分。
**几条不那么显而易见的规则:**
- 深色主题里 `--shell` **比 `--surface` 更深 / 更饱和**;浅色主题里
`--shell` **比 `--surface` 略灰一点** —— 这样舞台读起来是"主体",
外围会退后。
- 维持 `--text` 与 `--surface` **至少 4.5:1 对比度**。96px+ 的标题
可以放宽到 3:1,body / cue 必须 ≥ 4.5:1。
- `--accent` 是**唯一的**饱和色。第二个饱和色会跟第一个打架。
- `--accent-glow` 和 `--accent-soft` 是 `--accent` **同色相的透明度
叠层**,永远不要用别的色相。
- `--text-faint` 在 `--surface` 上 13px 大写时**仍然要可读**。
- 挑**一个设计签名**重重发力:虚线 rule、粗黑边、扫描线、纸纹、glass
slab。别同时叠三个。
### 3. 改 `my-theme/theme.json`
```json
{
"id": "my-theme",
"name": "My Theme",
"nameZh": "我的主题",
"description": "一句英文描述它的气质。",
"descriptionZh": "一句中文描述它的气质。",
"mood": ["dark", "moody", "futuristic"],
"bestFor": ["<匹配场景 1>", "<匹配场景 2>"],
"preview": {
"shell": "#080808",
"surface": "#101010",
"text": "#f0f0f0",
"accent": "#ffd54a"
}
}
```
`id` 必须等于目录名。
### 主题元数据字段说明
| 字段 | 必填 | 取值 | 决定什么 |
|---|---|---|---|
| `id` / `name` / `nameZh` | ✓ | 字符串 | 主题标识 |
| `description` / `descriptionZh` | ✓ | 一句话 | Checkpoint Plan 列清单时的简介 |
| `mood` | ✓ | 标签数组 | 模糊匹配用 |
| `bestFor` | ✓ | 场景数组 | Checkpoint Plan 智能推荐时的命中点 |
| `preview` | ✓ | 4 色对象 | Checkpoint Plan 列清单时的视觉预览 |
> **主题不再约束动画选型 / 时长 / 字号 / emoji**。视觉风格由 `tokens.css`
> 的颜色 / 字体 / 字号 token 决定,动画 / 节奏 / 视觉演示完全交给 chapter
> agent 在每章实现时按内容自由发挥,避免主题字段过早限制创造力。
>
> 风格审美约束(不要紫粉渐变、不要 emoji 装饰、不要假数据等)由
> [`CHAPTER-CRAFT.md`](CHAPTER-CRAFT.md) 统一规定,与具体主题无关。
### 4. 用所有 demo 章节测试一遍
```bash
bash scripts/scaffold.sh /tmp/test-theme --theme=my-theme
cd /tmp/test-theme
npm run dev
```
把 demo 每一步点完。检查:
- 标题衬线在舞台上很清晰。
- accent 圆点在发光但不爆。
- 斜体强调有可读的背景。
- 进度条(悬浮底边)能看到,是 accent 色。
- masthead 行(`.masthead`)读起来像编辑 chrome,不像 navbar。
- hero 数字(`.hero-num`)感觉**和整体字型同源**,不像贴上去的。
- 卡片(`.card`)感觉是合适的材质(纸 / 玻璃 / cell)。
- 装饰**被注意到一次然后被忘掉** —— 永远不打扰。
哪里不对就改 `tokens.css`,刷新即可。无需重新构建。
### 5. 加到文档里
在本文件顶部"内置主题"表里追加一行。
---
## 反模式
- **章节 CSS 硬编码 hex 颜色 / 字体名** —— 缺哪个色彩 / 字体语义就在
契约里补一个,给所有主题加上(注意:**字号 / 间距 / 时长**硬编码不算
反模式,章节按内容自由设计)
- **演示中途切换主题** —— 选一个,一以贯之
- **第二个 accent 色** —— 只能有一个。用尺度 + 字重做层级
- **在组件层 override 主题 token**(颜色 / 字体 / 性格签名)—— 只在
`:root` 里覆盖。一次性的颜色需求 = 提一个派生 token,让所有主题都
提供自己的值
- **依赖主题的 TSX 条件分支** —— 章节必须主题无关。布局依赖明 vs 暗
= 布局脆弱,修布局
- **一个主题叠三个设计签名** —— 选 ONE 个(虚线 rule / 扫描线 /
glass slab / 纸纹 / 粗边),三个会自己打架