11 KiB
音频合成
把每个章节 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
(脚手架跑完后路径是 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
cd presentation
npm run extract-narrations
这会扫所有章节的 narrations.ts,按 chapters.ts 注册顺序生成
audio-segments.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
ls scripts/tts-providers/ # 看本项目带了哪些
2.A 用内置 minimax 合成
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 合成
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 跑:
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 字段命名就行。
不要假装合成成功。
校验时长
合成完后跑:
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「代码层最小约束」)。音频文件缺失(还没合成 / 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 - mmx-cli 仓库:https://github.com/MiniMax-AI/cli
- mmx 官方文档:https://platform.minimaxi.com/docs/token-plan/minimax-cli
- mmx 参数 / 音色查询:
mmx speech --help