虽然我是一个已经不怎么混迹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合成中文比较合适,如果是用参考文本跨语种合成,听起来有一种抗日片大佐味(