OmniVoice本地部署实现音色克隆和语音合成

OmniVoice本地部署实现音色克隆和语音合成

_

前言

上次hermes接入了minimax token plan plus 对接到QQ,实现了音色克隆和语音合成,小助手能使用我喜欢的角色的声音发语音,简直不要太棒

https://www.xrilang.top/archives/shi-jian-minimaxyin-se-ke-long-he-yu-yin-sheng-cheng

美中不足是——克隆音色不包含在套餐,然后克隆一次好像十块钱。

于是打算在本地试试部署模型实现音色克隆和语音合成。

本文包含两大内容。

(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)
  1. Whisper ASR:将参考音频转录成文本(如果未提供 ref_text)

  2. 音频特征提取:从参考音频中提取说话人的音色特征

  3. 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)

关键参数说明

参数

类型

说明

ref_audio

str/Path

参考音频文件路径

ref_text

str/None

参考音频文本,None 则自动用 Whisper 转录

preprocess_prompt

bool

是否自动预处理(去除静音、清理标点)

最终得到.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 生成模型] → 输出音频
  1. 加载 .pt 文件:读取之前保存的音色提示

  2. 调用 generate:将文本和音色提示传入模型

  3. 保存音频:生成 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() 关键参数详解

参数

类型

默认值

说明

text

str

必填

要合成的文本内容

language

str

"Chinese"

语言:Chinese/English/Japanese 等

voice_clone_prompt

VoiceClonePrompt

必填

音色提示(.pt 加载的)

speed

float

1.0

语速,1.0 正常,0.5 慢一倍,2.0 快一倍

num_step

int

32

扩散步数,32 平衡,16 快但质量低,64 慢但质量高

guidance_scale

float

2.0

引导强度,2.0 平衡,1.0 更自然但可能偏离文本,5.0 更贴近文本但可能机械

duration

float

None

固定输出时长(秒),与 speed 二选一

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的音色克隆和语音合成。

https://github.com/mllt992/xrilang-voice-clone-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

开源模型新王登基:Qwen3.6-35B-A3B vs Gemma-4-E4B 全面对比 2026-04-18

评论区