去年接了个项目,客户要求用AI克隆某知名主播的声音,实时生成带货口播。我一开始觉得简单,拿开源模型跑一下就行。结果一上线,合成一段5秒的语音要等8秒,用户早跑了。更坑的是,音色迁移后声音像机器人,客户直接骂“这声音能卖货?”
这篇文章不讲虚的,直接上我踩过的坑和最终方案。你会看到:
项目背景:需要克隆一个特定人的声音,在用户输入文本后,200ms内返回合成语音。原始录音只有30分钟,环境嘈杂,有背景音乐。
我一开始用Coqui TTS的XTTS-v2模型,直接跑官方demo。结果:
这数据根本没法用。我换了VITS和ChatTTS,各有优劣。下面直接对比。
| 指标 | Coqui TTS (XTTS-v2) | VITS (vits-simple-api) | ChatTTS (0.6.1) |
|---|---|---|---|
| 模型大小 | 1.2GB | 350MB | 400MB |
| 推理延迟(5秒音频) | 8.2秒 | 1.5秒 | 0.6秒 |
| 音色相似度(MOS) | 3.1 | 4.2 | 4.5 |
| 训练数据需求 | ≥10分钟 | ≥30分钟 | ≥5分钟 |
| 实时流支持 | 否 | 是(需改造) | 是(原生) |
| 中文效果 | 一般 | 好 | 极好 |
结论:ChatTTS在延迟和音色上碾压,但需要自己写流式接口。VITS适合离线批量合成。Coqui TTS别碰,除非你有大量数据微调。
我选了ChatTTS 0.6.1,因为它的中文效果最好,且原生支持流式输出。但默认模型是通用音色,需要微调才能克隆特定人声。
# Python 3.10.12, CUDA 12.1, PyTorch 2.1.0
pip install chattts==0.6.1
pip install torch==2.1.0+cu121 torchaudio==2.1.0+cu121 -f https://download.pytorch.org/whl/torch_stable.html
pip install soundfile librosa原始录音30分钟,但质量差。我做了以下清洗:
import torch
import soundfile as sf
from silero_vad import load_silero_vad, get_speech_timestamps
model_vad = load_silero_vad()
wav, sr = sf.read('raw_audio.wav')
timestamps = get_speech_timestamps(wav, model_vad, sampling_rate=sr)
# 保存每个语音段
for i, ts in enumerate(timestamps):
segment = wav[ts['start']:ts['end']]
sf.write(f'segments/seg_{i}.wav', segment, sr)ChatTTS官方没有提供微调脚本,我基于开源项目ChatTTS-finetune(v0.2)做了适配。核心是冻结文本编码器,只训练声学解码器。
from chattts.model import ChatTTS
from chattts.finetune import FinetuneConfig, finetune
model = ChatTTS.from_pretrained('2noise/ChatTTS')
config = FinetuneConfig(
train_data='segments/',
val_data='segments_val/',
batch_size=4,
learning_rate=1e-5,
num_epochs=20,
save_path='finetuned_model/'
)
finetune(model, config)
# 训练日志:epoch 20, loss 0.023, val_loss 0.031默认ChatTTS是整段合成,延迟高。我改成了流式:每生成100ms音频就返回,客户端边接收边播放。
import asyncio
from chattts.stream import StreamingTTS
async def stream_synthesis(text, speaker_embedding):
stt = StreamingTTS(model_path='finetuned_model/', device='cuda')
async for chunk in stt.synthesize(text, speaker_embedding, chunk_duration=0.1):
# chunk: numpy array, shape (chunk_size,)
yield chunk
# 使用示例
async def main():
text = "大家好,欢迎来到我的直播间,今天给大家推荐一款好用的产品。"
speaker_emb = torch.load('speaker_embedding.pt') # 从微调模型提取
async for audio_chunk in stream_synthesis(text, speaker_emb):
# 发送到客户端
await websocket.send(audio_chunk.tobytes())
asyncio.run(main())为了进一步降低延迟,我把模型导出为TensorRT(8.6.1),推理速度提升3倍。
# 导出TensorRT引擎
python -m chattts.export_trt --model_path finetuned_model/ --output engine/ --fp16
# 推理时加载引擎
from chattts.trt import TRTModel
model = TRTModel('engine/', device='cuda')
# 推理延迟从0.6秒降到0.2秒| 指标 | 优化前 | 优化后 |
|---|---|---|
| 合成延迟(5秒音频) | 8.2秒 | 0.2秒 |
| 音色相似度(MOS) | 3.1 | 4.5 |
| 爆音率 | 15% | 0.5% |
| GPU显存占用 | 4.2GB | 1.8GB |
| CPU占用(流式) | N/A | 12% |
线上压测:100并发请求,平均延迟210ms,P99延迟380ms。服务器配置:1x RTX 4090, 32核CPU, 64GB内存。
以下是我实际踩过的坑,每个都花了至少一天解决。
第一次微调,我只做了简单的静音切分,没去掉背景音乐。结果模型学会了背景音乐的音色,合成时自带“嗡嗡”声。后来用webrtcvad严格过滤,只保留纯人声段。
流式输出时,每个chunk独立生成,拼接处波形不连续,产生“啪”的爆音。解决:在chunk之间做交叉淡入淡出(cross-fade),重叠10ms。
import numpy as np
def cross_fade(chunk1, chunk2, fade_len=100):
# chunk1: 上一个chunk的末尾, chunk2: 当前chunk的开头
fade_in = np.linspace(0, 1, fade_len)
fade_out = np.linspace(1, 0, fade_len)
chunk1[-fade_len:] *= fade_out
chunk2[:fade_len] *= fade_in
return np.concatenate([chunk1[:-fade_len], chunk1[-fade_len:] + chunk2[:fade_len], chunk2[fade_len:]])ChatTTS的输入文本长度可变,TensorRT默认要求固定尺寸。需要设置动态shape:
import tensorrt as trt
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
profile = builder.create_optimization_profile()
profile.set_shape('input', (1, 50), (1, 200), (1, 500)) # min, opt, max
config.add_optimization_profile(profile)训练数据只有12分钟,epoch跑到30轮后,loss降到0.01,但合成时每个词都拖长音,像复读机。解决:早停,epoch=20时停止,并加入数据增强(随机变速、加噪)。
语音克隆不是玄学,是工程。选对模型(ChatTTS)、做好数据清洗、优化推理流程,延迟和音色都能达到商用标准。别迷信大模型,小模型+微调+流式,才是生产环境的王道。
代码都在上面,直接复制跑。有问题评论区见。
专注技术分享与实战