Hurston-1 是一类支持多说话人、多口音英语合成的开源 TTS 项目。把它跑在本地,好处是音频不出内网、可以批量跑长文、可以针对口音做细粒度挑选。这篇教程按"装起来 → 挑音色 → 处理长文 → 拼接调优"的顺序走一遍,命令尽量写成可以直接照抄的形式,涉及具体版本号与权重名的地方,一律以官方文档当前版本为准。
适用场景
适合需要批量生成带地域口音英语语音的场景,比如播客旁白、有声读物、课程配音、客服话术试听。也适合数据不能离开内网、或者需要对同一段文本反复对比不同口音的团队。如果你只是偶尔合成一两句话,直接用在线 API 更省事,本地部署的价值在于可控、可批量、可复现。
环境与前置条件
- 操作系统:Linux(Ubuntu 一类的主流发行版)最省心;macOS 可用 CPU 或 Apple Silicon 加速;Windows 建议走 WSL2,避免路径与依赖问题。
- Python:3.10 及以上,具体下限看项目
pyproject.toml或requirements.txt。 - 内存:建议 16 GB 起。加载模型时会有明显的瞬时峰值。
- 显存:CPU 也能推理,但长文会明显变慢。有 NVIDIA GPU 时建议 8 GB 以上,开启半精度可以进一步降低占用。显存不足时优先缩短单块文本长度。
- 磁盘:模型权重加缓存预留 10–20 GB。长文合成会产出大量 wav,建议单独挂一个数据盘。
- 系统依赖:
git、git-lfs(权重通常用 LFS 托管)、ffmpeg(读写音频、转码)。
一句话检查:
```bash
python3 --version && git --version && git lfs version && ffmpeg -version | head -n 1
```
四条都有输出即视为环境备齐。
分步骤部署
第 1 步:安装系统依赖
```bash
Debian / Ubuntu 系
sudo apt update
sudo apt install -y git git-lfs ffmpeg libsndfile1 build-essential
git lfs install
macOS
brew install git git-lfs ffmpeg libsndfile
git lfs install
```
libsndfile1 是 soundfile 读写 wav 的底层依赖,缺了会在导入时直接报错。git lfs install 只需执行一次,它是给当前用户装钩子。
第 2 步:创建虚拟环境
```bash
python3 -m venv ~/venvs/hurston
source ~/venvs/hurston/bin/activate
python -m pip install -U pip setuptools wheel
```
所有后续操作都在这个 venv 里进行。装完后 which python 应指向 ~/venvs/hurston/bin/python,这是判断是否真的激活成功的可靠方式。
第 3 步:安装 PyTorch
PyTorch 的版本必须和本机 CUDA 驱动匹配。不要去猜索引地址,直接打开 PyTorch 官方安装页面,按"你的系统 + 包管理器 + CUDA 版本"选出来的那条命令粘贴执行即可。CPU 环境则选 CPU 版本。
装完立刻验证:
```bash
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
```
输出里 True 表示 GPU 可用。如果是 False 但机器有卡,多半是驱动或 CUDA 版本不匹配,先解决这个再往下走。
第 4 步:获取代码与权重
```bash
cd ~
git clone <Hurston-1 官方仓库地址> hurston-1
cd hurston-1
pip install -r requirements.txt
```
仓库地址、依赖清单和权重下载方式都以官方文档当前版本为准。部分项目会把权重单独发布,需要按 README 的说明下载后放到约定的 checkpoints/ 或 models/ 目录。下载完检查文件大小,如果只有几百字节,基本是 LFS 指针没拉下来,执行:
```bash
git lfs pull
```
第 5 步:跑通最小推理
先不要碰长文,用一句最短的英文确认整条链路通:
```bash
python -m <项目提供的推理入口> \
--text "Hello, this is a short test." \
--voice <音色名> \
--output ./out/test.wav
```
不同项目的入口和参数名不一样,--voice 也可能叫 --speaker、--speaker_id。以 --help 输出为准:
```bash
python -m <推理入口> --help
```
成功标志是 ./out/test.wav 存在且能正常播放。
第 6 步:列出并挑选口音音色
多说话人模型一般会在配置里维护一份音色表,字段通常包含音色名、性别、地区/口音标签。先把表打出来:
```bash
方式一:项目自带的列表命令(推荐)
python -m <推理入口> --list-voices
方式二:直接看配置文件
cat configs/voices.* 2>/dev/null || ls configs/
```
挑口音不要靠标签硬信,做一次"试听矩阵"最靠谱:固定同一句包含易暴露口音的文本,把所有候选音色各跑一遍。
```bash
TEXT="I parked the car down by the river and walked over the bridge."
for v in $(python -m <推理入口> --list-voices --quiet); do
python -m <推理入口> --text "$TEXT" --voice "$v" --output "./audition/${v}.wav"
done
```
试听时重点听三处:元音是否走样(如 parked 的 r 音处理)、句尾语调是否上扬、连读是否自然。把通过初筛的 3–5 个音色记到一张表里,后面长文只用这几个。同一篇长文务必只用一个音色,中途换音色是最容易被听出来的破绽。
第 7 步:长文分块
TTS 模型对单次输入长度有限制,长文必须切。切分原则是在句子边界切,不在句中切,因为模型看到句号才会收尾降调,看到逗号会保持上扬。
```python
split.py
import re
SENT_END = re.compile(r'(?<=[.!?])\s+')
def split_text(text, max_chars=300):
sents = [s.strip() for s in SENT_END.split(text.strip()) if s.strip()]
chunks, cur = [], ""
for s in sents:
if len(cur) + len(s) + 1 <= max_chars:
cur = (cur + " " + s).strip()
continue
if cur:
chunks.append(cur)
单句仍然超长时,退到逗号处切
while len(s) > max_chars:
cut = s.rfind(",", 0, max_chars)
if cut == -1:
cut = max_chars
chunks.append(s[:cut + 1].strip())
s = s[cut + 1:].strip()
cur = s
if cur:
chunks.append(cur)
return chunks
```
max_chars 建议从 200–400 起步,具体上界以模型支持的输入长度为准。块太短会让语调碎,太长会增加显存压力和前后不一致的概率。
调用前先做文本规范化:把数字、货币、缩写、专有名词统一写法定型。同一个 Dr. 在块 A 里读成 "doctor"、块 B 里读成 "drive",拼起来就是明显的瑕疵。
```bash
python - <<'PY'
from split import split_text
text = open("script.txt", encoding="utf-8").read()
chunks = split_text(text, max_chars=300)
for i, c in enumerate(chunks):
open(f"chunks/{i:04d}.txt", "w", encoding="utf-8").write(c)
print(len(chunks), "chunks")
PY
```
第 8 步:批量合成与拼接
```python
synth_and_join.py
import os, subprocess, numpy as np, soundfile as sf
GAP_SENT, GAP_PARA = 0.25, 0.6
def synth(txt_path, wav_path, voice):
text = open(txt_path, encoding="utf-8").read()
subprocess.run([
"python", "-m", "<推理入口>",
"--text", text, "--voice", voice, "--output", wav_path,
], check=True)
def join(paths, out):
parts, sr = [], None
for p in paths:
data, s = sf.read(p, dtype="float32")
if data.ndim > 1:
data = data.mean(axis=1) # 统一为单声道
sr = s
parts.append(data)
gap = GAP_PARA if p.endswith("_para.wav") else GAP_SENT
parts.append(np.zeros(int(sr * gap), dtype=np.float32))
sf.write(out, np.concatenate(parts), sr)
```
所有块必须采样率一致,否则拼接会出现变速或爆音。采样率不一致时先用 ffmpeg -ar <目标采样率> 统一转一遍再拼。
第 9 步:拼接处的韵律一致性调校
这是效果差异最大的一步,具体做这几件事:
1. 固定音色与语速:全程同一个 voice、同一组 speed/pitch 参数,不要在块之间调。
2. 块长尽量均匀:都在 200–350 字符之间,避免出现一个 30 字符的碎块紧接一个 400 字符的长块——模型对长短输入的韵律处理并不一致。
3. 让每块以句号结尾:如果切出来是逗号结尾,宁可多带一句过去,也别把逗号留在块尾,否则块尾会升调,下一块又从平调起,听感像换人。
4. 段间静音大于句间静音:句间 0.2–0.3 秒、段间 0.5–0.8 秒,接近真实朗读的停顿节奏。
5. 块首块尾去空格与引号:多余标点会被读成额外停顿。
6. 若有随机采样,固定随机种子:让同一批任务可复现。
7. 末尾统一做响度归一化:各块音量本身可能就有几个 dB 的差异,用 ffmpeg 的 loudnorm 滤镜整段过一遍,比逐块处理更稳。
```bash
ffmpeg -i joined.wav -af loudnorm=I=-16:TP=-1.5:LRA=11 -ar 44100 final.wav
```
验证部署是否成功
按顺序跑这三条:
```bash
1. GPU 与基础依赖
python -c "import torch, soundfile; print(torch.cuda.is_available(), soundfile.__version__)"
2. 音频参数
ffprobe -v error -show_entries stream=sample_rate,channels,duration \
-of default=nw=1 final.wav
```
预期:第一条输出 True 和一个版本号;第二条输出采样率(以模型输出为准,常见 22050 或 24000)、声道数 1、时长与你文本量大致匹配。再听一遍成品,检查三点:块与块之间有没有明显的"重新起头"感、句尾有没有异常的升调、整体音量是否平稳。三项都过,部署就算完成。
常见报错与解决
1. torch.cuda.OutOfMemoryError: CUDA out of memory
→ 原因为单块文本过长或显存被其他进程占用。
→ 解决:把 max_chars 降到 200 以下、开启半精度、关闭占卡进程;仍不够时临时设置分配策略:
```bash
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
```
2. KeyError: '<音色名>' 或 Unknown speaker
→ 原因:音色名与配置里的键不一致,或者大小写、连字符写法有差异。
→ 解决:先列出可用音色,照抄输出里的名字:
```bash
python -m <推理入口> --list-voices
```
3. FileNotFoundError: ffmpeg 或 sndfile 相关报错
→ 原因:系统缺少音频处理工具,或装了但不在 PATH 里。
→ 解决:
```bash
sudo apt install -y ffmpeg libsndfile1
which ffmpeg
```
4. torch not compiled with CUDA enabled 或 libcudart.so 找不到
→ 原因:装的 PyTorch 是 CPU 版本,或与驱动的 CUDA 版本不匹配。
→ 解决:回到 PyTorch 官方安装页面,按本机 nvidia-smi 显示的 CUDA 版本重装,装完重新验证 torch.cuda.is_available()。
5. 权重文件只有几百字节,推理时报权重格式错误
→ 原因:仓库用了 Git LFS,但没拉取真实文件。
→ 解决:
```bash
git lfs install && git lfs pull
ls -lh models/ # 确认文件大小正常
```
6. 输出是静音或纯噪声
→ 原因:文本为空、采样率设置与模型输出不符,或输入里混入了不可见字符。
→ 解决:先 cat -A 检查块文本,确认非空且只有可见 ASCII;再用最小输入 "Hello." 复现一次,排除文本因素。
后续维护
- 锁依赖:首次跑通后立刻
pip freeze > requirements.lock,之后升级先在新 venv 里试,不要直接改生产环境。 - 备份三类东西:音色筛选表、文本规范化规则(数字/缩写映射)、拼接参数(块长、静音时长、响度目标)。这三样丢了,重跑出来的音频风格会变,返工成本高于模型本身。
- 日志:每块记录字符数、耗时、是否失败。据此算实时率(生成秒数 / 音频秒数),数值明显上升通常意味着显存吃紧或系统在换页。
- 断点续跑:批量任务按块编号落盘,已存在的 wav 直接跳过,长任务中断后不用从头再来。
- 磁盘清理:中间块文件体积不小,成品验收后清理
chunks/与out/,只保留最终 wav 和脚本。 - 升级策略:模型或依赖升级后,用固定的 3 句测试文本 + 固定的 2 个音色重跑一次 A/B,确认口音和韵律没有明显漂移再全量切换。
