给 VibeVoice-ASR 做 LoRA 微调:官方微调代码怎么用
手上有一堆自家业务的录音——客服通话、内部会议、行业访谈——通用模型转出来八成能听懂,但一到专有名词、人名、产品型号就开始飘。你已经有对应的转写稿,甚至有分段和说话人标记,很自然会想:能不能拿这批数据把模型往自己的领域上拽一拽。
VibeVoice 仓库里有一个 finetuning-asr/ 目录,放的就是 VibeVoice-ASR 的 LoRA 微调脚本。这篇文章不讲 LoRA 的原理,只讲这份代码要你把数据摆成什么样、脚本认哪些参数、跑完之后目录里会多出什么、以及哪些东西在命令行里根本改不动。结论主要来自 finetuning-asr/README.md 与 finetuning-asr/lora_finetune.py、finetuning-asr/inference_lora.py 这三个文件,涉及环境的部分另外引用了 pyproject.toml 与 docs/vibevoice-asr.md。
一、前置条件:别跳过这一段
finetuning-asr/README.md 的 Requirements 节只有两行:
# Install vibevoice first
pip install -e .
pip install peft
也就是说,微调依赖是在本体之外单独装的——peft 不在 pyproject.toml 的 dependencies 里,你不显式装它,lora_finetune.py 开头那句 from peft import (LoraConfig, get_peft_model, ...) 就直接崩在导入阶段。
除此之外,还有几处是从代码里读出来、README 没有单独强调的硬前提:
- Python 版本:
pyproject.toml写明requires-python = ">=3.10"。同一份文件还给transformers设了带上下界的版本约束,另有一个名为streamingtts的 optional-dependencies 分组把transformers钉在一个精确版本上——具体数值以仓库当前声明为准,随版本变动。 - flash-attn 是写死的。
lora_finetune.py的setup_model_for_training()里加载模型时用的是attn_implementation="flash_attention_2",这个值是硬编码在函数里的,命令行没有开关可以换成别的实现。docs/vibevoice-asr.md的 Installation 节也提到,如果你的环境里没带 flash attention,需要按 Dao-AILab/flash-attention 的说明自行安装。 - tokenizer 来源也是写死的。两个脚本里加载 processor 都是
VibeVoiceASRProcessor.from_pretrained(model_path, language_model_pretrained_name="Qwen/Qwen2.5-7B"),这个参数值同样没有暴露成命令行参数。
关于 Windows:docs/vibevoice-asr.md 的 Installation 节推荐用 NVIDIA 的 Deep Learning Container 管理 CUDA 环境,给出的是 sudo docker run ... nvcr.io/nvidia/pytorch:... 这条 Linux 侧的命令;训练命令用的是 torchrun,Gradio demo 那节还有一句 apt update && apt install ffmpeg -y。仓库里我们没有找到针对 Windows 原生环境的安装或训练说明。 Windows 侧要跑,现实路径是走 WSL2 或容器,但这属于通用做法、不是该项目的官方内容,具体环境请自行评估。
二、数据要摆成什么样
这一步最容易返工,先说清楚。README.md 的 Data Format 节要求音频文件和 JSON 标注同名、同目录:
toy_dataset/
├── 0.mp3
├── 0.json
├── 1.mp3
├── 1.json
└── ...
JSON 的结构照抄 README:
{
"audio_duration": 351.73,
"audio_path": "0.mp3",
"segments": [
{
"speaker": 0,
"text": "Hey everyone, welcome back...",
"start": 0.0,
"end": 38.68
}
],
"customized_context": ["Tea Brew", "Aiden Host", "The property is near Meter Street."]
}
上面的具体取值是仓库自带示例里的值,不是什么推荐配置。真正要记住的是字段语义,这些可以在 lora_finetune.py 的 VibeVoiceASRDataset._load_samples() 里逐条对上:
audio_path是必填的。_load_samples()用self.data_dir.glob("*.json")排序遍历所有 JSON,取data.get("audio_path");取不到就logger.warning一句然后continue。注意它是以 JSON 为准去找音频,不是反过来,所以你目录里躺着一个没有配 JSON 的 mp3,脚本压根不会看它一眼。- 音频文件按
data_dir / audio_filename拼路径,文件不存在同样是 warning 加跳过。这两种跳过都不报错,你只会在日志里看到样本数变少了。 audio_duration只在过滤时被用到。传了--max_audio_length时,_load_samples()会拿data.get("audio_duration", float("inf"))跟阈值比,超了就跳过。这里有个反直觉的点:默认值是无穷大,所以如果你的 JSON 漏写了audio_duration,配上--max_audio_length之后这条样本必然被跳过。customized_context是可选的,README 注释里写的是 domain-specific terms or context sentences。__getitem__()里的处理是"\n".join(customized_context),然后作为context_info传给 processor——是换行拼接,不是逗号拼接。
还有一处容易踩:JSON 里的键名和模型要学的输出格式不是同一套。_format_transcription() 会把 segments 重新组装成 Start / End / Speaker / Content 四个首字母大写的键,再用 json.dumps(..., ensure_ascii=False, separators=(',', ':')) 序列化成紧凑字符串。也就是说你写标注用小写的 start / end / speaker / text,脚本负责翻译成大写那套作为训练目标。顺带一提,_format_transcription() 的签名里收了 audio_duration 这个参数,但函数体里并没有用到它。
toy_dataset/ 本身别当数据集用。README 的 Note 写得很直白:那是 VibeVoice TTS 合成的音频,仅供演示,不是完整的微调数据集;用自己的数据时要准备真实录音与准确转写,并根据数据规模和领域调整学习率、epoch 数与 LoRA rank。
三、训练怎么起
README 的 Basic 一节给的单卡命令:
torchrun --nproc_per_node=1 lora_finetune.py \
--model_path microsoft/VibeVoice-ASR \
--data_dir ./toy_dataset \
--output_dir ./output \
--num_train_epochs 3 \
--per_device_train_batch_size 1 \
--learning_rate 1e-4 \
--bf16 \
--report_to none
这条命令原样抄自 README,里面的 epoch 数、batch size、学习率都只是仓库示例里的取值,不是针对你的数据的推荐配置。多卡那条只是把 --nproc_per_node 改成对应数量并前置 CUDA_VISIBLE_DEVICES,其余参数一模一样。
参数从哪来,看 main() 就明白了:它用 HfArgumentParser((ModelArguments, DataArguments, LoraArguments, TrainingArguments)) 一次性解析四个 dataclass。前三个是这个脚本自己定义的——ModelArguments 只有 model_path,DataArguments 是 data_dir / max_audio_length / use_customized_context,LoraArguments 是 lora_r / lora_alpha / lora_dropout;第四个是 HuggingFace 的 TrainingArguments,所以 --num_train_epochs、--save_steps、--bf16、--report_to 这些都不是这个脚本自己实现的,README 里也是这么说的(“so all standard options are available”)。搞清楚这条分界,你才知道该去哪份文档查某个参数。
模型侧发生了什么,setup_model_for_training() 里是顺着写下来的:
- 加载 processor 和
VibeVoiceASRForConditionalGeneration; - 遍历
model.named_parameters(),把名字里含acoustic_tokenizer或semantic_tokenizer的参数requires_grad = False冻掉——代码注释说明意图是只微调语言模型部分; get_peft_model(model, lora_config)套上 LoRA,然后调model.print_trainable_parameters()。
get_lora_config() 里 target_modules 的默认值是 q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj 这七个,配上 bias="none" 与 task_type=TaskType.CAUSAL_LM。这是仓库当前代码里的默认值,随版本可能变动。
四、边界:这些地方命令行改不动
target_modules没有命令行入口。get_lora_config()虽然留了target_modules形参,但train()调它时只传了r/lora_alpha/lora_dropout,LoraArguments里也没有对应字段。要换目标层只能改代码。- 两个
TrainingArguments会被脚本强行覆盖。train()里直接写了training_args.dataloader_num_workers = 0(注释说音频加载配多进程容易出问题)和training_args.remove_unused_columns = False。你在命令行传这两个是不生效的。 --gradient_checkpointing这里有个不一致,值得留意。README 的 Key Parameters 表把它列为默认False;但代码里,main()调train()时并没有把这个开关透传过去,而train()自己的gradient_checkpointing形参默认是True,并原样传给setup_model_for_training(),触发model.enable_input_require_grads()与model.gradient_checkpointing_enable()。README 与代码这两处写的不是一回事,以仓库最新代码为准。- 训练用右 padding,推理用左 padding。
VibeVoiceASRDataCollator.__call__()里有一句注释点明了这件事:processor 在推理/生成时用左 padding,训练走右 padding。它同时会把labels里输入部分填成-100——__getitem__()返回的labels就是[-100] * len(input_ids) + target_tokens,损失只落在目标转写上。 - 样本数为零时直接返回。
train()里if len(train_dataset) == 0会打一条logger.error("No training samples found!")然后return,不抛异常。配合前面说的静默跳过,数据没配对上的表现就是”跑了一下什么也没发生”。 - 至于要多少数据、训多久合适,README 只给了”根据数据规模和领域调整”这一句,仓库里没有找到更具体的说明。
五、产物长什么样,怎么验证
训练结束时 train() 依次做四件事:trainer.save_model(training_args.output_dir) 保存模型、trainer.log_metrics("train", metrics) 与 trainer.save_metrics("train", metrics) 落训练指标、processor.save_pretrained(training_args.output_dir) 把 processor 配置也写进同一个目录。所以 --output_dir 指向的是一个既有 LoRA 适配器权重、又有 processor 配置的目录,不是一份完整的基座模型。
这一点在推理侧能对上:README 给的验证命令是
python inference_lora.py \
--base_model microsoft/VibeVoice-ASR \
--lora_path ./output \
--audio_file ./toy_dataset/0.mp3 \
--context_info "Tea Brew, Aiden Host"
inference_lora.py 的 load_lora_model() 先照常 from_pretrained 加载基座模型,再 PeftModel.from_pretrained(model, lora_path) 把适配器挂上去,最后 model.eval()。注意脚本里 model = model.merge_and_unload() 那一行是被注释掉的,默认走的是挂载而非合并。--lora_path 和 --audio_file 在 argparse 里都标了 required=True。
跑完之后怎么判断”配对了”:transcribe() 返回的字典里有 raw_text 和 segments 两项,后者来自 processor.post_process_transcription(generated_text);这个方法会先找 ```json 代码块、找不到再退回去找 [ 来定位 JSON。解析失败时脚本不会崩,只打印一句 Warning: Failed to parse structured output 并把 segments 置空。所以看输出的时候要分开看:raw_text 有内容说明生成链路通了,segments 非空才说明输出的结构符合训练时那套 Start / End / Speaker / Content 的格式。
想把适配器并进基座,README 的 Merging 一节给的是 peft 的标准写法:PeftModel.from_pretrained(model, "./output") 之后 model.merge_and_unload(),再 model.save_pretrained("./merged_model")。README 把这一节标为 Optional。
以上代码路径与参数均按仓库文件转述,未经实测,以仓库最新代码为准。该项目持续更新,文中的文件路径、参数名与默认值随版本变动。
本文依据 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 生成内容时主动披露。
许可条款请以仓库 LICENSE 原文与你所在组织的要求为准,本文不构成法律意见。