通过CUDA(RTX4060)使用VoxCPM2克隆声音

使用的硬件是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