短语音频生成器
短语 Word 进,<名称>_短语音频N条.zip 出。每个短语读两遍:男声一遍 + 女声一遍。三步一条链:
短语.docx ──① 读清单──> N 条短语(条数先核对)
│
└─② TTS ──> 整段朗读.mp3(每条 W 一遍 + 1.0s + E 一遍,条间 1.8s)
│
③ 切分编号 ──> zip 交付(每条含男女两遍)
关键设计:整篇用两个声音各合成一条整轨(共 2 次请求),本地按条切成段,再交错拼成
「W 一遍 → 停 1.0s → E 一遍 → 停 1.8s → 下一条」。条间 1.8s 远大于两遍间的 1.0s,
切分器用 --merge 1.52 把「两遍」当成一个单元整块切出,交付文件里就是同一条短语的男女各一遍。
防吞音(v1.3.0 修复):起音被切掉是这类拼接最常翻车的地方,这里做了三件事——
① 切段起点落在停顿内部(不是 silence_end 那个 -35dB 越阈点);
② 裁剪阈值压到 -60dB,/m/ /ð/ /tʃ/ 这类从 -70dB 慢慢爬升的弱辅音完整保留;
③ 每一遍前后各留 50ms 静音缓冲。验收方式:交付切片的第一遍语音长度应与单独合成同一句完全相等。
文档排版:每条短语写两行是常见排版。解析时不会把第二行扔掉——两行正好对应两遍朗读
(第 1 行读 William,第 2 行读 Emily),所以 36 行 → 18 条,不会读成四遍。每条只写一行也一样读两遍。
环境
PY="C:/Users/huawei/.workbuddy/binaries/python/envs/default/Scripts/python.exe"
"$PY" -m pip install python-docx openpyxl imageio-ffmpeg numpy # 首次或缺依赖时
SP="C:/Users/huawei/.workbuddy/skills/phrase-audio-splitter/scripts"
[ -d "$SP" ] || SP="C:/Users/huawei/.workbuddy/skills/phrase-audio-splitter__skillhub/scripts"
工作流
第 1 步 读清单,核对条数
"$PY" "<skill>/scripts/tts_build.py" --doc "短语.docx" --dry-run
看前几行:
清单 短语.docx 18 条(原始 55 行 → 有效 36 行 → 合并成 18 条,每条 2.0 行)
声音 en-AU-WilliamNeural William-English (Australia) 正常语速
声音 en-IE-EmilyNeural Emily-English (Ireland) 正常语速
排布 每条 2 遍(男声 → 女声) 两遍间隔 1.00s 条间停顿 1.80s
脚本 634 字符/轨 2 次请求
18 条要和文档里肉眼数的短语数一致。不一致说明标题行/中文释义行被误收或短语被切断,先修文档。每条 2.0 行是提示:文档确实是一短语两行,两行分别喂给两个声音,不是重复四遍。- 支持
.docx(首选)、.xlsx、.txt、.csv;旧版.doc先另存为.docx。 - 这一步不发请求、几秒钟,别跳过。
第 2 步 生成朗读音频(男声 + 女声)
"$PY" "<skill>/scripts/tts_build.py" \
--doc "短语.docx" --verify --name "必修一Unit2" --outdir outputs
读这几行再往下走:
生成 William 请求中…
William 52.6s 0.30MB 用时 7.1s
生成 Emily 请求中…
Emily 56.0s 0.32MB 用时 7.4s
切段 两轨识别 18 / 18 段,清单 18 条 -> 对齐
音频 outputs/必修一Unit2_William+Emily.mp3 86.5s 1.32MB 平均每条 4.80s
校验 两遍之间 1.04~1.09s(目标 1.0s,18 处) 条与条 1.86~1.91s(目标 1.8s,17 处)
预检 切分器识别单元 18 个,清单 18 条 -> 期望 18 条(每条含男女两遍) 对齐正常
切段 18/18两轨都识别对了才往下走;⚠ 不一致会自动转逐条兜底(每条约 2 次请求,慢但稳)。- 结尾「对齐正常」→ 进第 3 步;
⚠ 条数不一致(退出码 3)→ 同一条命令加--per-phrase重来。 平均每条 < 1.5s→ 可能有短语被漏读,--dry-run看脚本。- 校验值比目标略长 0.04~0.09s 属正常:silencedetect 会把语音收尾的衰减也算成静音。
第 3 步 切分编号并打包
"$PY" "$SP/split_audio.py" \
--audio "outputs/必修一Unit2_William+Emily.mp3" \
--doc "短语.docx" \
-o outputs --name "必修一Unit2" --merge 1.52 --repeats 1
--merge 1.52 是关键:告诉切分器「1.52 秒以内的停顿算句中、不要断」,于是每条的两遍
被整块切下来。--repeats 1 明确不拆遍数。读自检那行:
切分 18 个文件 时长 2.22-4.29s 总计 59.4s
自检 静音文件 0 过短 0 内部停顿偏长 18 异常长 0
交付 outputs/必修一Unit2_短语音频18条.zip
内部停顿偏长等于条数是正常的:每条中间本来就有 1.0s 的男女分隔,超过切分器 0.75s 的报警线。
看「可能粘连」里列出的秒数——若是 0.98~1.00 就是这条正常的男女停顿;若冒出 1.8 左右的,
才说明两条被并成一条。真正要卡的是静音文件过短异常长三项必须为 0。异常长非 0 → 两条被并成一条,把--gap加到 2.2 重新生成(--merge会跟着自动变,用脚本打印的新命令)。- 用了
--per-phrase时第 2 步已产出整段 mp3,本步照常执行即可。
第 4 步 交付
present_files 给出 zip(必要时连同短语 Word 一起),正文写清:条数、两遍音色(先 William 澳洲男声后 Emily 爱尔兰女声)、两遍间隔 1.0s、时长范围、自检结果。散装 mp3 在 …_短语音频/ 里,不必逐个列出。
声音
| 说法 | 传入 | 站点声音 |
|---|---|---|
| 默认搭档(男 → 女) | --voices william,emily |
William(AU) + Emily(IE) |
| 女声在前 | --voices emily,william |
Emily(AU) + William(IE) |
| 只要一个人读一遍 | --voices william |
单轨单遍,切分不加 --merge |
| 站点里别的音色 | --voices en-GB-RyanNeural,emily |
直接给完整 id |
| 想看看还有哪些 | --list-voices |
打印免费库常用英文音色 |
默认就是 william,emily,用户没指定时不要改。免费声音不需要账号、不消耗积分。
常用参数
| 需求 | 做法 |
|---|---|
| 两遍之间停久一点 / 短一点 | --intra 1.5 / --intra 0.8(切分阈值自动跟着变,用脚本打印的命令) |
| 条间停顿更长(更保险) | --gap 2.2 |
| 慢一点,适合跟读 | --rate -15(默认 0 = 正常语速) |
| 只要一个声音读一遍 | --voices william,切分去掉 --merge |
| 整段对齐总是差一两条 | --per-phrase 逐条生成 |
| 材料太长(>9000 字符/轨) | 拆成两份分别跑,或 --per-phrase |
| 只想看脚本不发请求 | --dry-run |
排错
403 Forbidden——请求头缺 User-Agent,脚本已带;手工 curl 时记得 -A。
Invalid input / 400——文本超长或声音 id 不存在,先 --list-voices 核对。
免费额度用尽 / 需要积分——两个声音都在免费库,稍后再试;接口返回网页而不是音频时脚本会明确报错。
单次请求要几分钟——服务端排队,别急着中断重试(实测偶发 3 分钟,最终会返回正常音频)。
条数总是差一条——某条被漏读;--dry-run 对比脚本与清单,改写那条再生成。
开头吞音 / 第一个音没发出来——v1.3.0 之前的老包才有这问题(切在 -35dB 越阈点上、又用 -45dB 硬削头)。
现版本切段起点在停顿里、裁剪阈值 -60dB、每遍前后留 50ms。若仍遇到,先量一下:交付切片第一遍的语音长度
应与「单独把这一句合成一次」等长,差超过 20ms 才算异常,此时把 --intra 加 0.2 重生成。
一切看起来都对但切出来只有男声——切分时漏了 --merge,切分器会按「每遍」拆开并只留第一遍。
接口细节、发现过程与重新探测方法见 references/realdubbing.md(接口变更、或要换站点时读它)。
注意
- 别自己手搓 TTS 文本或手工拼音频:一定走
tts_build.py,它和切分器共用同一份清单解析器,保证「念的条数」与「对齐的条数」一致。 - 别跳过
--verify:几秒钟,能避免切完 18 个文件才发现全错位。 - 切分命令里的
--merge必须用第 2 步打印的那个值,别沿用旧任务的 0.99(那是 0.6s 间隔年代的)。 - 改动
--intra/--gap后不要手改阈值:脚本会按intra + (gap-intra)*0.65自动算,照打印的命令切。 - 中间产物放在输出目录随任务走,不要写进技能目录。