generate_tokenizer_files.py 做了什么:VibeVoice 上 vLLM 部署

2026-08-18

翻 VibeVoice 仓库的 vLLM 部署部分时,很容易把注意力全放在 vllm serve 那一长串参数上,结果卡在一个看起来无关的地方:模型权重明明下下来了,服务却起不来。仓库 docs/vibevoice-vllm-asr.md 的 Troubleshooting 一节把这种情况写得很直白,“Model not found” 条目下的两条处置是「确认模型目录里有 config.json 和模型权重」以及「Generate tokenizer files if missing」——也就是说,权重齐全和 tokenizer 文件齐全是两件事。

负责补齐后者的,是 vllm_plugin/tools/generate_tokenizer_files.py。这篇就沿着这个脚本的源码走一遍:它到底往目录里放了哪几个文件、每个文件被改了什么、以及什么时候你得再跑一次。本文讲的是 ASR 方向的 vLLM 部署链路,对应文档是 docs/vibevoice-vllm-asr.md,模型是 microsoft/VibeVoice-ASR(这是 vllm_plugin/scripts/start_server.py--model 的默认值,随版本可能变动)。

前置条件:这个脚本要什么

先看它的 import。文件顶部只有 argparsejsonosshutiltempfiletyping,没有 torch,也没有 vllm。真正的外部依赖出现在 download_qwen_tokenizer_files() 函数体内部:

try:
    from huggingface_hub import hf_hub_download
except ImportError:
    raise ImportError("Please install huggingface_hub: pip install huggingface_hub")

值得注意的是,pyproject.tomldependencies 列表里并没有直接列出 huggingface_hub,脚本自己写了这层 ImportError 兜底和安装提示。所以如果你不是在完整的部署环境里跑它,缺这个包是要自己补的。

Python 版本方面,pyproject.toml 写明 requires-python = ">=3.10"(这是仓库当前的声明,随版本可能变动)。

另外有个源码之间对不上的地方,顺手记下来。start_server.pyinstall_vibevoice() 执行的是 pip install -e /app[vllm],而 pyproject.toml[project.optional-dependencies] 里只定义了 streamingtts 这一个 extra,没有 vllm。这两处白纸黑字摆在一起就是不一致,仓库里我们没有找到对这个 extra 的其它说明,到此为止。

它生成哪六个文件

脚本主函数 generate_vibevoice_tokenizer_files() 的 docstring 直接写明了产出,一共六个,来源分成两类:

文件来源脚本对它做了什么
vocab.json从 Qwen 下载不改
merges.txt从 Qwen 下载不改
tokenizer.json从 Qwen 下载追加扩展 token
tokenizer_config.json从 Qwen 下载追加扩展 token、改 chat template、改 model_max_length
added_tokens.json本地生成tokenizer_config.json 反推出来
special_tokens_map.json本地生成按脚本内的常量写死

下载来源由常量 DEFAULT_QWEN_MODEL 决定,当前代码里是 "Qwen/Qwen2.5-7B",可以用 --qwen-model 覆盖(这是仓库当前代码里的默认值,随版本可能变动)。脚本注释解释了为什么用 2.5 而不是 2:注释写「These are NOT in base Qwen2-7B but ARE in Qwen2.5 and Qwen2-VL」,指的是 151646 起的那一段扩展 token,这是仓库源码自述的理由。

真正属于 VibeVoice 的新增部分只有三个:

VIBEVOICE_AUDIO_TOKENS = {
    "<|AUDIO|>": 151665,
    "<|audio_bos|>": 151666,
    "<|audio_eos|>": 151667,
}

这三个 id 是仓库当前代码里硬编码的值,随版本可能变动。

每一步在改什么

generate_vibevoice_tokenizer_files() 里的调用顺序是固定的,而且后面的步骤依赖前面的结果:

第一步download_qwen_tokenizer_files() 把上表前四个文件从 Hugging Face 拉到输出目录。

第二步patch_tokenizer_config()tokenizer_config.json,源码里编了号,一共五处:把 Qwen2.5 扩展 token 与三个音频 token 一起写进 added_tokens_decoder;把三个音频 token 追加进 additional_special_tokens;改 chat template(下一节单说);第四处是

config["model_max_length"] = 131072

注释写的是 for long audio support。这是仓库当前代码里写死的值,随版本可能变动;顺带一提,start_server.py 传给 vLLM 的 --max-model-len 默认是另一个值,两处并不是同一个东西,别把它们当成一回事。第五处最不起眼:add_bos_token 这个键缺失时会被补上,补的值是 False

added_tokens_decoder 时有个细节:并非所有 token 都标成 special。源码里 is_special<tool_call></tool_call>、几个 fim 系列以及 <|repo_name|><|file_sep|> 排除在外,注释说明「tool_call tokens are NOT special in Qwen2.5」。

第三步patch_tokenizer_json() 把同一批 token 追加进 tokenizer.jsonadded_tokens 数组,用已有 id 集合去重,is_special 的判定逻辑与上一步一致。

第四步generate_added_tokens_json() 读回刚写好的 tokenizer_config.json,把 added_tokens_decoder 翻成「token 文本 → id」的扁平映射写出去。所以第二步没写对,这一步就跟着错,顺序不能调。

第五步generate_special_tokens_map_json() 不读任何输入,按脚本里的常量拼出结果:eos_tokenpad_tokenunk_token 都是 <|endoftext|>additional_special_tokens 里除了三个音频 token,还额外塞了 <|object_ref_start|><|object_ref_end|><|box_start|>

最后这三个 token 不是随便挑的。vibevoice/modular/modular_vibevoice_text_tokenizer.py 里的 VibeVoiceASRTextTokenizerFast._add_vibevoice_special_tokens() 正是把它们分别缓存成 speech_start_idspeech_end_idspeech_pad_id,源码注释写着 reusing vision tokens。同一个文件里还有一个非 ASR 的 VibeVoiceTextTokenizer,它的 speech_start_id / speech_end_id 取的是 <|vision_start|> / <|vision_end|>——同一个仓库里两套映射不一样,读代码时看清你手上是哪个类。

边界:几处要留神的地方

chat template 是「尽量原地打补丁」,不是直接替换。 patch_tokenizer_config() 的第三步先判断 if chat_template and "<|AUDIO|>" not in chat_template,然后用一段正则去匹配模板里处理 part['type'] == 'text' 的位置,在其后插入 audio 分支。如果正则没匹配上,源码走的是 fallback:打印 Warning: Could not patch existing template, using predefined template,改用脚本里预置的 VIBEVOICE_CHAT_TEMPLATE。这条 warning 混在一堆输出里很容易被刷过去,但它意味着你最终拿到的模板和上游的不是同一份。

还有一个更安静的分支:如果 tokenizer_config.jsonchat_template 为空字符串或缺失,上面那个条件为假,整个 chat template 环节直接跳过,什么都不打印。

patch_tokenizer_json() 的读写不对称。 收集已有 id 时有 if "added_tokens" in tokenizer 的判断,但追加时是直接 tokenizer["added_tokens"].append(...),没有对应的缺键兜底。

不传 --output 时,产物会被删掉。 main() 里如果 args.output 为空,输出目录取的是 tempfile.mkdtemp(prefix="vibevoice_tokenizer_"),并且 cleanup = not args.comparefinally 块里满足条件就 shutil.rmtree。也就是说,只想看看效果、既没给 --output 也没给 --compare 的话,跑完目录就没了。

它是往模型目录里写。 start_server.pygenerate_tokenizer()--output 指向的是 snapshot_download() 返回的模型快照路径,等于在 Hugging Face 缓存目录里原地改文件。这一点在清缓存、换机器、重建容器时会直接反映出来。

--compare 的比对深度有限。 compare_text_files()merges.txt 这类文本只报行数差异和第一处不同的行号;compare_with_reference() 每个文件最多打印前五条差异,剩下的只给一个计数。

怎么验证配对了

脚本自带的验证方式就是 --compare,把生成结果和一份参考目录逐文件比,输出 IDENTICALDIFFERENT

python generate_tokenizer_files.py --output /path/to/output [--compare /path/to/reference]

这行是脚本文件头 docstring 里的 Usage 原文,方括号表示 --compare 可选。Windows 上路径分隔符不同,命令换成模块形式更省事:

python -m vllm_plugin.tools.generate_tokenizer_files --output <你的项目目录>\vibevoice-asr

模块形式 -m vllm_plugin.tools.generate_tokenizer_files 来自 start_server.pygenerate_tokenizer() 的调用,用它需要在仓库根目录下、且包能被导入。以上为按仓库文档中的参数语义组合的示例,未经实测,以仓库最新内容与 --help 的实际输出为准。

如果手边没有参考目录,还有一条从下游倒着看的路子。vllm_plugin/model.pyget_audio_token_info() 是直接从 tokenizer.get_vocab() 里查 <|AUDIO|><|audio_bos|><|audio_eos|> 的 id;_get_prompt_updates() 里再查 <|object_ref_start|><|object_ref_end|><|box_start|>

这两批 token 查不到时的待遇并不一样,值得看清楚。三个 speech token 各自写了一条 or 串起来的兜底链:先查 <|object_ref_start|> 这类名字,查不到就退到 tokenizer 对象上的 speech_start_id 属性,再查不到就退到 <|speech_start|> 这个备用名。而 <|AUDIO|> 没有这层兜底,源码里紧接着的一句是:

if audio_token_id is None:
    return []

也就是不做任何 prompt 替换,直接返回空。所以「文件生成过了」和「vocab 里确实查得到这些名字」是两个不同的检查点,前者只能靠列目录确认,后者要看词表内容。

什么时候需要重新生成

按仓库里能查到的线索,有这么几种情况:

一是每次走 start_server.py 的完整流程时它本来就会跑——脚本注释把「Generating tokenizer files」列为第 4 步,main() 里由 --skip-tokenizer 控制是否跳过。换句话说,正常按文档启动的话你不用手动跑;只有在加了 --skip-tokenizer、或者绕开这个启动脚本自己拉模型时,才需要自己补。

二是文档 Troubleshooting 里那条:模型目录看着齐全但服务报 “Model not found”,处置之一就是补生成 tokenizer 文件。

三是输出目录被动过。前面说了它写的是模型快照目录,清过 Hugging Face 缓存、重新 snapshot_download、或者换了模型路径之后,之前打的补丁不会跟着走。

四是你改了 --qwen-model。这六个文件里有四个是从那个仓库下载后再打补丁的,换基座就得重来一遍。

至于「哪个 VibeVoice 模型版本必须配哪一套 token id」这类对应关系,仓库里我们没有找到相关说明,只能以你手上模型目录里 config.json 与实际词表为准。


本文依据 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 生成内容时主动披露。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。