使用的硬件是RTX4060,环境使用uv管理,需要安装ffmpeg来转换格式(m4a->wav),使用的模型是VoxCPM2 · 模型库
依赖:
[project]
name = "tts"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"voxcpm>=2.0.3",
"torch==2.13.0+cu126",
"torchaudio==2.11.0+cu126",
"pilk>=0.2.4",
]
[tool.uv.sources]
torch = { index = "pytorch-cu126" }
torchaudio = { index = "pytorch-cu126" }
[[tool.uv.index]]
name = "pytorch-cu126"
url = "https://download.pytorch.org/whl/cu126"
explicit = true
提前准备好使用windows录制的音频(.m4a)和录音对应的文本(.txt),文件名保持一致,参考音频最好纯净并保持在5~30秒
执行uv run python clone_voice.py --ref-audio 文件名.m4a --text "妈的,这也太像了" --output 输出文件名.wav
即可得到克隆后的音频
"""VoxCPM2 终极克隆(Ultimate / Hi-Fi Cloning)脚本。
终极克隆 = 参考音频 + 精确转写文本(prompt_wav_path + prompt_text +
reference_wav_path 指向同一段音频),最大程度还原音色与发声细节。
用法(最简,自动读取同名 .txt 作为转写):
uv run python clone_voice.py \
--ref-audio ref_speak.m4a \
--text "要合成的目标文本" \
--output clone.wav
# ref_speak.m4a 的转写自动从 ref_speak.txt 读取。
显式指定转写:
uv run python clone_voice.py \
--ref-audio ref_speak.m4a \
--prompt-text "参考音频的精确转写文本" \
--text "要合成的目标文本"
参考音频建议 5–30 秒、干净无噪声;转写文本必须与音频逐字一致。
非 wav/flac 格式(m4a/mp3 等)自动用 ffmpeg 转为 16kHz 单声道 wav。
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
import soundfile as sf
from voxcpm import VoxCPM
DEFAULT_MODEL_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "VoxCPM2")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="VoxCPM2 终极克隆:参考音频 + 精确转写文本,最高保真度克隆音色。",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--ref-audio", required=True, help="参考音频路径(wav/flac/m4a/mp3,5-30 秒)")
parser.add_argument(
"--prompt-text",
default=None,
help="参考音频的精确转写文本;省略时自动读取 <ref-audio 同名>.txt",
)
parser.add_argument("--text", required=True, help="要合成的目标文本")
parser.add_argument("--output", default="clone.wav", help="输出音频路径")
parser.add_argument("--model-dir", default=DEFAULT_MODEL_DIR, help="本地 VoxCPM2 模型目录")
parser.add_argument("--cfg", type=float, default=2.0, help="CFG 引导强度(1.0-3.0,越高越贴合文本)")
parser.add_argument("--steps", type=int, default=10, help="扩散推理步数(4-30,越多越精细)")
parser.add_argument("--device", default="auto", help='运行设备: auto / cuda / cuda:0 / cpu')
parser.add_argument("--normalize", action="store_true", help="开启文本归一化(展开数字、日期等)")
parser.add_argument(
"--denoise",
action="store_true",
help="先对参考音频降噪(需额外下载 zipenhancer 模型)",
)
parser.add_argument(
"--optimize",
action="store_true",
help="启用 torch.compile 优化(Windows 上可能编译失败,默认关闭)",
)
return parser.parse_args()
def resolve_prompt_text(ref_audio: str, prompt_text: str | None) -> str:
"""转写文本:显式 --prompt-text 优先,否则读同名 .txt。"""
if prompt_text:
return prompt_text.strip()
txt_path = os.path.splitext(ref_audio)[0] + ".txt"
if os.path.exists(txt_path):
with open(txt_path, encoding="utf-8") as f:
return f.read().strip()
sys.exit(f"未提供 --prompt-text 且未找到转写文件: {txt_path}")
def to_wav16k(path: str) -> tuple[str, bool]:
"""非 wav/flac 输入用 ffmpeg 转 16kHz 单声道 wav;返回 (路径, 是否临时文件)。"""
ext = os.path.splitext(path)[1].lower()
if ext in (".wav", ".flac"):
return path, False
ffmpeg = shutil.which("ffmpeg")
if ffmpeg is None:
sys.exit("需要 ffmpeg 转换音频格式,但未找到。请安装 ffmpeg 或改用 wav 输入。")
fd, tmp = tempfile.mkstemp(suffix=".wav", prefix="voxcpm_ref_")
os.close(fd)
result = subprocess.run(
[ffmpeg, "-y", "-i", path, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", tmp],
capture_output=True,
)
if result.returncode != 0:
os.unlink(tmp)
sys.exit(f"ffmpeg 转换失败: {result.stderr.decode(errors='replace')[-500:]}")
return tmp, True
def main() -> None:
args = parse_args()
for path in (args.ref_audio, args.model_dir):
if not os.path.exists(path):
sys.exit(f"路径不存在: {path}")
prompt_text = resolve_prompt_text(args.ref_audio, args.prompt_text)
print(f"转写文本: {prompt_text[:60]}{'...' if len(prompt_text) > 60 else ''}")
wav_path, is_temp = to_wav16k(args.ref_audio)
if is_temp:
print(f"已用 ffmpeg 转换参考音频 -> {wav_path} (16kHz 单声道)")
try:
print(f"加载模型: {args.model_dir} (device={args.device}, denoiser={args.denoise})")
model = VoxCPM.from_pretrained(
args.model_dir,
load_denoiser=args.denoise,
optimize=args.optimize,
device=args.device,
)
print("开始终极克隆生成 ...")
wav = model.generate(
text=args.text,
prompt_wav_path=wav_path,
prompt_text=prompt_text,
reference_wav_path=wav_path, # 与 prompt 同一 clip → 最高相似度
cfg_value=args.cfg,
inference_timesteps=args.steps,
normalize=args.normalize,
denoise=args.denoise,
)
sample_rate = model.tts_model.sample_rate
sf.write(args.output, wav, sample_rate)
duration = len(wav) / sample_rate
print(f"完成: {args.output} ({sample_rate} Hz, {duration:.1f}s)")
finally:
if is_temp:
os.unlink(wav_path)
if __name__ == "__main__":
main()
文章摘自:https://www.cnblogs.com/JackieJK/p/22519413
