[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`