| name | voice-clone-tts |
| description | 把自媒体口播文案变成配音音频。自动做章节/分段拆分,用 MiniMax TTS(含用户本人的复刻音色)逐句合成,按采样帧精确拼接,停顿节奏可控。默认只出音频,需要字幕时可加出 SRT。触发场景:(1) 用户给一段文案要求"生成配音"、"做成音频"、"配个音"、"转成语音";(2) 口播稿/自媒体文案转成品音频;(3) 用户要"出个口播"、"用我的声音读一遍";(4) 需要复刻声音做 TTS;(5) 用户要文案的 SRT 字幕;(6) 使用 /voice-clone-tts 命令。 |
文案 → 配音
给一段口播文案,产出 voiceover.mp3。
默认不出 SRT——用户在剪映里做字幕,那边能直接可视化调时间轴,比导 SRT 顺手;
而且 macOS 不认识 .srt 扩展名,双击会触发 Gatekeeper 警告,反而添乱。
用户明确要字幕文件时才加 --srt(能力仍在,见「需要 SRT 时」一节)。
音色已固化在 config.json,直接跑就是用户本人的复刻声音,不需要问用户选音色,
更不要重新复刻——复刻是一次性的独立动作,只有用户明确要求换声音时才碰 clone_voice.sh。
四条命令跑完全程,不要逐步征求同意,一口气做完再汇报:
S=~/.claude/skills/voice-clone-tts/scripts
mkdir -p voiceover-主题 && cd voiceover-主题
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 字符,整篇一次性合成技术上可行。不这么做有四个理由:
- 停顿可控。 整段合成的停顿由模型即兴决定;逐句合成才能让章节 700ms、
自然段 400ms、句子 180ms 各归各位,口播节奏才稳。
- 增量续跑。 改一句只重合成一句(靠文件名指纹识别),改一版文案不用重跑全篇。
- 失败粒度细。 撞限流时只补失败的那几段,不是整篇重来。
- 超长文案不受 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:
[
{"title": "开场钩子", "text": "第一段……\n\n第二段……"},
{"title": "核心论证", "text": "……"}
]
分章原则:按论述逻辑切(钩子/铺垫/论证/转折/结论),单章 200–600 字为宜。段落之间用空行分隔——空行会成为 400ms 停顿,是听感上的呼吸点。
步骤 2:切分
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:合成
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:拼接出片
python3 $S/build.py segments.json -o voiceover
产出 voiceover.mp3。
需要 SRT 时
默认不出。用户明确要字幕文件时:
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 拦。直接在剪映里「导入字幕」,或右键用文本编辑打开。
验证(每次都要做)
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 复刻」:
$S/clone_voice.sh 素材.mov myVoiceName01
素材要求: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。