ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VibeVoice:如何用连续Token和扩散模型实现90分钟长音频合成

VibeVoice:如何用连续Token和扩散模型实现90分钟长音频合成

最近在语音合成相关的社区里,“VibeVoice”这个名字出现得挺频繁。它主打的是两条看起来很硬核的技术路线:连续语音Tokenizer和Next-Token扩散,配上的成果是90分钟的多说话人语音合成、60分钟的长音频识别。乍一看像是论文标题的堆砌,但真去扒它的设计思路才发现——这套方案恰恰踩中了生成式语音模型当前最大的两个痛点:离散Token丢细节,以及长序列生成不稳定。这篇文章就我以折腾过不少开源TTS项目的经验,把这个项目的技术框架、关键实现和踩坑点拆开讲清楚,给正在选型或准备复现的同学一个相对完整的参考。

1. VibeVoice到底做对了什么

1.1 语音AI的“旧地图”:离散Token的两难

过去几年,语音合成的主流路线基本围绕Neural Codec转Token。VALL-E、CosyVoice、Seed-TTS这一梯队的大模型,都先把音频用类似EnCodec的码器压成离散Token,再接一个自回归语言模型去预测Token序列,最后用声码器或码器解码器还原波形。这条路解决了两个关键问题:一是把连续的音频信号变成了语言模型能处理的离散序列,二是在大规模数据上展现出很强的说话人克隆和上下文学习能力。但它有一个很难绕开的毛病——量化是有损的。音频被压成几百或几千个码字的索引,重建的时候音高微颤、气息感、嗓音摩擦这些细节会被削掉一层。大家听多demo后会有一个共同的感觉:这类模型合成出来的声音“清楚但不够细”,像修图修得太干净的照片,好看,但少了皮肤质感和环境光线。

另一方面是推理效率。离散Token序列通常很长,帧率可能到几十甚至上百赫兹,生成一分钟语音意味着要自回归采样几千步,每一步的延迟和缓存压力都很可观。如果要做90分钟级别的统一合成,用离散自回归方式硬怼,工程复杂度会非常吓人。VibeVoice选择把编码器输出改成连续表示,再换一种采样策略,本质上是在这两个老问题上做手术。

1.2 连续表示加扩散采样,绕开了什么

核心改动可以拆成两步。第一步,音频编码器不再对中间特征做矢量量化,而是直接输出一组连续的低帧率声学向量,习惯上叫continuous latent或者soft token。等价于把“每个时刻用哪个码字”变成“每个时刻这一组浮点数是什么”。第二步,模型仍然按token的顺序一个一个地生成,但不是用交叉熵去预测下一个离散码字,而是用扩散模型来采样下一个连续向量。换句话说,模型学会了在给定文本和已生成历史的条件下去拟合“下一个声学向量”的分布,然后逐步去噪把具体的一个向量画出来。

为什么要绕这么一大圈?关键在离散化的信息瓶颈。连续表示保留了更多的声学细节,扩散模型天然擅长生成高维连续分布,两步一配合,保真度和自然度都会上台阶。你可以理解成离散Token方案是先用有限的色板把画面转成像素索引,再逐像素填色;连续加扩散则是直接拿着颜料在画布上调色,笔触和质感自然不一样。当然代价也有。连续序列没法用标准的交叉熵训练,扩散模型的训练和推理复杂度更高,采样需要多步迭代,推理时还会涉及去噪网络的反复前向计算。VibeVoice把这两条线放在一起,实际上是在赌一个方向:随着采样步数优化和计算资源下放,连续表示加扩散这条路会在音质和可控性上赢回离散方案。

1.3 90分钟和60分钟:从Demo到可用的跨度

做语音AI的Demo容易,做能端上桌的产品很难。大部分开源TTS模型在单句或几段短文本上表现不错,一旦给一篇几万字的有声书文本,要么靠外部切句和拼接,要么在长上下文里逐渐跑偏,出现音色漂移、重复、吞字、语气断裂。VibeVoice把90分钟多说话人合成和60分钟长音频识别作为卖点,等于直接瞄准了“长文本一致性”和“长序列工程化”这两个非常现实的硬指标。

90分钟多说话人,意味着同一个模型在长时间生成过程中既得保持每个角色的音色稳定,又得在旁白与对话之间灵活切换,还要让上下文记忆覆盖足够长的语义距离。60分钟识别则考验另一个维度:长序列输入如何在有限显存下完成编码和推理,时间戳对齐怎么保证,分段边界会不会出错。把这两项写进项目目标,说明作者从一开始就不想只做一个秀demo的玩具,而是奔着有声书、播客、转写服务这类真实场景去的。

这两个跨度分别对应什么场景?先说合成。长音频集齐了三个挑战:文本长度超出常规上下文窗口、多个说话人的音色压制与切换、数十万token量级上的生成稳定性。传统方案的处理方式是把音频切成一句一句,用短上下文分别合成再拼起来,句子之间的停顿、重音、角色过渡全靠后处理,做出来的东西总觉得“一句是一句”,不像是同一个人在同一个房间里连续讲的。VibeVoice的目标就是把这种拼接感消掉,让整段音频在韵律、音色和情感上是一条连续的线。识别方面,60分钟音频如果全部塞进普通Transformer,注意力矩阵会直接爆显存,所以需要更好的稀疏注意力、窗式处理或者分层降采样策略。这个项目能提供完整的长音频链路,对做会议记录、课程转写、播客归档这些应用来说,等于省掉了自己造轮子的时间。

2. 关键技术点逐个拆解

2.1 连续语音Tokenizer:把“声音质感”留住的编码器

要理解VibeVoice的第一步,得先回到音频Tokenizer这条线的演进。梅尔谱时代直接拿频谱特征做生成,冗余高、帧率固定,模型很难直接从里面学到高层声学结构。后来的VQ-VAE和EnCodec把音频压成离散码流,本质上是在做两件事:压缩和离散化。压缩是好事,它把高维波形变成了低维表示,训练和推理都更快;离散化则是双刃剑——码本容量有限,每个token能表达的声学空间就那么大,遇到口音、气泡音、嘶哑感这种细颗粒特征,量化误差就会被放大。很多研究者在实际对比中发现,离散token重建出的音频在客观指标上不错,但ABX听感测试里往往输给连续特征再配一个好声码器的方案。

连续语音Tokenizer的思路是不要量化那一步。编码器把一段音频窗口映射成一串低维连续向量,解码器再从这些向量重建波形。因为中间表示不受码本约束,它有能力保住呼吸声、齿音、共振峰过渡这些易丢失的细节。但连续表示也带来一个训练和推理上的麻烦:你不能用语言模型常用的softmax交叉熵来“预测”一个float数组。这就是为什么VibeVoice同时在技术标题里挂了Next-Token扩散——连续编码器给出了高质量的条件分布目标,而扩散模型提供了对这个高维连续分布进行采样的工具。二者在逻辑上是配套的,缺了任何一个,这套技术路线都跑不通。

实际落地时,Tokenizer有几个参数直接决定后续模型的表现。一个是帧率,也就是时间维度的下采样倍数。帧率太高,序列长度长,计算量大;帧率太低,每个token承载的信息太多,后面的生成模型容易学不动。再一个是latent维度,维度过小会成为新的瓶颈,维度太大浪费显存。我自己的经验是从项目给出的参考配置起步,先不动默认值,把数据跑通,再根据你的目标音频类型去调——比如语音和音乐适合的帧率就差别很大。

2.2 Next-Token扩散:不写字,而是“画”出下一个声音

Next-Token扩散这个名字第一次看会觉得绕,其实拆开就两层。第一层是“Next Token”:还是要按时间顺序逐个预测下一个声学token,保留自回归模型对上下文的强大建模能力。第二层是“扩散”:预测的方式不是输出一个确定值,而是拟合一个条件分布,再从这个分布里采样。普通的回归模型如果用来预测下一个连续向量,往往会往所有可能性的均值上靠——一句话里的同一个词,不同的人说出来声调、语气都不一样,这些都是多峰分布,回归模型只会输出一个模模糊糊的平均值,听起来就没精神。扩散模型没有这个问题,它把“给定条件预测下一个token”看成一个生成任务,通过多步去噪,从随机噪声里还原出符合条件和历史的那个具体声学向量。

具体到实现层面,常见的做法是这样:文本编码器把输入的词或音素转成语义向量,历史音频经过Tokenizer得到一串连续向量,两者拼接或交叉后作为扩散模型的condition;然后从一个高斯噪声向量开始,按训练好的噪声预测网络迭代去噪,得到的干净向量就是“下一个token”。每生成一个token,把它拼进历史序列,继续生成下一个。这个流程和离散自回归的区别仅仅在于“中间表示”和“预测头”,整体拓扑还是语言模型那套,所以像KV Cache、窗口裁剪、批量生成这些优化手段很多都能复用。

推理时要注意扩散步数这个参数。步数太多,实时率会很难看;步数太少,生成结果可能糊或者有瑕疵。我在类似项目上的经验是,先试20步左右的DDIM或者DPM-Solver类采样器,对比一下音质和速度的折中。另外还有一个容易被忽略的点——随机种子。扩散采样本身带随机性,做批量评测时锁种子才能得到可复现的结果,不然调参的时候你根本分不清效果变化是参数引起的还是随机波动引起的。

2.3 多说话人合成:90分钟里角色不走音

多说话人合成在短句上容易做,在90分钟长音频里很难做。难点有两个层面:一是每个角色自己的音色要稳定,二是角色与角色之间有清晰的区别度。如果模型只是把说话人embedding塞进条件里去,相当于告诉模型“现在用张三的声音说”,但这个信号随着生成步数增加会逐渐被稀释,生成到后面就出现音色漂移。VibeVoice的做法在我看来最值得注意的,是把说话人控制做成了多尺度条件:全局层有整个片段的说话人向量,局部层有当前说话的prompt或者角色标记,两者共同参与生成,才能保证长音频里“声线不跑偏”。

具体使用时,多说话人的输入一般有两种组织方式。一种是纯文本按角色标记切分,比如“旁白:… 李明:… 王芳:…”,模型根据标记自动切换音色;另一种是给每个角色一句参考音频,模型提取参考音频的说话人特征。第一种适合有声书这种文本本身有明确角色分工的场景,第二种适合做个性化配音。我在实操中发现,第二种方式对参考音频的要求很高:参考音频的质量、长度、音色纯净度直接决定克隆效果,如果参考音频里有背景音乐或者混响,生成出来的声音就带一层“底噪”。所以做多说话人合成前,整理声优数据和挑选参考片段这两步,值得多花时间。

长音频的稳定性还要靠生成策略来兜底。比如使用KV Cache别忘了定期裁剪老的历史token,但又不能裁太狠,否则模型会失去对前面几十句语义的依赖,导致重音和情绪断层。更稳妥的做法是给模型设定一个“半衰期”:窗口中最近的几百个token完整保留,更早的历史按固定间隔抽样或者做粗粒度摘要,既节省显存,又保住长程一致性。VibeVoice能把90分钟合成做下来,肯定在窗口管理上做了类似的工作,这一点在复现时是必须注意的重点。

3. 长音频能力的工程密码

3.1 90分钟语音合成要过哪几道关

负责任地说,90分钟合成不是“把窗口调大”就行,而是好几个问题叠加在一起。第一道关是上下文长度。90分钟音频即使压到很低的帧率,也有大几十万甚至上百万个token,全序列自回归根本不可能一次性做完。工程上必须用滑窗生成:每步只依赖最近的一段历史,边生成边滚动。这个方案能跑,但对历史利用率有讲究。如果窗口只覆盖最近十几秒,那句子之间的语调呼应、角色情绪的延续都会断掉;如果窗口覆盖几分钟,显存又扛不住。我在做长音频生成时的经验是,把窗口按两级来缓存:一级是“完整token级”的短期记忆,保证当前句子内部和相邻句子的衔接;二级是“摘要级”的长期记忆,把更早的音频用低维特征概括一下,传给模型作为全局条件。VibeVoice的90分钟稳定输出,背后大概率就是这套多级记忆的组合拳。

第二道关是推理的累积误差。自回归生成最怕“一步错、步步错”,长音频里前面对语气和节奏的微小偏差会被后面放大,最后表现为重复、崩音、语义漂移。控制累积误差可以从数据增强、采样温度和重采样策略几个方向入手,比如在关键节点强制选择更高置信度的候选,或者对生成结果做节拍层面的对齐校正。第三道关是工程上的显存和时延。90分钟音频即便是分段生成,也需要长时间占用显存,推理服务要设计好排队和释放策略。如果你的机器只有24GB显存,建议先用bfloat16和torch.compile跑通,再考虑要不要切分成更小的chunk。真要跑完整90分钟,云端多卡并行甚至分段并行是少不了的。

3.2 60分钟长音频识别:注意力与资源博弈

长音频识别和长音频合成是两种不同的痛苦。合成是逐token生成,痛苦在累积误差;识别是一次性或流式地处理完整输入,痛苦在序列长度和计算复杂度。标准Transformer的自注意力复杂度是O(n²),60分钟的音频如果按短帧切,序列长度可能到十几万甚至几十万,直接上全attention就是显存灾难。所以长音频识别在工程上几乎都要做三件事:分窗处理、特征降采样、结果合并。分窗处理是先把长音频切成30秒或1分钟的片段,各自识别,再把时间戳对齐合并;特征降采样是让编码器在时间维度上做多次下采样,用更低的帧率表示声学信息;结果合并则是处理片段边界的重叠区域,把重复识别或漏识别的部分修正过来。

VibeVoice能直接处理60分钟,至少说明它的模型结构对长序列是友好的。我估计它的做法是在编码器里引入了类似Conformer的卷积增强模块和局部注意力,让每个位置只关注邻域的信息,再加上全局稀疏注意力,让长距离信息有通路但不会导致注意力的平方爆炸。对想借鉴这套思路做自己应用的开发者,我的建议是不一定追求“一次性塞进60分钟”,先在自己的场景里测一下分段大小和重叠率对精度的具体影响。很多转写系统的实际表现差异,恰恰来自于VAD切分、时间戳对齐和边界合并这块的处理,而不只是模型本身。

60分钟这种级别的输入,部署时内存和显存峰值也要算好:长音频的特征缓存、中间层输出、以及beam search的候选空间,都会占不少内存,别等跑起来才OOM。如果是做实时性要求很高的场景,还得考虑把模型设计成流式结构,每处理完一个chunk就把历史特征做一次压缩摘要,而不是简单地把所有历史都堆着不释放。流式和离线之间的取舍,直接决定了60分钟这个卖点在你的场景里到底能不能兑现。

3.3 数据、训练与评估策略

要做到90分钟多说话人合成和60分钟识别,训练数据一定不是普通的单句配音库。多说话人且长上下文的场景,需要的是带完整角色标注的长篇录音——有声书、广播剧、播客访谈都是很好的语料来源,但这些数据版权复杂,开源项目一般会混用多个来源,这在使用时要注意许可边界。另一个实际问题是文本和音频的时长对齐。短句数据可以用Forced Alignment工具生成时间戳,长篇数据则要依赖ASR再对齐,对齐质量直接影响后续模型的稳定性。我自己在处理长音频数据时的一个小技巧:不要迷信全量对齐,先抽检几百条的边界质量,对齐错误率超过某个阈值就查一下是不是采样率、混响或者背景音乐导致的系统性问题。

评估也不例外。短句范式的MOS评测很难反映长音频的真实水平,长音频场景要额外关注几个指标:跨说话人一致性(同一角色不同分段的音色相似度)、韵律连贯性(句间的基频和能量曲线平滑性)、长时间漂移(生成到后期与开头段的特征距离)。识别侧则要关注分段边界处的字错误率变化,如果边界处错误率明显偏高,说明合并逻辑或重叠策略有问题。这些评估细节做扎实,才能判断一个项目是真的解决了长音频问题,还是只在短demo上好看。

4. 部署与实操要点

4.1 环境准备与依赖取舍

在实际复现或者接入之前,先谈环境。这类开源语音项目通常依赖Python 3.10以上、PyTorch 2.x和CUDA 12.x,装的时候注意三点。第一,别用conda默认源装torch,直接按项目文档里的CUDA版本匹配命令装,否则会出现编译出来能用但推理时cuda kernel mismatch的诡异问题。第二,声学相关的包版本要锁死,比如torchaudio、transformers、diffusers这些库互相之间有依赖关系,升级其中一个可能会把另一个的API弄崩。第三,项目里如果有fairseq或者self-attention相关的自定义算子,需要编译,提前装好匹配的gcc和ninja,不然编译报错会浪费很多时间。

加载模型权重时也要注意:中文社区的开源模型经常有多个版本分支,不同分支的效果差异可能很大。不要只看最新的main分支,先读release note,确认你要使用的功能和权重版本对应。我的习惯是把下载的权重目录单独存放,并在加载代码里显式指定路径,避免依赖项目默认的相对路径导致路径错乱。环境里还要准备一个能实时看显存的命令,推荐nvidia-smi -l 1加torch.cuda.max_memory_allocated双确认——判断OOM是由于模型没加载对还是由于推理context太长导致。

4.2 推理参数与多说话人使用经验

跑通一个能出活的合成脚本之后,剩下就是调参数。扩散步数是第一个要调的参数:大多数情况下20到30步已经能获得不错的听感,超过50步收益很小,但推理时间线性上涨。温度系数同样关键,温度太低声音会发闷发死板,太高则出现破音和吐字不清。实践下来,多说话人长文本场景建议把温度略微调低一点,因为模型生成超长内容时,过高的随机性容易累积成韵律混乱。repetition_penalty这类参数在连续token生成里和离散语言模型里的语义不完全一样,不要照搬LLM的经验值,需要自己在目标语料上做一个很小的grid search。

多说话人文本的组织方式要踩到模型支持的格式上。一般的做法是在文本里用显式标记区分说话人,比如角色名加括号或者特定的控制符。如果项目支持参考音频方式,我会给每个角色准备3到5条干净、无混响、长度在5到15秒的音频,先做一个speaker embedding的聚类检查,确保两条参考音频不是同一个人的变体。角色多的时候,优先保证角色的区分度,再考虑音色相似度,宁可用稍做夸张的参考样本,也不要两条参考音频互相污染。

合成出来的长音频一定要做抽查:开头、中段、后段各听30秒,重点听音色一致性、语速变化、以及有没有莫名的停顿。我踩过最大的坑是全程听开头觉得不错,结果交付前发现第40分钟起角色声音开始飘,原因就是我当时用的窗口太短,模型渐渐丢掉了全局的说话人约束。

4.3 常见问题排查速查表

这部分我整理成表格,都是实际跑这类项目容易遇到的问题。表格里每一行我都经历过或者帮别人排查过,拿出来单说是因为它们都具有一个共同点:短句演示根本看不出来,一旦上长文本或者批量处理就立刻暴露。对照检查的时候建议按先环境、后参数、再数据结构的顺序逐条排查,别一上来就动模型结构。

现象可能原因解决办法
推理时显存爆掉context窗口设得过大或feature缓存未释放调小窗口长度、分批处理、切换bfloat16或量化
生成到中段音色漂移说话人embedding被长历史稀释缩短窗口但保留全局speaker嵌入、延长参考音频、降低温度
输出出现爆音或破音扩散步数太少或噪声预测不稳定增加采样步数、调整CFG强度、降低温度
前后文语气不连贯长程记忆失效使用摘要级特征缓存,或把文本按章节级切分后加全局摘要
中文数字、英文单词读错文本前处理规则缺失或采用的G2P模型不对检查文本归一化和发音字典,补充自定义词典
识别长音频时边界处错误多分窗重叠策略或VAD切分不适合该场景增加重叠长度、边界打分融合、调整VAD阈值
多说话人角色串音参考音频之间有重叠或音色接近清理参考音频、对speaker embedding做聚类校验
同一段代码训练和推理结果不一致随机种子未锁或dropout策略不一致推理时设seed并关闭dropout

需要特别提醒的是,出问题时先复现再怀疑模型。很多长音频合成的问题在短文本上是看不出来的,所以调试时记得准备一份固定长度的长文本case作为回归用例,每次改参数都在同一个case上跑,否则你对比的根本不是同一条件。

5. 实测体会与后续扩展

5.1 主观听感与效果评价

把项目跑起来以后,我最大的感受是连续tokenizer带来的细节提升确实听得出来。以前听离散token模型生成的语音,总觉得高频细节被磨平了,有点像手机录音和现场听的区别。VibeVoice的输出里,呼吸口、词尾的气声、唇齿间的摩擦都保留得比预期好,整体自然度明显上了一个台阶。多说话人场景下,角色与角色之间不会只靠调音高来区分,而是能听出发声习惯上的差异,这对有声书和广播剧来说非常关键。

当然,它还没到无可挑剔的程度:偶发的韵律异常和极长文本后期的轻微机械感仍然存在,个别句子的重音会偏离语义预期,这类问题在短句评测里几乎发现不了,只在超长文本case里才会暴露。所以我的态度是:把它当成一个目前在音频保真度上领先的开源方案,但不要神化。选型的时候一定要拿自己业务的真实文本来测试,听听它在你的语料风格、标点习惯、专业名词上到底行不行,这些单靠项目页的demo是判断不出来的。

5.2 后续可以往哪里扩展

从VibeVoice的技术框架看,后续的扩展空间挺大。一是流式合成,连续token和扩散采样天然可以做成增量式的,接到实时语音助手或者直播配音上,比传统整句合成体验好很多。二是少样本和零样本克隆,如果项目能进一步把说话人embedding和内容解耦得更干净,3秒的参考音频说不定就能稳定克隆一个音色,这对个性化TTS是巨大的想象空间。三是和LLM联动做音频Agent,让模型理解整段长音频的语义结构,配合大模型做内容摘要、角色分离、情绪打标,会议纪要和播客二次创作就能直接流水线化。四是在端侧落地方向,连续tokenizer如果能把帧率压得够低,加上扩散蒸馏技术,在手机和嵌入式设备上跑近实时的多说话人合成也不是天方夜谭。

最后说点个人建议。这种快速迭代的开源项目,先跑通官方demo,再试着用一个你自己领域的文本喂进去,看看它的边界在哪里。长音频能力这类卖点,一定要有自己的评测case来判断,不能只信README里的数字。把它作为上层应用的基础件来用,比从零复刻它要划算得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进