
前言
上次hermes接入了minimax token plan plus 对接到QQ,实现了音色克隆和语音合成,小助手能使用我喜欢的角色的声音发语音,简直不要太棒
美中不足是——克隆音色不包含在套餐,然后克隆一次好像十块钱。
于是打算在本地试试部署模型实现音色克隆和语音合成。
本文包含两大内容。
(1)本地部署模型,配置,并简单实现语音克隆和合成
(2)基于上述成果,做成服务,方面使用。(Github代码见文末)
关于OmniVoice
OmniVoice — 600+语言零样本TTS
来源:小米下一代 Kaldi 团队(k2-fsa)开源项目
GitHub:github.com/k2-fsa/OmniVoice
Hugging Face:有预训练模型
https://huggingface.co/k2-fsa/OmniVoice
核心能力

完全开源可部署到本地,支持零样本(Zero-shot)语音克隆,覆盖语言数量远超同类商业方案
环境准备
(1)ffmpeg
你可以自行下载ffmpeg。也可以通过下方命令下载
winget install ffmpeg --accept-source-agreements --accept-package-agreements然后你可以输入
where ffmpeg你就知道下载到了哪个位置。比如我查出来位置是
C:\Users\xrilang\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-8.1-full_build\bin\ffmpeg.exe
(2)创建环境安装依赖
我使用的是conda创建的环境
conda create -n omnivoice python=3.10
conda activate omnivoice在此我补充说明:我的电脑具有显卡RTX 3060(6GB)
激活环境后安装依赖
pip install torch==2.8.0+cu128 torchaudio==2.8.0+cu128 --extra-index-url https://download.pytorch.org/whl/cu128这个命令是在OmniVoice的github仓库文档中看到的
安装完毕后测试一下
python -c "import torch; print('CUDA:', torch.cuda.is_available()); print('GPU:', torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'None')"
如果你不用显卡的话,可以试试
pip install torch==2.8.0 torchaudio==2.8.0
好吧,继续,比较关键的依赖
pip install omnivoice代码编写
(1)克隆并生成音色.pt文件
.pt文件是什么? .pt 文件是 PyTorch 模型的标准格式,全称是 PyTorch checkpoint。

OmniVoice 的音色克隆流程如下:
参考音频 → [Whisper ASR 转文本] → [音频特征提取] → VoiceClonePrompt (.pt)Whisper ASR:将参考音频转录成文本(如果未提供 ref_text)
音频特征提取:从参考音频中提取说话人的音色特征
VoiceClonePrompt:包含音色特征 + 文本信息的复合对象
最小示例代码如下:
from omnivoice import OmniVoice, VoiceClonePrompt
# 1. 加载模型(首次会自动下载)
model = OmniVoice.from_pretrained("k2-fsa/OmniVoice")
# 2. 创建音色提示(传入参考音频,自动转录并提取特征)
voice_prompt = model.create_voice_clone_prompt(
ref_audio="你的参考音频.mp3", # 参考音频路径
ref_text=None, # 不提供文本,则自动用 Whisper 转录
preprocess_prompt=True, # 自动预处理:静音去除、标点清理
)
# 3. 保存为 .pt 文件(下次直接加载,无需重复处理)
voice_prompt.save("音色名称.pt")
print("音色已保存到 音色名称.pt")注意:model = OmniVoice.from_pretrained("k2-fsa/OmniVoice") 首次需要从hugging face下载模型,需要配置HF_TOKEN。
import os # ⚠️ 必须在导入 transformers 之前设置 os.environ['HF_TOKEN'] = 'hf_PJScZDqlHUbx*****lFWFLCVdU'HF_TOKEN怎么获得?
到时候加载已保存的音色可以使用
voice_prompt = VoiceClonePrompt.load("音色名称.pt")参考代码如下:
import os
from pathlib import Path
from omnivoice import OmniVoice, VoiceClonePrompt
os.environ['HF_TOKEN'] = 'hf_PJScZDqlHUbx*****lFWFLCVdU'
def clone_voice(
ref_audio: str | Path,
voice_name: str,
ref_text: str | None = None,
rebuild: bool = False,
) -> dict:
"""
从参考音频克隆音色并保存为 .pt 文件
Args:
ref_audio: 参考音频文件路径(支持 mp3, wav, m4a 等)
voice_name: 音色名称(用于命名 .pt 文件)
ref_text: 参考音频的文本(None = 自动转录)
rebuild: 是否强制重建(True = 忽略缓存重新处理)
Returns:
dict: {"success": bool, "voice_id": str, "pt_path": str, "message": str}
"""
ref_audio = Path(ref_audio)
# 检查参考音频是否存在
if not ref_audio.exists():
return {
"success": False,
"voice_id": voice_name,
"pt_path": "",
"message": f"参考音频不存在: {ref_audio}",
}
# .pt 文件保存路径
pt_path = Path("voices") / f"{voice_name}.pt"
pt_path.parent.mkdir(parents=True, exist_ok=True)
# 加载 OmniVoice 模型
model = OmniVoice.from_pretrained("k2-fsa/OmniVoice")
try:
# 如果缓存存在且不需要重建,直接加载
if pt_path.exists() and not rebuild:
print(f"加载缓存音色: {pt_path}")
prompt = VoiceClonePrompt.load(pt_path)
else:
# 创建新的音色提示
print(f"从音频创建音色: {ref_audio}")
prompt = model.create_voice_clone_prompt(
ref_audio=str(ref_audio),
ref_text=ref_text, # None 则自动转录
preprocess_prompt=True, # 自动预处理
)
# 保存到 .pt 文件
prompt.save(pt_path)
print(f"音色已保存: {pt_path}")
return {
"success": True,
"voice_id": voice_name,
"pt_path": str(pt_path),
"message": "音色克隆成功",
}
except Exception as e:
return {
"success": False,
"voice_id": voice_name,
"pt_path": str(pt_path),
"message": f"克隆失败: {str(e)}",
}
# 使用示例
if __name__ == "__main__":
result = clone_voice(
ref_audio="C:/Users/xrilang/Desktop/voice/yunli.mp3",
voice_name="yunli",
ref_text=None, # 自动转录
)
print(result)关键参数说明
最终得到.pt文件的文件结构:
.pt 文件是一个包含以下信息的序列化对象:
VoiceClonePrompt {
speaker_embedding: Tensor # 说话人嵌入向量(音色特征)
language: str # 语言标识
ref_text: str # 参考文本
ref_audio_path: str # 参考音频路径
duration: float # 参考音频时长
sample_rate: int # 采样率
}这个 .pt 文件包含了复现音色所需的全部信息,后续合成时直接加载即可。
常见问题
Q: 为什么 .pt 文件比参考音频小很多?
A: .pt 保存的是提取的音色特征(一个向量),不是音频数据本身。通常只有几 KB。
Q: 需要多长的参考音频?
A: 建议 10-30 秒。太短可能提取不到完整音色特征,太长无必要。
Q: 每次合成都要重新处理参考音频吗?
A: 不需要。创建 .pt 后,后续直接 VoiceClonePrompt.load("xxx.pt") 即可。
(2)语音合成
语音合成的流程:
输入文本 + VoiceClonePrompt (.pt) → [OmniVoice 生成模型] → 输出音频加载 .pt 文件:读取之前保存的音色提示
调用 generate:将文本和音色提示传入模型
保存音频:生成 wav 文件
最小示例代码
from omnivoice import OmniVoice, VoiceClonePrompt
import soundfile as sf
# 1. 加载模型
model = OmniVoice.from_pretrained("k2-fsa/OmniVoice")
# 2. 加载音色(之前保存的 .pt 文件)
voice_prompt = VoiceClonePrompt.load("yunli.pt")
# 3. 合成语音
audios = model.generate(
text="要合成的文本内容",
language="Chinese", # 语言
voice_clone_prompt=voice_prompt, # 音色
)
# 4. 保存为 wav 文件
sf.write("输出音频.wav", audios[0], model.sampling_rate)
print("合成完成!")完整合成函数
from omnivoice import OmniVoice, VoiceClonePrompt
import soundfile as sf
from datetime import datetime
def synthesize(
text: str,
voice_id: str,
language: str = "Chinese",
output_filename: str = None,
speed: float = 1.0,
num_step: int = 32,
guidance_scale: float = 2.0,
) -> dict:
"""
使用指定音色合成语音
Args:
text: 要合成的文本内容
voice_id: 音色 ID(对应 voices/ 目录下的 .pt 文件)
language: 语言(Chinese/English/Japanese 等)
output_filename: 输出文件名(None = 自动生成带时间戳的文件名)
speed: 语速 (0.5 - 2.0)
num_step: 扩散步数(越多越慢,质量越高)
guidance_scale: 引导 scale (1.0 - 5.0)
Returns:
dict: {"success": bool, "audio_path": str, "sample_rate": int, "message": str}
"""
pt_path = f"voices/{voice_id}.pt"
# 检查 .pt 文件是否存在
import os
if not os.path.exists(pt_path):
return {
"success": False,
"audio_path": "",
"sample_rate": 0,
"message": f"音色文件不存在: {voice_id}.pt",
}
try:
# 1. 加载音色
voice_prompt = VoiceClonePrompt.load(pt_path)
# 2. 加载模型
model = OmniVoice.from_pretrained("k2-fsa/OmniVoice")
# 3. 生成语音
audios = model.generate(
text=text,
language=language,
voice_clone_prompt=voice_prompt,
speed=speed,
num_step=num_step,
guidance_scale=guidance_scale,
)
# 4. 保存音频
if output_filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_filename = f"synth_{voice_id}_{timestamp}.wav"
sf.write(output_filename, audios[0], model.sampling_rate)
return {
"success": True,
"audio_path": output_filename,
"sample_rate": model.sampling_rate,
"message": "语音合成成功",
}
except Exception as e:
return {
"success": False,
"audio_path": "",
"sample_rate": 0,
"message": f"合成失败: {str(e)}",
}
# 使用示例
if __name__ == "__main__":
result = synthesize(
text="你好,这是语音合成测试。",
voice_id="yunli",
language="Chinese",
speed=1.0,
)
print(result)generate() 关键参数详解
audios = model.generate(...) # 返回 numpy 数组列表 # audios[0] 是第一个(也是唯一)音频 # shape: (样本数,) 即 (duration * sample_rate,) print(audios[0].shape) # e.g., (48000,) 表示 1 秒音频(采样率 48000)
保存音频
import soundfile as sf
# model.sampling_rate 是模型的采样率(通常是 24000 或 48000)
sample_rate = model.sampling_rate
# 保存为 wav
sf.write("output.wav", audios[0], sample_rate)
# 或保存为其他格式(需要 ffmpeg)
# sf.write("output.mp3", audios[0], sample_rate)一站式合成函数
上面是分步操作,其实可以简化为一个函数:
def synthesize_with_audio(text, ref_audio_path, output_path=None):
"""直接从参考音频合成(自动处理音色创建和加载)"""
from omnivoice import OmniVoice, VoiceClonePrompt
import soundfile as sf
from datetime import datetime
model = OmniVoice.from_pretrained("k2-fsa/OmniVoice")
# 创建音色(自动处理)
prompt = model.create_voice_clone_prompt(ref_audio=ref_audio_path)
# 合成
audios = model.generate(text=text, language="Chinese", voice_clone_prompt=prompt)
# 保存
if output_path is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = f"output_{timestamp}.wav"
sf.write(output_path, audios[0], model.sampling_rate)
return output_path
# 使用
synthesize_with_audio(
text="你好世界",
ref_audio_path="yuli.mp3",
)常见问题
Q: 合成很慢怎么办?
A: 减少 num_step(如改为 16),或降低 guidance_scale(如改为 1.5)。
Q: 合成的声音不像参考音色?
A: 检查 guidance_scale 是否太高(建议 2.0),或尝试提供 ref_text 让音色更准确。
Q: 音频有噪音?
A: 尝试增加 num_step,或使用更清晰的参考音频。
Q: 如何批量合成?
A: 循环调用 model.generate(),复用同一个 voice_clone_prompt:
voice_prompt = VoiceClonePrompt.load("yunli.pt")
for text in texts:
audios = model.generate(text=text, voice_clone_prompt=voice_prompt)
sf.write(f"{text[:10]}.wav", audios[0], model.sampling_rate)整合
经过上述操作,我大致了解了声音克隆和语音合成是个什么事,于是进一步完善一个MVP。
本项目包含以下内容
(1)模型文件文件夹
(2)音色文件文件夹(.pt)
(3)克隆声音,生成.pt的方法
(4)使用.pt文件,根据文字合成语音的方法
(5)fastapi做服务,提供接口。一个是克隆音色接口、音色列表接口、语音合成接口、语音列表接口。
(6)HTML做前端,支持上传文件克隆音色,支持输入文字合成语音
本项目不包含:omnivoice、ffmpeg,需要自行准备哦。
说一下omnivoice,如果你不需要对模型做调整的话,可以不必去github clone ommivoice。直接pip install omnivoice 安装依赖即可。
项目地址:
mllt992/xrilang-voice-clone-ominovoice: 基于ominovoice的音色克隆和语音合成。
git clone https://github.com/mllt992/xrilang-voice-clone-ominovoice.git复制 config.example.py 为 config.py
你可以手动去 k2-fsa/OmniVoice at main 下载模型到models文件夹


不想手动下载的话建议把config.py的HF_TOKEN = "hf_your_token_here" 设置一下,加快模型下载速度。
然后就是准备conda虚拟环境,建议创建虚拟环境名字改为“omnivoice”,这样能自动识别,不然的话就需要你改一下启动脚本启动服务.bat 的内容,以匹配你的环境名称。
克隆音色时,如果参考文件是mp3,可能会遇到问题:缺少 liblzma.dll(XZ 压缩库)
可以执行
conda install -c conda-forge xz启动项目,访问127.0.0.1:8000
