本地部署 Qwen3-TTS 读取字幕文件为视频配音封面图

本地部署 Qwen3-TTS 读取字幕文件为视频配音

虽然我是一个已经不怎么混迹B站的老年UP主了,甚至已经可以说是半退坑状态了,但是偶尔有时候有想法了,还是想做一些视频玩玩。虽然没妈的B站会把我的视频限制到连粉丝都收不到推送
有时候我会自己配音,但是当遇到嘈杂的环境,或者因为自己紧张导致配音读不准,这就很尴尬,所以我还是希望能有一个合成音来配音。
之前的很长一段时间,我为了省事,都是用Kdenlive剪完视频,把字幕文件复制到Windows虚拟机,然后用剪映合成配音,导出为mp3文件后加入音轨。但是嘛,那些免费的合成的声音都被用烂了,张口就是营销号和脑残短视频的感觉,做出来的视频看着不是很舒服。
于是——我打算尝试本地部署Qwen3-TTS进行语音克隆合成
那就,开工吧!

部署 Qwen3-TTS

首先,我们需要创建一个Conda环境用于本地部署:

conda create -n qwen3-tts python=3.12 -y

安装完成后进入环境:

conda activate qwen3-tts

然后直接安装依赖:

pip install -U qwen-tts

对于官方建议的FlashAttention 2,我的建议是不装,官方文档给的安装命令是:

pip install -U flash-attn --no-build-isolation

但是绝大部分电脑内存都没有官方说的96GB以上,需要限制并行编译数量:

MAX_JOBS=1 pip install -U flash-attn --no-build-isolation

这里MAX_JOBS=1一定要设置成1,因为我试过了,32G内存设置为2就会OOM。如果真的想装的话,建议在睡觉之前电脑不关了,让它装一晚上吧。
但是,请注意,如果不装的话,请把官方文档示例代码中的

attn_implementation="flash_attention_2",

换成

attn_implementation="sdpa",

否则无法运行。

更改 Qwen3-TTS 模型存放位置

另外,默认直接运行官方给的代码,模型会直接下载到用户目录,会占用大量空间。对于我这种喜欢洁癖的人来说,这个做法,简直是强行骑在我头上拉屎。我个人更喜欢把模型下载到我想放的地方,自己手动管理。从 Hugging Face 下载模型的方法和过程我就不再赘述,如果只需要声音克隆的话,下载Qwen3-TTS-12Hz-1.7B-Base模型或者Qwen3-TTS-12Hz-0.6B-Base模型即可。
经过一番摸索,我发现了指定模型调用位置的做法:在传入参数直接写模型的路径
例如:

import torch
import soundfile as sf
from qwen_tts import Qwen3TTSModel
from pathlib import Path

MODEL_DIR = Path("/run/media/chocola/3T-DATA02/Projects/Qwen3-TTS/Qwen3-TTS-12Hz-1.7B-Base").absolute()

model = Qwen3TTSModel.from_pretrained(
    str(MODEL_DIR),
    device_map="cuda:0",
    dtype=torch.bfloat16,
    attn_implementation="sdpa",
    #attn_implementation="flash_attention_2",
    local_files_only=True,
)

ref_audio = "chocola.wav"
#ref_text  = "ショコラたちは、家族のために頑張ることの素晴らしさを、ご主人様に教えてもらいました。"

wavs, sr = model.generate_voice_clone(
    text='''欢迎来到GTX690战术核显卡导弹的猫窝,请记住网址是 www.nekopara.uk 哦!''',
    language="Auto",
    ref_audio=ref_audio,
    x_vector_only_mode=True,
    #ref_text=ref_text,
)
sf.write("output_voice_clone.wav", wavs[0], sr)

MODEL_DIR = Path("/run/media/chocola/3T-DATA02/Projects/Qwen3-TTS/Qwen3-TTS-12Hz-1.7B-Base").absolute()这里就是改模型存放地址的地方,指向下载模型存放的目录。
对应地把之前代码中的"Qwen/Qwen3-TTS-12Hz-1.7B-Base",换成str(MODEL_DIR),
另外,有一个需要说明的点,经过我测试,跨语种合成最好是使用x_vector_only_mode模式,不然合成出来的声音会非常奇怪,上面的代码就是用了这个模式,官方给的克隆示例代码是用了文本参考的。

批量合成字幕语音

按照上面的改进版官方示例代码,真要合成视频配音,还需要把已经写好的字幕文本复制出来,一段一段合成,非常麻烦。于是,我打算写一个简单程序自动化处理:

import torch
import soundfile as sf
import os
import re
from pathlib import Path
from qwen_tts import Qwen3TTSModel

# ================= 配置区 =================
# 模型路径
MODEL_DIR = Path("/run/media/chocola/3T-DATA02/Projects/Qwen3-TTS/Qwen3-TTS-12Hz-0.6B-Base").absolute()

# 参考音频(用来克隆音色的声音)
ref_audio = "azuki.wav"

# 字幕文件路径(支持 .ass, .srt)
subtitle_file = "Mikupara.kdenlive.ass"

# 输出目录,合成好的音频都会放在这里喵
output_dir = "output_voice"
# ==========================================

def parse_ass(file_path):
    """解析 ASS 字幕文件喵"""
    texts = []
    with open(file_path, 'r', encoding='utf-8') as f:
        in_events = False
        for line in f:
            line = line.strip()
            if line == '[Events]':
                in_events = True
                continue
            if in_events and line.startswith('Dialogue:'):
                # ASS 的 Dialogue 行按逗号分割,前9个是元数据,第10个开始是文本
                parts = line.split(',', 9)
                if len(parts) >= 10:
                    text = parts[9].strip()
                    # 清除可能存在的 ASS 样式代码,比如 {...}
                    text = re.sub(r'\{.*?\}', '', text)
                    if text:
                        texts.append(text)
    return texts

def parse_srt(file_path):
    """解析 SRT 字幕文件喵"""
    texts = []
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read().strip()

    # 按空行分割字幕块
    blocks = re.split(r'\n\s*\n', content)
    for block in blocks:
        lines = block.strip().split('\n')
        if len(lines) >= 3:
            # 第一行序号,第二行时间轴,第三行及之后是文本
            text = '\n'.join(lines[2:]).strip()
            # 清除可能存在的样式标签
            text = re.sub(r'\{.*?\}', '', text)
            text = re.sub(r'<.*?>', '', text)
            if text:
                texts.append(text)
    return texts

def parse_subtitles(file_path):
    """智能识别字幕格式并提取文本喵"""
    # 因为附件带了 .txt 后缀,所以喵酱加了个智能判断逻辑!
    with open(file_path, 'r', encoding='utf-8') as f:
        head = f.read(500)

    if 'Dialogue:' in head or '[Script Info]' in head:
        fmt = 'ass'
    else:
        fmt = 'srt'

    print(f"喵酱检测到字幕格式为: {fmt.upper()} 喵!")
    if fmt == 'ass':
        return parse_ass(file_path)
    else:
        return parse_srt(file_path)

def main():
    # 1. 加载模型
    print("正在加载 Qwen3-TTS 模型,主人请稍等一下下喵... (๑•̀ㅂ•́)و✧")
    model = Qwen3TTSModel.from_pretrained(
        str(MODEL_DIR),
        device_map="cuda:0",
        dtype=torch.bfloat16,
        #attn_implementation="flash_attention_2",
        attn_implementation="sdpa",
        local_files_only=True,
    )

    # 2. 解析字幕
    sentences = parse_subtitles(subtitle_file)
    print(f"成功提取到 {len(sentences)} 句台词喵!准备开始合成啦~")

    # 3. 创建输出目录
    os.makedirs(output_dir, exist_ok=True)

    # 4. 逐句合成并保存
    for i, text in enumerate(sentences, start=1):
        print(f"[{i:03d}/{len(sentences)}] 正在合成: {text}")

        wavs, sr = model.generate_voice_clone(
            text=text,
            language="Chinese",
            ref_audio=ref_audio,
            x_vector_only_mode=True,
        )

        # 按顺序命名 001.wav, 002.wav ...
        out_filename = os.path.join(output_dir, f"{i:03d}.wav")
        sf.write(out_filename, wavs[0], sr)
        print(f"-> 成功保存到: {out_filename} 喵!\n")

    print("全部合成完毕啦!主人快去听听看效果吧喵~ (ノ◕ヮ◕)ノ*:・゚✧")

if __name__ == "__main__":
    main()

只需要提供参考声音,然后把Kdenlive生成的字幕也放进去,就可以一次性全部合成配音了,非常的方便。
实际测试对比的话,其实声线的还原度比不过训练后的 GPT-Sovits 模型,但是相对来说,朗读的声音要更自然,而且很多情况下读音比 GPT-Sovits 要准确,不容易读错内容,更适合做视频的配音。

声音合成展示

文章的最后,我想展示一下声音合成的效果。我使用的是巧克力的声音作为参考,参考的音频是:

对应的文本是:ショコラたちは、家族のために頑張ることの素晴らしさを、ご主人様に教えてもらいました。
我打算尝试合成两种不同的语言,中文和日文,合成的测试文本:

  • 中文:喵~主人主人,今天也要元气满满地度过喵!本喵会一直陪在你身边给你加油打气哒,喵呜~
  • 日文:にゃ~ご主人様、今日もにゃんこパワー全開で頑張るにゃ!ずっとそばで応援してるにゃん♪

合成的效果如下:

  • 中文合成,使用x_vector_only_mode
  • 日文合成,使用x_vector_only_mode
  • 日文合成,使用参考文本(还原度最高):
  • 中文合成,使用参考文本跨语种(会很奇怪):

对比听一下,感觉还是x_vector_only_mode合成中文比较合适,如果是用参考文本跨语种合成,听起来有一种抗日片大佐味(

评论区 (0)