VibeVoice-ASR 上手:文档给的调用路径与输出结构
手上有一段多人会议录音,要出的稿子不是一整块文字,而是「谁、在哪一段时间、说了什么」。传统做法是拼三样东西:一个 ASR 出文字,一个 diarization 分说话人,再想办法把两边的时间轴对齐。拼起来的活儿最麻烦的不是识别本身,是三方时间戳对不上时的那堆胶水代码。
VibeVoice-ASR 在仓库文档里给的定位是另一条路:docs/vibevoice-asr.md 开头写明它是一个统一的 speech-to-text 模型,直接生成包含 Who(Speaker)、When(Timestamps)、What(Content)的结构化转写,并且说明它把 ASR、diarization 与 timestamping 是联合完成的。这篇就沿着仓库里给的那条调用路径走一遍:输入怎么进去、调用怎么写、出来的东西长什么样。
需要先说清楚的是,本文所有内容都来自 github.com/microsoft/VibeVoice 仓库里的文档与源码,我们没有下载权重、没有跑过推理,所以下面不会出现任何关于速度、显存、识别准确率的描述。
前置条件:这一步别跳
pyproject.toml 里写明 requires-python = ">=3.10",依赖项里对 transformers 同时写了下限与上限(一个不跨大版本的区间),其余列了 torch、accelerate、librosa、gradio 等。具体钉的是哪个版本以仓库当前的 pyproject.toml 为准,这类声明随版本变动,别照抄本文。
安装步骤 docs/vibevoice-asr.md 给的是两段。环境侧它推荐用 NVIDIA 的深度学习容器来管 CUDA,代码侧是:
git clone https://github.com/microsoft/VibeVoice.git
cd VibeVoice
pip install -e .
两个容易被忽略的点。
第一是 ffmpeg。 vibevoice/processor/audio_utils.py 里的 load_audio_use_ffmpeg 是直接 subprocess 调 ffprobe 读采样率、再调 ffmpeg 解码成单声道 s16le 的,也就是说 ffmpeg 与 ffprobe 必须在 PATH 上。文档的 Gradio 用法那一节里写的是 apt update && apt install ffmpeg -y,这是 Linux 容器里的装法;Windows 侧仓库里没有找到对应说明,得自己装好 ffmpeg 并确认命令行能直接调到 ffmpeg 与 ffprobe。另外 vibevoice/processor/vibevoice_asr_processor.py 在 import audio_utils 失败时会 warnings.warn 并回退到 soundfile 读文件——但 soundfile 并不在 pyproject.toml 的 dependencies 列表里,这两处放在一起看是对不上的,走到回退分支时得自己补装。
第二是 attention 实现。 demo/vibevoice_asr_inference_from_file.py 的 --attn_implementation 取值是 flash_attention_2、sdpa、eager、auto 四个,默认 auto。auto 的分支逻辑写得很直白:设备是 cuda 且 CUDA 可用时先 import flash_attn,导入失败就打印一行提示并落到 sdpa;非 cuda 设备(脚本注释写明 MPS/XPU/CPU 不支持 flash_attention_2)一律 sdpa。所以没装 flash-attn 不会报错中断,它自己会降级。同一个脚本里 dtype 也是按设备分的:mps、xpu、cpu 走 torch.float32,其余走 torch.bfloat16。
最短调用路径
命令行这条最短,docs/vibevoice-asr.md 原样给的是:
python demo/vibevoice_asr_inference_from_file.py --model_path microsoft/VibeVoice-ASR --audio_files [add an audio path here]
要看代码怎么组织的,就读 demo/vibevoice_asr_inference_from_file.py 里的 VibeVoiceASRBatchInference。它的构造函数做两件事:先 VibeVoiceASRProcessor.from_pretrained(model_path, language_model_pretrained_name="Qwen/Qwen2.5-7B") 拿 processor,再 VibeVoiceASRForConditionalGeneration.from_pretrained(...) 拿模型,两个类分别来自 vibevoice.processor.vibevoice_asr_processor 与 vibevoice.modular.modeling_vibevoice_asr。
这里有个反直觉的地方值得单独说。VibeVoiceASRProcessor.from_pretrained 里取 tokenizer 名字的写法是「先读 preprocessor_config.json 里的 language_model_pretrained_name,读不到才用调用方传进来的值」——用的是 or 短路。也就是说 demo 里显式写的那个名字,只在模型目录的 preprocessor_config.json 没有这个键时才生效。你照着 demo 改自己的加载代码时,别以为改这个参数一定能换 tokenizer。
真正的调用三步在 transcribe_batch 里:
inputs = self.processor(
audio=audio_inputs,
sampling_rate=None,
return_tensors="pt",
padding=True,
add_generation_prompt=True
)
output_ids = self.model.generate(**inputs, **generation_config)
transcription_segments = self.processor.post_process_transcription(generated_text)
以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。
输入在 processor 里被怎么处理
VibeVoiceASRProcessor.__call__ 接受的 audio 可以是文件路径字符串、np.ndarray、torch.Tensor,也可以是它们组成的列表(列表即 batch)。传路径时走 ffmpeg 读,读到的采样率与 target_sample_rate 不一致就用 librosa.resample 重采样;构造函数里 target_sample_rate 的默认值是 24000,normalize_audio 默认 True,开启后由 AudioNormalizer 先归一到 target_dB_FS(默认 -25)再做一次防削波缩放。这些都是仓库当前代码里的默认值,随版本可能变动。
批量跑目录时注意一个细节:--audio_dir 是按 audio_utils.py 里的 COMMON_AUDIO_EXTS 过滤扩展名的,而 --audio_files 直接把你给的路径塞进列表,不做扩展名过滤。
再往下是 prompt 的拼法,这一段决定了输出长什么样。_process_single_audio 会先按 math.ceil(len(audio_array) / self.speech_tok_compress_ratio) 算出 vae_tok_len(speech_tok_compress_ratio 默认 3200,docstring 注明是 encoder 各级压缩比的乘积),然后取 start / pad / end 三个特殊 token 的字面量,拼成「一个 start + vae_tok_len 个 pad + 一个 end」的占位串,代码原样是 [sp_start_token] + [sp_pad_token] * vae_tok_len + [sp_end_token]。
这三个 token 的来路值得单独留意,不然你去词表里搜会扑空。VibeVoiceASRProcessor._cache_special_tokens 的写法是「tokenizer 上有 speech_start_id / speech_pad_id / speech_end_id 属性就直接取,没有才回退去查 <|speech_start|> 这类字面量」。而 ASR 这条路走的 VibeVoiceASRTextTokenizerFast(vibevoice/modular/modular_vibevoice_text_tokenizer.py)恰好三个属性都定义了,它的 _add_vibevoice_special_tokens 里注册的是 <|object_ref_start|>、<|object_ref_end|>、<|box_start|> 三个 Qwen 已有的特殊 token,源码注释写的是 reusing vision tokens。也就是说 processor 里那行 <|speech_pad|> 只是属性缺失时的回退分支,走 ASR tokenizer 时根本不会用到——排查占位 token 时按它去搜词表只会一无所获。顺带一提,后面 acoustic_input_mask 就是按 token == self.speech_pad_id 逐个标出来的,pad token 认错,这个 mask 也跟着错。
真正点题的是紧跟其后的那句 user 后缀,代码里的 show_keys 写死为:
show_keys = ['Start time', 'End time', 'Speaker ID', 'Content']
后缀文本会把音频时长和这四个键一起写进去,让模型「按这些键转写」。传了 context_info 时,后缀换成带 with extra info: 的那一版——Gradio 界面上那个「Context Info」输入框走的就是这个参数,它的输入框提示语写的是填 hotwords、说话人姓名、话题一类的上下文,页面下方的使用说明里则写成「hotwords、专有名词、说话人姓名或技术术语」。
输出:说话人与时间戳落在哪几个字段
模型吐出来的是文本,结构化那一步在 post_process_transcription 里。它先把 JSON 抠出来:文本里有 ```json 代码块就取代码块内容,否则从第一个 [ 或 { 开始做括号计数找配对的收尾。解析出来之后按一张固定的映射表清洗键名:
| 模型输出里的键 | 清洗后的字段名 |
|---|---|
Start time / Start | start_time |
End time / End | end_time |
Speaker ID / Speaker | speaker_id |
Content | text |
这张表要看两处。一是它带别名:Start time 与 Start 都收,Speaker ID 与 Speaker 都收,但 prompt 里写死要求的是长的那一组。二是 Content 映射过去叫 text,不叫 content——demo 的打印函数里取的就是 seg.get('text', ''),你自己接下游时按 content 取会一直拿空串,这是最容易踩的一脚。
最终 transcribe_batch 返回的每条记录是 file、raw_text、segments、generation_time 四个键,segments 就是上面清洗过的列表。
边界:这些地方仓库写得很克制
解析失败是静默的。 post_process_transcription 遇到 json.JSONDecodeError 只 logger.warning 然后返回空列表;demo 里又套了一层 try/except,打印一句 Warning: Failed to parse structured output 就把 transcription_segments 置空。所以 segments 为空不等于没转写出内容,raw_text 里的原始文本还在,排查时先看它。
映射表以外的键会被丢弃。 清洗时只挑 key_mapping 里有的键组装 cleaned_item,模型若多吐了别的字段,不会出现在 segments 里。
时间戳的类型不保证。 清洗这一步只做键名映射,不做类型转换,值是模型输出什么就是什么。demo/vibevoice_asr_gradio_demo.py 里的处理很能说明问题:切分片段前先 isinstance(start_time, (int, float)) 判一次,不是数值就跳过;渲染文本时又写了 f"{start_time:.2f}" if isinstance(start_time, (int, float)) else str(start_time) 的双路兜底。仓库自己都做了两手准备,你接下游时也别默认它一定是浮点秒。
几处默认值对不上,直接调库时要留神。 命令行 --max_new_tokens 默认 32768,而 transcribe_batch 与 _prepare_generation_config 的函数签名默认是 512;transcribe_batch 的 top_p 默认 1.0,_prepare_generation_config 的签名默认 0.9。命令行走的是前者,你直接 import 类来用走的是后者。另外 main() 里 do_sample = args.temperature > 0,--temperature 默认 0.0,也就是默认走贪心解码,temperature 与 top_p 在不采样时压根不会被写进 generation config。以上都是仓库当前代码里的默认值,随版本可能变动。
use_streaming 这个参数现在没那么「流式」。 _process_single_audio 里对时长小于 60 秒的音频会把 use_streaming 自动置 False;而下面那段按 streaming 与否分别用 floor / ceil 计算 vae_tok_len 的代码是被注释掉的,当前两条路都走 ceil。
docstring 与实际返回不一致。 __call__ 的文档里列了 vae_tok_seqlens,但 _batch_encode 的代码注释写明 vae_tok_seqlens 与 speech_type 没有被放进 BatchEncoding,理由是它们不是模型输入。实际能拿到的字段以 model_input_names 属性为准,它返回 input_ids、attention_mask、acoustic_input_mask、speech_tensors、speech_masks。
微调方面,docs/vibevoice-asr.md 写明支持 LoRA,指向 finetuning-asr/README.md;另有 vLLM 路线的单独文档 docs/vibevoice-vllm-asr.md。这两条本文没有展开。
怎么确认自己配对了
不看快慢,只看结构对不对,按顺序过三关:
一是输入关。transcribe_batch 里本来就打印了 inputs['input_ids'].shape、inputs['speech_tensors'].shape、inputs['attention_mask'].shape 三个形状,跑通到这里说明音频读进来了、占位 token 拼上了。读文件这一步若报的是 ffprobe/ffmpeg 找不到,那是 PATH 的问题,不是模型的问题。
二是原文关。先打印 raw_text,确认它是 JSON 形态(带不带 ```json 外壳都行,post_process_transcription 两种都认)。
三是字段关。取一条 segments 里的记录,确认 start_time、end_time、speaker_id、text 四个键都在、text 非空。四个键齐了,「谁在什么时候说了什么」这条链路才算真的通了。这一步没通而 raw_text 有内容,问题在解析与键名,不在识别。
该项目持续更新,上面涉及的模块路径、参数名与默认值都以仓库最新内容为准。
本文依据 github.com/microsoft/VibeVoice 仓库与 Hugging Face 模型卡于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有下载权重、没有跑过推理、也没有做过训练,
因此不涉及显存占用、推理速度、识别准确率与音质的任何描述,也不与其它模型做比较或排名。
该项目持续更新,文中涉及的模块路径、配置字段与接口写法随版本变动,请以仓库最新内容为准。
仓库 README 的风险与限制一节写明:该模型仅供研究与开发用途, 未经进一步测试与开发不建议用于商业或真实场景,并特别提示了合成语音被用于伪造与虚假信息的风险。 使用合成语音时应遵守所在司法辖区的法律法规,并在分享 AI 生成内容时主动披露。