# Voice Clone Tts

> 把自媒体口播文案变成配音音频。自动做章节/分段拆分，用 MiniMax TTS（含用户本人的复刻音色）逐句合成，按采样帧精确拼接，停顿节奏可控。默认只出音频，需要字幕时可加出 SRT。触发场景：(1) 用户给一段文案要求"生成配音"、"做成音频"、"配个音"、"转成语音"；(2) 口播稿/自媒体文案转成品音频；(3) 用户要"出个口播"、"用我的声音读一遍"；(4) 需要复刻声音做 TTS；(5) 用户要文案的 SRT 字幕；(6) 使用 /voice-clone-tts 命令。

- Skill: `nanmicoder/voice-clone-tts` (Agent Skill, multi-file: 19 files)
- Install (CLI): `npx skillmds@latest add nanmicoder/voice-clone-tts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nanmicoder/voice-clone-tts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: nanmicoder (https://skillmd.com/u/nanmicoder)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nanmicoder/voice-clone-tts

---


# 文案 → 配音

给一段口播文案，产出 `voiceover.mp3`。

**默认不出 SRT**——用户在剪映里做字幕，那边能直接可视化调时间轴，比导 SRT 顺手；
而且 macOS 不认识 `.srt` 扩展名，双击会触发 Gatekeeper 警告，反而添乱。
用户明确要字幕文件时才加 `--srt`（能力仍在，见「需要 SRT 时」一节）。

音色已固化在 `config.json`，**直接跑就是用户本人的复刻声音**，不需要问用户选音色，
更不要重新复刻——复刻是一次性的独立动作，只有用户明确要求换声音时才碰 `clone_voice.sh`。

**四条命令跑完全程**，不要逐步征求同意，一口气做完再汇报：

```bash
S=~/.claude/skills/voice-clone-tts/scripts
mkdir -p voiceover-主题 && cd voiceover-主题     # 文案写进 script.md
python3 $S/segment.py script.md -o segments.json
python3 $S/synth.py segments.json
python3 $S/build.py segments.json -o voiceover
```

跑完必须做验证那一节的时间轴核对，再把结果告诉用户。

## 为什么逐句合成，而不是把整篇丢给 TTS

`mmx speech synthesize` 支持到 10k 字符，整篇一次性合成技术上可行。不这么做有四个理由：

1. **停顿可控。** 整段合成的停顿由模型即兴决定；逐句合成才能让章节 700ms、
   自然段 400ms、句子 180ms 各归各位，口播节奏才稳。
2. **增量续跑。** 改一句只重合成一句（靠文件名指纹识别），改一版文案不用重跑全篇。
3. **失败粒度细。** 撞限流时只补失败的那几段，不是整篇重来。
4. **超长文案不受 10k 限制。**

**语调不是理由。** 实测同一段 435 字文案，停顿两侧音高跳变：逐句拼接 26.4%、
整段合成 28.2%，逐句反而略优（MiniMax 内部很可能本来就按句处理）。
所以不要为了"语调更连贯"改成整段合成，那会白白丢掉上面四条。

## 三条实测得出的硬约束

**1. 必须用 WAV 合成，不能用 MP3。** MP3 的 `duration_ms` 比真实音频长 54–59ms
（编码器 padding），逐段累加会持续漂移；WAV 误差 <1ms。

| 段 | MP3 API/实测 | WAV API/实测 |
|---|---|---|
| 1 | 2736 / 2681.9 (**+54**) | 2728 / 2728.4 (+0.4) |
| 2 | 4320 / 4260.9 (**+59**) | 4388 / 4388.6 (+0.6) |

`synth.py` 已强制 `--format wav`，不要改。这条即使不出 SRT 也要守——
成品时长会不准，和视频轨对不齐。

**2. 一个分段 = 一次 TTS 调用 = 一个 WAV。** 严格一一对应，才谈得上增量续跑和精确时间轴。

**3. 时间轴按采样帧算，不要用 API 的 duration_ms。** `build.py` 用 Python `wave` 模块
按写入输出流的 frame 数累加，时间戳和音频出自同一个计数器，数学上必然一致。

顺带一提 `--subtitles` 为什么没用：41 字文案只返回**一条** 9 秒字幕；
435 字返回 5 条、每条 11–16 秒——按时长切不按句切，做视频字幕不可用。

## 拼接后听感是否连续（已实测，别再重复怀疑）

逐句合成再拼接，最容易被质疑的就是「听起来断不断」。四项都实测过：

| 项 | 实测结果 | 处理方式 |
|---|---|---|
| 拼接处爆音 | 50 个边界**零跳变**，段尾样本值全为 0 | 天然安全，MiniMax 输出首尾即静音 |
| 段间音量 | RMS 标准差 **0.56dB**，最大偏离 ±1.4dB | 无需处理（人耳需 3dB 才明显） |
| 停顿节奏 | 每段自带首尾静音中位 60ms、最坏 230ms | `build.py` 默认修剪，停顿改由 `silence_ms` 独控 |
| 跨句语调 | 见下 | 无需处理 |

**关于语调：整段合成并不比逐句拼接更连贯。** 拿同一段 435 字文案做过 A/B：

| 版本 | 停顿两侧音高跳变（中位） | >25% 占比 |
|---|---|---|
| 整段一次性合成 | 28.2% | 57% |
| 逐句合成后拼接 | **26.4%** | **53%** |

逐句版反而略优，差异在噪声范围内——MiniMax 内部很可能本来就是按句处理的。
所以**不要为了"语调连贯"改成整段合成**，那会丢掉句子级时间戳，得不偿失。

顺带确认过：`speech synthesize` 支持到 10k 字符，长文本本身没问题；
但 435 字的 `--subtitles` 只返回 5 条、每条 11–16 秒（按时长切，不按句），
做视频字幕依然不可用。这条路堵死了，逐句合成是目前唯一能同时拿到
「精确时间轴 + ≤20 字字幕」的方案。

## 开工前：先把这两件事定了

**文案从哪来。** 用户通常直接把文案粘在对话里，不是给文件路径。
先把它原样写成 `script.md`（**不要改写、不要润色、不要加标题**——用户给什么就是什么，
这是配音稿，改一个字声音就不对了）。用户给的是文件路径时直接用。

**产出放哪。** 默认在**当前工作目录**下建 `voiceover-<主题短名>/`，
所有中间件和成品都放里面，不要污染用户的项目根目录。
用户明确指定了目录就用他给的。开工前用一句话告诉用户产出位置。

目录长这样：

```
voiceover-<主题>/
├── script.md        # 原始文案
├── segments.json    # 切分结果（中间态，可校对、可续跑）
├── wav/             # 逐段音频（中间件）
└── voiceover.mp3    # ← 成品
```

中间件（`segments.json`、`wav/`）**保留不删**：改文案时能增量续跑，只重合成变化的段。

## 工作流

### 步骤 1：分章（这一步由你做，不是脚本）

「合理拆分章节」是语义判断，正则做不好。先读文案，判断结构：

- **文案已有章节标记**（`## 标题`、`一、`、`【标题】`、`第一章`）→ 直接进步骤 2，`segment.py` 能识别。
- **文案是大段白文** → 你来读懂内容、找逻辑断点，写成 `chapters.json`：

```json
[
  {"title": "开场钩子", "text": "第一段……\n\n第二段……"},
  {"title": "核心论证", "text": "……"}
]
```

分章原则：按论述逻辑切（钩子/铺垫/论证/转折/结论），单章 200–600 字为宜。段落之间用空行分隔——空行会成为 400ms 停顿，是听感上的呼吸点。

### 步骤 2：切分

```bash
S=~/.claude/skills/voice-clone-tts/scripts

python3 $S/segment.py script.md -o segments.json
# 或用你写好的章节骨架
python3 $S/segment.py script.md --chapters chapters.json -o segments.json
```

输出会报告分段数、计费字符数、单条宽度分布。**检查一下最大宽度是否超限**，超限说明有超长无标点句。

### 步骤 3：合成

```bash
python3 $S/synth.py segments.json
```

**并发默认 3，不要调高。** 实测并发 6 跑 51 段时，有 19 段撞 `rate limit exceeded(RPM)`；
降到 3 之后 51 段零失败。脚本内置自适应节流：撞限流会自动降速，之后慢慢恢复。

中断或失败后重跑同一命令即可续传，已合成的段会跳过。
若仍有大量限流失败，按提示加 `--jobs 2 --interval 1.5`。

**音频文件名带「文本+音色+模型」指纹**（`0007_a3f2b1c9.wav`）。改了文案重跑，
变化的段指纹变、自动重新合成，没变的段照常复用——不会出现「文案改了但音频还是旧的」。
遗留的失效音频用 `--prune` 清理。

### 步骤 4：拼接出片

```bash
python3 $S/build.py segments.json -o voiceover
```

产出 `voiceover.mp3`。

## 需要 SRT 时

默认不出。用户明确要字幕文件时：

```bash
python3 $S/build.py segments.json -o voiceover --srt
```

时间轴与音频同源（都来自采样帧计数），实测偏差 0.0ms，抽样波形互相关相关度 1.000。

**什么时候值得要**：文案里有英文专有名词。实测 whisper 转写本 skill 的音频，
数字全对（2736、2681.9、54），但专名大面积出错——
`ffprobe`→**NAS Pro**、`WAV`→Wave、`MiniMax`→minimax、`duration`→Duration。
剪映的 ASR 同理。而本 skill 的 SRT 文本就是原稿，一个字不会错。

法律、医疗、技术类文案（「代位权」「折价补偿」「《建工解释二》」这类）尤其明显。

**交付时提醒用户**：`.srt` 别双击——macOS 没有默认关联，会随机挑 App 打开，
还可能被 Gatekeeper 拦。直接在剪映里「导入字幕」，或右键用文本编辑打开。

## 验证（每次都要做）

```bash
ffprobe -v error -show_entries format=duration -of csv=p=0 voiceover.mp3
```

实测时长应与 `build.py` 报告的总时长一致。两者对不上说明有段音频缺失或格式不一致，别交付。

再确认 `synth.py` 报的是 `N/N 段就绪`——有失败段却继续 build，成品会缺句子。

## 参数

改 `config.json` 调默认值，或用命令行覆盖单次运行。

| 项 | 默认 | 说明 |
|---|---|---|
| `voice` | `nanmiVoice2026a` | 个人复刻音色，**默认就用它，无需每次指定** |
| `fallback_voice` | `Chinese (Mandarin)_Radio_Host` | 仅在音色失效的报错提示里出现，**不会自动切换** |
| `model` | `speech-2.8-hd` | 也可 `speech-2.8-turbo`（更快更便宜） |
| `max_chars` | 20 | 单条字幕最大视觉宽度（中文字=1，ASCII=0.5） |
| `min_chars` | 8 | 低于此宽度的片段会并入相邻段 |

段间停顿（`silence_ms`）：子句 80ms / 句子 180ms / 自然段 400ms / 章节 700ms。
嫌节奏赶就调大 `sentence`；口播感要紧凑就调小。静音时长由我们指定，改了也不影响时间轴精度。

`build.py` 默认会剪掉每段自带的首尾静音（`--no-trim` 可关闭），这样实际停顿就等于
上面设定的值，不会因为个别段自带 200ms+ 静音而忽长忽短。附带好处：字幕起点正好卡在
出声瞬间，不再提前 60ms 出现。修剪阈值 `--trim-db -45`、保留边距 `--trim-margin 15`
（留边距是为了不切掉爆破音和气声的起始）。

字幕停留：`build.py --gap-hold 600` 表示间隔 ≤600ms 时字幕延续到下一条开始，避免闪烁。设 0 则严格按语音起止。

## 声音复刻

`mmx` **没有** voice clone 命令，走「CLI 上传 + HTTP 复刻」：

```bash
$S/clone_voice.sh 素材.mov myVoiceName01          # 底噪明显时加 --denoise
```

素材要求：10 秒–5 分钟，≤20MB，mp3/m4a/wav（脚本会自动从视频抽音轨并校验）。
`voice_id` 规则：8–256 字符，首字符必须字母，只允许字母/数字/`-`/`_`，末位不能是 `-`/`_`。

两个坑：
- 需要账号**已完成实名认证**，否则复刻接口报错
- 复刻音色 **7 天内未被正式调用会被自动删除**，长期不用要定期合成一次保活

## 排错

| 现象 | 原因 |
|---|---|
| `rate limit exceeded(RPM)` | 并发太高。用默认 `--jobs 3`，严重时 `--jobs 2 --interval 1.5`，重跑续传 |
| `voice id not exist` | 音色 ID 拼错，或复刻音色已过期被删。`mmx speech voices` 查系统音色 |
| 成品时长不对／SRT 对不上音频 | 多半是有段落用了 MP3 合成。检查 `wav/` 下是否都是 `.wav` |
| 成品少了句子 | `synth.py` 有失败段就直接 build 了。先确认它报 `N/N 段就绪` |
| `build.py` 报格式不一致 | 某段 WAV 采样率/声道不同，删掉该段 WAV 重跑 `synth.py` |
| 合成大面积失败 | `mmx auth status` 查 key；`mmx quota` 查额度。TTS 走字符计费，不在 Token Plan 面板内 |
| 某段语调突兀 | 该段可能太短。调大 `min_chars` 让它并入相邻段 |
| 纯中文长句在词中间断开 | 中文没有词边界信号，无标点时只能按宽度切。给文案加逗号是唯一解 |

底层 CLI 的完整命令参考见官方 skill `mmx-cli`。

