给 VibeVoice-ASR 做 LoRA 微调:官方微调代码怎么用

2026-08-18

手上有一堆自家业务的录音——客服通话、内部会议、行业访谈——通用模型转出来八成能听懂,但一到专有名词、人名、产品型号就开始飘。你已经有对应的转写稿,甚至有分段和说话人标记,很自然会想:能不能拿这批数据把模型往自己的领域上拽一拽。

VibeVoice 仓库里有一个 finetuning-asr/ 目录,放的就是 VibeVoice-ASR 的 LoRA 微调脚本。这篇文章不讲 LoRA 的原理,只讲这份代码要你把数据摆成什么样、脚本认哪些参数、跑完之后目录里会多出什么、以及哪些东西在命令行里根本改不动。结论主要来自 finetuning-asr/README.mdfinetuning-asr/lora_finetune.pyfinetuning-asr/inference_lora.py 这三个文件,涉及环境的部分另外引用了 pyproject.tomldocs/vibevoice-asr.md

一、前置条件:别跳过这一段

finetuning-asr/README.md 的 Requirements 节只有两行:

# Install vibevoice first
pip install -e .

pip install peft

也就是说,微调依赖是在本体之外单独装的——peft 不在 pyproject.tomldependencies 里,你不显式装它,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.pysetup_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"),这个参数值同样没有暴露成命令行参数。

关于 Windowsdocs/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.pyVibeVoiceASRDataset._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_pathDataArgumentsdata_dir / max_audio_length / use_customized_contextLoraArgumentslora_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() 里是顺着写下来的:

  1. 加载 processor 和 VibeVoiceASRForConditionalGeneration
  2. 遍历 model.named_parameters(),把名字里含 acoustic_tokenizersemantic_tokenizer 的参数 requires_grad = False 冻掉——代码注释说明意图是只微调语言模型部分;
  3. get_peft_model(model, lora_config) 套上 LoRA,然后调 model.print_trainable_parameters()

get_lora_config()target_modules 的默认值是 q_projk_projv_projo_projgate_projup_projdown_proj 这七个,配上 bias="none"task_type=TaskType.CAUSAL_LM。这是仓库当前代码里的默认值,随版本可能变动。

四、边界:这些地方命令行改不动

  • target_modules 没有命令行入口get_lora_config() 虽然留了 target_modules 形参,但 train() 调它时只传了 r / lora_alpha / lora_dropoutLoraArguments 里也没有对应字段。要换目标层只能改代码。
  • 两个 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,推理用左 paddingVibeVoiceASRDataCollator.__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.pyload_lora_model() 先照常 from_pretrained 加载基座模型,再 PeftModel.from_pretrained(model, lora_path) 把适配器挂上去,最后 model.eval()。注意脚本里 model = model.merge_and_unload() 那一行是被注释掉的,默认走的是挂载而非合并。--lora_path--audio_fileargparse 里都标了 required=True

跑完之后怎么判断”配对了”:transcribe() 返回的字典里有 raw_textsegments 两项,后者来自 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 原文与你所在组织的要求为准,本文不构成法律意见。

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