
开发者用Qwen3-TTS为亲子游戏打造定制旁白语音
来源标题:Custom Voices with Qwen3-TTS
一位开发者为了给和孩子们一起开发的地下城探索游戏加上独特旁白,最终选用支持声音克隆的Qwen3-TTS。他此前用过Kokoro,但因Qwen3支持声音嵌入和克隆、能让配音更有个性而改用后者。文章记录了实践中的经验与示例,并指出Qwen3提供三种模型,但缺少同时克隆声音并用指令控制它的公开方法。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
我正在和孩子一起做一个地牢探索小游戏,也想借这个机会学一点游戏开发。不过既然我们生活在能力越来越强的现代,就想给游戏配一个旁白和配音。但我们又不想让它听起来像那种千篇一律的标准 AI 声音,于是我去研究了有哪些可选方案。经过一番调研,我把范围缩小到了 Qwen3-TTS,它用语音嵌入来实现自定义音色。稍微发挥一点创意,你还能用这些自定义音色做出比最初看起来更多的事情。如果你感兴趣,这篇博文就记录了我们学到的东西,并附上一些示例。你大概可以把这篇文章丢给你的 agent,让它照着做点什么。
Qwen3 并不是市面上唯一的 TTS 模型,甚至都不是我的首选。我们最初用的是 Kokoro。但 Qwen3 支持声音克隆,而我们希望声音听起来更有独特性。不过实际操作起来要麻烦一些。Qwen3 提供三类模型:
base 模型可以通过上下文克隆,也可以仅凭说话人嵌入来克隆,但并没有文档化的方法能在克隆音色的同时用指令来控制它。也就是说,与 custom voice 和 voice design 模型不同,用克隆的 base 模型你没法说“用悲伤的语气说”或“耳语”。
目前官方给出的自定义音色方案,要么是在单个说话人的数据上微调 Base,要么是用 VoiceDesign 生成一段音频,再用 Base 克隆它。这倒也不算太糟,但意味着指令控制就没有了。
为了这篇博文,我从自己在 PyAI Conf 演讲的录像里截了 28 秒,剪在两个停顿之间,让它以完整句子开头和结尾,并用 Whisper 转写了这段音频。转写文本其实只有烘焙(bake)时才需要,但如果要拿它和已有的音色做对比,让克隆音色说和原始录音相同的内容会很有用。
要注意,会议演讲的录音并不理想,但它照样能用。这也提醒我们:其实你并不需要特别干净的录音(也不需要很长的录音),就能得到一个勉强像某个人的版本。尤其是当它通过低质量的电话通话呈现给你时,想骗过你的祖母并不难。
你能做的第一件事,是把这段录音和另一个音色混合。比如你可以用 VoiceDesign 根据一段描述凭空生成一个音色。这篇博文里,设计出来的音色是这样生成的,用的是 Qwen 自家的 Python 包 qwen-tts(pip install qwen-tts,我用的版本是 0.1.1),本文其余代码也都用它:
import torch from qwen_tts import Qwen3TTSModel kw = dict(device_map="mps", dtype=torch.bfloat16, attn_implementation="sdpa") design = Qwen3TTSModel.from_pretrained("Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign", **kw) wavs, sr = design.generate_voice_design( text=open("voices/transcript.txt").read(), language="English", instruct="A man in his forties with a warm, natural, medium-low voice, speaking " "clearly and calmly at an easy conversational pace, like someone reading " "aloud to a friend.", )
你可以听出这两种不同音色的表现,尤其是混合在一起时:
交互式语音样例需要 JavaScript。
仅凭嵌入克隆时,Base 从录音里提取的是一个说话人嵌入,本质上就是训练好的说话人编码器输出的 2048 个数字。同一个声音会得到相近的数字,不同的声音则得到不同的数字。顺便说一句,如果你想做录音中的说话人检测,这也是个很方便的办法!它并不完美,但如果你做一些基本的切分处理(下面会详细讲),对于单条音轨的录音来说,区分两个说话人是相当有用的。
因为它只是一个向量,你可以对它做算术运算。比如两个嵌入的加权平均,就是一个介于两个音色之间的说话人:
base = Qwen3TTSModel.from_pretrained("Qwen/Qwen3-TTS-12Hz-1.7B-Base", **kw) def fingerprint(path): prompt = base.create_voice_clone_prompt(ref_audio=path, x_vector_only_mode=True) return prompt[0].ref_spk_embedding def blend(a, b, t): a, b = a.float(), b.float() mixed = (1 - t) * a + t * b return (mixed / mixed.norm() * ((1 - t) * a.norm() + t * b.norm())).to(torch.bfloat16) armin, designed = fingerprint("voices/armin.flac"), fingerprint("voices/designed.flac")
下面是同一句话在十一种混合比例下的效果:
注意,它很快就听起来不像我了,但仍保留了我的一些特征。而且无论如何,它最终都会得到一个独特的音色。我发现,如果确保两个音色的频谱图相似(比如“录音设置”相近),效果会更好。在这段录音里你能听出,用领夹麦录的那个音色相比设计出来的音色显得有点空洞。
如果你喜欢混合出来的某个音色,可以把它烘焙成一段新录音,然后拿它当参考:
import soundfile as sf mixed = blend(armin, designed, 0.5) audio, rate = say(open("voices/transcript.txt").read(), mixed) # say() is shown further down sf.write("voices/final.flac", audio, rate) final = fingerprint("voices/final.flac")
烘焙出来的音色不会和混合结果完全一致,但如果你想要的话,它能让你后续处理更方便。它就只是一个音色参考文件。
那么回到用这些嵌入做说话人识别的问题:效果到底有多好?我拿了一集 State of Agentic Coding——我和 Ben 一起做的播客——把它切成间隔一秒的 2 秒窗口,然后把每个窗口和我在会议录音里的嵌入做比较。有点意外,但也许并不太意外:用我的会议录音片段作参考时,在 Qwen3 上这完全行不通。但如果改用那集开头我自我介绍的那 20 秒作参考,其余部分有 98% 判断正确。
另一方面,换成一个专门为说话人验证训练的模型,确实能从会议录音片段里认出我。比如 ECAPA,
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。