VibeVoice 的 processor 家族:一段输入在四个类之间怎么流转
第一次翻 VibeVoice 仓库的 vibevoice/processor/ 目录,会先被命名绕一下:目录里有 vibevoice_processor.py、vibevoice_asr_processor.py、vibevoice_streaming_processor.py、vibevoice_tokenizer_processor.py 四个文件,名字都带 processor,但它们不是同一层的东西,也不是可以互换的四种实现。
更容易踩的是导出这件事。vibevoice/processor/__init__.py 的 __all__ 里写着 VibeVoiceProcessor、VibeVoiceStreamingProcessor、VibeVoiceTokenizerProcessor、AudioNormalizer——没有 VibeVoiceASRProcessor。而 vibevoice_asr_processor.py 文件末尾自己写了 __all__ = ["VibeVoiceASRProcessor"]。这两处对不上的结果,在 demo 里能直接看到:demo/vibevoice_asr_inference_from_file.py 与 finetuning-asr/lora_finetune.py 都是写全模块路径 from vibevoice.processor.vibevoice_asr_processor import VibeVoiceASRProcessor 才拿到它的。再往上一层,vibevoice/__init__.py 只再往外抛了 VibeVoiceStreamingProcessor 和 VibeVoiceTokenizerProcessor 两个。
下面按「一段输入怎么走」的顺序读一遍。
最底下那层:VibeVoiceTokenizerProcessor 其实不处理文本
名字里有 tokenizer,实际上它一个 token 都不产。类定义在 vibevoice/processor/vibevoice_tokenizer_processor.py,继承的是 transformers 的 FeatureExtractionMixin,文件里有一行注释说明了这个选择:「Change from ProcessorMixin to FeatureExtractionMixin which is designed for single components」——这是仓库自述的理由,我不替它多解释。
它干的事只有音频前处理:
_ensure_mono()把(2, time)或(time, 2)的双声道取均值成单声道,形状对不上就直接raise ValueError;- 构造参数
normalize_audio打开时挂一个AudioNormalizer(在vibevoice/processor/audio_utils.py),它的__call__是两步:先tailor_dB_FS()按target_dB_FS缩放,再avoid_clipping()按峰值回缩; _load_audio_from_path()按扩展名分流:音频扩展名走librosa.load(..., sr=self.sampling_rate, mono=True),.pt走torch.load(..., weights_only=True),.npy走np.load。
这里有个值得记一笔的小坑:该方法 else 分支抛出的错误信息里,把 .npz 写进了「Supported formats」的清单,但上面的判断分支并没有 .npz 这一条。也就是说错误信息本身和分支列表对不齐,读的时候别把它当支持列表用。
另一处对不齐在返回值上。类属性写的是 model_input_names = ["input_features"],__call__ 的 docstring 也说返回 input_features,但函数最后返回的 dict 键是 "audio",旁边还留着一行注释 # Use "audio" instead of "input_features"。你按 docstring 取键会取空。
return_tensors="pt" 时单条输入走 unsqueeze(0).unsqueeze(1) 补成三维;多条走 torch.stack。注释写的是「For batched input with different lengths, create a batch properly」,但这一行并没有补齐逻辑,就是 torch.stack。两处放在一起看就是注释与实现不一致,具体后果我们没有跑过,不下结论。
ASR 那条链:音频进去,一串 chat template 出来
VibeVoiceASRProcessor 是四个里最完整的一条端到端前处理链,落点也最实。它的 __init__ 接两个东西:tokenizer 和 audio_processor——后者不传就自己 new 一个 VibeVoiceTokenizerProcessor。所以「音频处理器」是被「ASR processor」持有的,不是并列关系。
一段音频进 __call__ 之后,逐条走 _process_single_audio():
- 解码。传的是路径就先试
load_audio_use_ffmpeg()(audio_utils.py里,先用ffprobe探出原始采样率,再用ffmpeg解成s16le单声道),失败会warnings.warn后退回soundfile;采样率和target_sample_rate不一致时再补一次librosa.resample。顺带一提,audio_utils.py支持一个环境变量VIBEVOICE_FFMPEG_MAX_CONCURRENCY来给 ffmpeg 进程数加信号量,注释里写明是为 vLLM 并发场景准备的。 - 归一化,走上面那个
AudioNormalizer。 - 算占位长度:
vae_tok_len = math.ceil(len(audio_array) / self.speech_tok_compress_ratio)。speech_tok_compress_ratio的默认值在类 docstring 里写着 3200,并注明它是 encoder 各级压缩比[8,5,5,4,2,2]的乘积(这是仓库当前代码里的默认值,随版本可能变动)。 - 拼 token 序列——text tokenizer 就是在这一步才登场的。先
apply_chat_template([{"role": "system", "content": SYSTEM_PROMPT}], tokenize=False)拿到字符串再encode;然后把语音位置铺成一串占位符:sp_start_token+sp_pad_token重复vae_tok_len次 +sp_end_token,后面接一句 user 描述,里面带上音频时长和show_keys = ['Start time', 'End time', 'Speaker ID', 'Content'],整段再过一次apply_chat_template(..., tokenize=True)。 - 造掩码:
acoustic_input_mask是逐 token 比对是否等于speech_pad_id得到的 0/1 列表。
注意这里的顺序:音频从来没有被送进 text tokenizer,它只是决定了要铺多少个 <speech_pad> 占位 token。 真实波形是另走一路,在 _batch_encode() 里补零成 speech_tensors,配一张按 vae_tok_len 置位的 speech_masks。文本侧与音频侧靠 acoustic_input_mask 对齐——这是整条链的接缝。
_batch_encode() 还有个容易忽略的细节:padding 是左补的([self.pad_id] * padding_length + input_ids),代码注释写明是「for autoregressive generation」。最终 BatchEncoding 里的键,以 model_input_names 属性为准是 input_ids、attention_mask、acoustic_input_mask、speech_tensors、speech_masks。__call__ 的 docstring 里还列了 vae_tok_seqlens,但 _batch_encode() 里有一行注释说明它和 speech_type 不是模型输入所以没放进去——又是一处 docstring 领先于实现的地方。
模型出来的文本再交给 post_process_transcription(),它会先尝试从带 json 标记的围栏代码块里抠出 JSON,抠不到就找第一个 [ 或 { 做括号配平,然后按一张 key_mapping 换键名:Start time / Start 都映到 start_time,Speaker ID / Speaker 映到 speaker_id,Content 映到的不是同名蛇形而是 text。解析失败它只 logger.warning 并 return [],不抛异常。
text tokenizer 分成两套,别配错
vibevoice/modular/modular_vibevoice_text_tokenizer.py 里有三个类:VibeVoiceTextTokenizer、VibeVoiceTextTokenizerFast、VibeVoiceASRTextTokenizerFast,都从 Qwen2 的 tokenizer 派生。关键差异在它们各自挂了哪些特殊 token:
| 类 | 语音起 / 止 / 占位 token | 暴露的属性 |
|---|---|---|
VibeVoiceTextTokenizer(Fast) | <|vision_start|> / <|vision_end|> / <|vision_pad|> | speech_start_id、speech_end_id、speech_diffusion_id |
VibeVoiceASRTextTokenizerFast | <|object_ref_start|> / <|object_ref_end|> / <|box_start|> | speech_start_id、speech_end_id、speech_pad_id |
两边都是复用 Qwen 词表里已有的特殊 token,代码注释自己写着 # (reusing vision tokens)。注意第三个属性名都不一样:ASR 侧叫 speech_pad_id,另一侧叫 speech_diffusion_id——所以 processor 和 tokenizer 是成对的,VibeVoiceASRProcessor._cache_special_tokens() 找的是 speech_pad_id,配错一套就取不到。
三个 processor(VibeVoiceProcessor、VibeVoiceASRProcessor、VibeVoiceStreamingProcessor)的 from_pretrained() 里各有一段几乎一样的硬判断:language_model_pretrained_name 先从 preprocessor_config.json 取,取不到才落到缺省值 "Qwen/Qwen2.5-1.5B"(同样是仓库当前代码里的默认值,随版本可能变动),且只有名字小写后含 qwen 才走加载分支,否则直接 raise ValueError。顺带一提,前两者的报错文案里还写着「Supported types: Qwen, Llama, Gemma.」,但代码分支里只有 qwen 一条——又是一处文案比实现走得靠前的地方。
还有一处 docstring 与代码的不一致值得标出来:VibeVoiceTextTokenizerFast.pad_id 的 docstring 写着「returns -100 for loss masking」,但它 return self._pad_id,而 _pad_id 取的是 <|image_pad|> 的 id;真正 return -100 的是慢速类 VibeVoiceTextTokenizer.pad_id。
VibeVoiceProcessor:文本脚本这一侧
VibeVoiceProcessor 处理的是「多说话人脚本 + 参考音频」这条 TTS 形态的输入。它的 _process_single() 顺序是:先判断 text 是不是 .json / .txt 路径(是就先转成脚本文本),再 _parse_script() 用正则 ^Speaker\s+(\d+)\s*:\s*(.*)$(带 re.IGNORECASE)逐行拆成 (speaker_id, text);如果所有 speaker id 都大于 0,会整体减一归一化到从 0 开始。解析不出任何一行会 raise ValueError。
然后拼序列,段落顺序在代码里写得很直白:system_prompt → _create_voice_prompt() 产出的 Voice input:\n 段 → Text input:\n 段(每行 f" Speaker {speaker_id}:{speaker_text}\n")→ Speech output:\n 加一个 speech_start_id 收尾。参考音频在 _create_voice_prompt() 里同样只贡献长度:按 math.ceil(wav.shape[0] / speech_tok_compress_ratio) 铺相应个数的 speech_diffusion_id,同时把 True 写进 voice_speech_masks,波形本身进 voice_speech_inputs 另走 prepare_speech_inputs()。
关于这条链的状态必须交代清楚:仓库 README 记载,2025-09-05 微软因发现有与既定意图不符的使用方式、基于负责任 AI 原则从该仓库移除了 VibeVoice-TTS 代码;现在的 vibevoice/modular/modeling_vibevoice.py 首行注释标明它是从社区 fork github.com/vibevoice-community/VibeVoice 复制回来的,且 vibevoice/modular/__init__.py 的 __all__ 只导出 Streaming 系列六个符号,TTS 的模型类不在其中。这一节讲的是代码结构,不是可用性保证。
另外,这个文件里的 _merge_inputs() 方法,我们在仓库内没有找到调用点。
VibeVoiceStreamingProcessor:唯一一个 __call__ 会抛异常的
这个类最反直觉:__call__ 的实现体就是 raise NotImplementedError,docstring 明说是 intentionally not implemented,让你改用 process_input_with_cached_prompt()。
而这个方法的输入也和前面几个不一样——它要一个 cached_prompt 字典,长度直接从里面的张量读:cached_prompt['lm']['last_hidden_state'].size(1) 和 cached_prompt['tts_lm']['last_hidden_state'].size(1)。拿到长度后用 self.tokenizer.pad_id 铺出伪的 input_ids 与 tts_lm_input_ids(代码注释就叫 # pseudo input ids and masks),真正过 tokenizer 的只有一句:self.tokenizer.encode(text_input.strip() + "\n", add_special_tokens=False),结果放在 tts_text_ids 里。方法开头还写死了 texts = [text]、is_batched = False,docstring 也说明当前只支持单条。
顺着往下看:encoding 里 "speech_inputs" 恒为 None,所以 _batch_encode() 里的 has_speech 不会被置 True,speech_tensors 与 speech_masks 走 None 分支——同一个文件里的 prepare_speech_inputs() 与 VibeVoiceProcessor 里那份逐字相同(连同一行被注释掉的备用实现),但在这条链路上不会被走到。四个类里,这是把 text tokenizer 用得最轻的一个。
一句话对照
| 类 | 主入口 | 主要输入 | 主要输出 |
|---|---|---|---|
VibeVoiceTokenizerProcessor | __call__ | 波形 / 音频路径 | dict,键为 audio |
VibeVoiceASRProcessor | __call__ | 音频(单条或批) | input_ids、attention_mask、acoustic_input_mask、speech_tensors、speech_masks |
VibeVoiceProcessor | __call__ | 脚本文本 / 路径 + voice_samples | input_ids、attention_mask、speech_tensors、speech_masks、speech_input_mask |
VibeVoiceStreamingProcessor | process_input_with_cached_prompt | 文本 + cached_prompt | 上表基础上多 tts_lm_input_ids、tts_text_ids |
读完这一圈,最值得带走的判断是:这四个类里,只有 VibeVoiceTokenizerProcessor 真的碰波形,另外三个都是在拼 token 序列并给语音留位置;text tokenizer 的位置永远在「算完 vae_tok_len 之后」,它负责把占位符和文字编成 id,从不看到音频本身。定位问题时,先分清你的报错落在「波形这一路」还是「占位对齐这一路」,能省掉一半排查时间。
还有一件事:本文里几处 docstring 与实现不一致(input_features 与 audio、.npz、pad_id 的说明、use_streaming 那两条被注释掉的分支——_process_single_audio() 里在 audio_duration < 60.0 时会把 use_streaming 置 False,但下面按流式与否分开取整的 if/else 已被注释,现在两种情况都走 math.ceil),都是照着当前代码读出来的。这个项目持续更新,以仓库最新内容为准。
本文依据 github.com/microsoft/VibeVoice 仓库与 Hugging Face 模型卡于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有下载权重、没有跑过推理、也没有做过训练,
因此不涉及显存占用、推理速度、识别准确率与音质的任何描述,也不与其它模型做比较或排名。
该项目持续更新,文中涉及的模块路径、配置字段与接口写法随版本变动,请以仓库最新内容为准。
需要说明的是:仓库 README 记载,2025-09-05 微软因发现有与既定意图不符的使用方式,
基于负责任 AI 原则从该仓库移除了 VibeVoice-TTS 代码;
当前 vibevoice/modular/modeling_vibevoice.py 首行注释标明其来自社区 fork,
且该模块未被 vibevoice/modular/__init__.py 的 __all__ 导出。
本文只讲代码与架构,不构成 TTS 推理的可用性保证。
仓库 README 的风险与限制一节写明:该模型仅供研究与开发用途, 未经进一步测试与开发不建议用于商业或真实场景,并特别提示了合成语音被用于伪造与虚假信息的风险。 使用合成语音时应遵守所在司法辖区的法律法规,并在分享 AI 生成内容时主动披露。