vLLM 语音转文本(ASR)模型接入指南:完整实现 SupportsTranscription 接口
本文讲解如何在 vLLM 中为自研的语音识别(ASR)/语音翻译模型接入 /v1/audio/transcriptions 与 /v1/audio/translations OpenAI 兼容 API。核心路径是让模型类实现 SupportsTranscription 接口,声明语言映射、提供 SpeechToTextConfig、构造生成 prompt;读完后你可以独立完成一个 ASR 模型在 vLLM 中的接入,理解服务端音频重采样、切块与语言校验的底层流程,并掌握用 curl 与官方客户端脚本验证接口的完整方法。
一、接口总览:SupportsTranscription 协议
vLLM 通过 vllm/model_executor/models/interfaces.py 中的 SupportsTranscription Protocol 定义 ASR 模型需要满足的契约(见 interfaces.py#L1302-L1488)。协议包含以下几类成员:
- 必选类属性:
supported_languages(ISO 639-1 语言码到语言名的映射)、supports_transcription(固定为True); - 能力开关类属性:
supports_transcription_only(模型只做转写、不做纯文本生成,如 Whisper)、supports_segment_timestamp(支持 segment 时间戳输出)、supports_diarized_transcription(支持diarized_json说话人分离格式)、supports_explicit_language_detection(需要额外前向传播做显式语言检测,如 Whisper 的语言 token 预测); - 方法:
get_generation_prompt(由SpeechToTextParams构造 prompt,必选)、get_speech_to_text_config(返回服务端行为配置,必选)、validate_language(语言校验,可选覆盖)、get_num_audio_tokens(时长→音频 token 数的快速估算,可选); - 源码中另外提供的默认实现:
post_process_output(清洗模型原始输出,默认原样返回)、get_streaming_post_processor_cls(流式输出的有状态后处理器)、以及语言检测三件套get_language_detection_prompt/parse_language_detection_output/get_language_token_ids(仅当supports_explicit_language_detection=True时实现)。
模型类继承该接口后,无需任何额外注册:API 服务端的 SpeechToTextBaseServing 会通过模型注册表拿到你的类并直接调用上述类方法。
二、声明语言支持与能力标志
supported_languages 与 supports_transcription_only
从源码看,supported_languages 会在类定义时(而非实例化时)被强制校验:Protocol 的 __init_subclass__ 会把映射中的每个语言码与完整语言表 LANGUAGES 做差集,出现非法语言码立即抛出 ValueError 并列出全部合法选项(见 interfaces.py#L1339-L1349)。因此语言码必须使用 ISO 639-1 标准编码:
from typing import ClassVar, Mapping, Literal
import numpy as np
import torch
from torch import nn
from vllm.config import ModelConfig, SpeechToTextConfig
from vllm.inputs import PromptType
from vllm.model_executor.models.interfaces import SupportsTranscription
class YourASRModel(nn.Module, SupportsTranscription):
# ISO 639-1 语言码 -> 语言名 的映射
supported_languages: ClassVar[Mapping[str, str]] = {
"en": "English",
"it": "Italian",
# ... 按需追加
}
# 若模型仅支持音频条件生成(不支持纯文本生成),置为 True
supports_transcription_only: ClassVar[bool] = True
设置 supports_transcription_only=True 后,模型只会暴露 transcription 任务而不会提供文本生成(例如 Whisper 这类音频专用模型)。
三、提供 ASR 配置:get_speech_to_text_config
该 classmethod 返回一个 SpeechToTextConfig 实例,用于控制 API 服务端在服务你的模型时的一般行为:
class YourASRModel(nn.Module, SupportsTranscription):
...
@classmethod
def get_speech_to_text_config(
cls,
model_config: ModelConfig,
task_type: Literal["transcribe", "translate"],
) -> SpeechToTextConfig:
return SpeechToTextConfig(
sample_rate=16_000,
max_audio_clip_s=30,
# 如果模型/processor 已自行处理分块,置 None 关闭服务端切块
min_energy_split_window_size=None,
)
结合 speech_to_text.py 中的定义,各字段含义与默认值如下:
| 字段 | 默认值 | 含义 |
|---|---|---|
sample_rate |
16_000 |
输入音频统一重采样到的采样率(Hz)。绝大多数语音模型期望 16kHz 输入,服务端会在处理前自动重采样 |
max_audio_clip_s |
30 |
单个音频片段在无切块情况下的最大时长(秒)。超过该时长且 allow_audio_chunking 为 True 时会被切块,否则被拒绝;None 表示时长不限且不切块 |
overlap_chunk_second |
1 |
切长音频时相邻 chunk 之间用于寻找切分点的搜索窗口秒数,帮助在分块边界保持上下文(源码注释说明其用于改善分块边界处的转写质量) |
min_energy_split_window_size |
1600 |
寻找低能量(安静)区域以最小化切断单词的样本窗口大小;默认 1600 样本约等于 16kHz 下的 100ms。设为 None 则完全不做切块 |
其中 allow_audio_chunking 是一个派生属性:当且仅当 min_energy_split_window_size 与 max_audio_clip_s 都非 None 时为 True(见 speech_to_text.py#L80-L85)。也就是说,文档中的建议——如果你的模型或 processor 已内部处理切块,就把 min_energy_split_window_size=None 放进返回的 SpeechToTextConfig——在服务端会直接关闭切块逻辑。
四、构造生成 Prompt:get_generation_prompt 的两种模式
服务端会把重采样后的波形、任务参数与请求级选项打包成一个 SpeechToTextParams 对象,你的模型只接收这一个对象并返回合法的 PromptType。SpeechToTextParams 的完整字段为:
| 字段 | 类型 | 说明 |
|---|---|---|
audio |
np.ndarray |
单个 chunk 重采样后的波形 |
stt_config |
SpeechToTextConfig |
服务端级语音转文本配置 |
model_config |
ModelConfig |
模型配置 |
language |
str | None |
经过校验/自动检测的 ISO 639-1 语言码 |
hotwords |
str | None |
转写时应重点关注的词/短语 |
task_type |
str |
"transcribe" 或 "translate" |
request_prompt |
str |
引导模型的可选文本 prompt |
to_language |
str | None |
翻译目标语言(依模型而定) |
这种设计的好处是:未来新增 API 级参数只需扩展 SpeechToTextParams,无需改动 get_generation_prompt 的函数签名。
模式一:多模态 LLM + 音频嵌入(如 Voxtral、Gemma3n)
返回包含 multi_modal_data(携带音频)与 prompt 字符串(或 prompt_token_ids)的 dict:
from vllm.config.speech_to_text import SpeechToTextParams
class YourASRModel(nn.Module, SupportsTranscription):
...
@classmethod
def get_generation_prompt(
cls,
stt_params: SpeechToTextParams,
) -> PromptType:
audio = stt_params.audio
stt_config = stt_params.stt_config
task_type = stt_params.task_type
task_word = "Transcribe" if task_type == "transcribe" else "Translate"
prompt = (
"<start_of_turn>user\n"
f"{task_word} this audio: <audio_soft_token>"
"<end_of_turn>\n<start_of_turn>model\n"
)
return {
"multi_modal_data": {"audio": (audio, stt_config.sample_rate)},
"prompt": prompt,
}
多模态输入的其他细节可参考 多模态输入文档。
模式二:Encoder–Decoder 纯音频模型(如 Whisper)
返回分别包含 encoder_prompt 与 decoder_prompt 的 dict:
from vllm.config.speech_to_text import SpeechToTextParams
class YourASRModel(nn.Module, SupportsTranscription):
...
@classmethod
def get_generation_prompt(
cls,
stt_params: SpeechToTextParams,
) -> PromptType:
audio = stt_params.audio
stt_config = stt_params.stt_config
language = stt_params.language
task_type = stt_params.task_type
request_prompt = stt_params.request_prompt
if language is None:
raise ValueError("Language must be specified")
prompt = {
"encoder_prompt": {
"prompt": "",
"multi_modal_data": {
"audio": (audio, stt_config.sample_rate),
},
},
"decoder_prompt": (
(f"<|prev|>{request_prompt}" if request_prompt else "")
+ f"<|startoftranscript|><|{language}|>"
+ f"<|{task_type}|><|notimestamps|>"
),
}
return cast(PromptType, prompt)
五、可选扩展点
validate_language:语言校验与默认语言
基类默认实现(interfaces.py#L1366-L1389)的行为是:请求语言为 None 或在 supported_languages 内则原样通过;若语言属于 Whisper 语言表但模型不原生支持,仅告警不报错;其他情况抛 ValueError。如果你的模型要求必须指定语言且希望提供默认值,可覆盖该方法(见 Whisper 的实现):
@classmethod
def validate_language(cls, language: str | None) -> str | None:
if language is None:
logger.warning(
"Defaulting to language='en'. If you wish to transcribe "
"audio in a different language, pass the `language` field "
"in the TranscriptionRequest."
)
language = "en"
return super().validate_language(language)
get_num_audio_tokens:免前向传播的 token 估算
该方法的文档字符串明确说明其用途是"不运行前向传播,把音频时长映射为 ASR 模型将产生的音频 token 数,用于估算该音频的处理量"。实现它可以提升流式请求 usage 统计中 prompt_tokens 的准确性:
class YourASRModel(nn.Module, SupportsTranscription):
...
@classmethod
def get_num_audio_tokens(
cls,
audio_duration_s: float,
stt_config: SpeechToTextConfig,
model_config: ModelConfig,
) -> int | None:
# 未知时返回 None,否则返回估算值
return int(audio_duration_s * stt_config.sample_rate // 320) # 示例
协议中还提供了若干可按需覆盖的钩子,与文档主题相关的包括:post_process_output(剥离语言标签、特殊 token 等结构化输出)、get_streaming_post_processor_cls(流式 delta 的有状态清洗器,配合多 chunk 拼接使用),以及 supports_explicit_language_detection=True 时需要的三个语言检测方法——服务端的 _detect_language 会用 temperature=0.0、max_tokens=1 且 allowed_token_ids 受限的采样参数执行一次独立前向来完成自动语言检测(见 serving.py#L210-L259)。
六、音频预处理与切块:服务端实际做了什么
API 服务器在构造 prompt 之前负责基础音频 I/O 与可选切块。当前仓库中该逻辑位于 SpeechToTextBaseServing._decode_and_chunk_speech:
# vllm/entrypoints/speech_to_text/base/serving.py(简化自 _decode_and_chunk_speech)
y, sr = load_audio(
buf,
sr=self.asr_config.sample_rate, # 重采样到模型采样率
mono=True,
max_duration_s=self.max_audio_decode_duration_s,
max_decode_bytes=self.max_audio_decode_bytes,
)
duration = get_audio_duration(y=y, sr=sr)
do_split_audio = self.asr_config.allow_audio_chunking and (
self.asr_config.max_audio_clip_s is not None
and duration > self.asr_config.max_audio_clip_s
)
chunks = [y] if not do_split_audio else split_audio(
audio_data=y,
sample_rate=int(sr),
max_clip_duration_s=self.asr_config.max_audio_clip_s,
overlap_duration_s=self.asr_config.overlap_chunk_second,
min_energy_window_size=self.asr_config.min_energy_split_window_size,
)
要点归纳(与文档描述一一对应,并有源码佐证):
- 重采样:
load_audio直接解码并重采样到SpeechToTextConfig.sample_rate。源码注释解释这样做的原因:既高效,又是切块算法的前置条件(切块按 Whisper 采样率假设计算);对 MP4/M4A/WebM 等 soundfile 无法从 BytesIO 识别的容器格式,会透明回退到 ffmpeg 解码。 - 解码防护:解码受
VLLM_MAX_AUDIO_DECODE_DURATION_S与VLLM_MAX_AUDIO_DECODE_BYTES两个环境变量上限约束,非法音频抛出ValueError("Invalid or unsupported audio file.")。 - 切块:当
allow_audio_chunking为 True 且时长超过max_audio_clip_s时调用split_audio(来自 vllm/multimodal/audio.py),每个 chunk 随后各自构建一个SpeechToTextParams与 prompt,即"一个 chunk 一个 prompt"。 - 能量感知切分:当
min_energy_split_window_size设置时,切块算法在该窗口内寻找能量最低(最安静)的时刻下刀,尽量避免切断词语;overlap_chunk_second控制用于定位切分点的搜索窗口大小。 - 预处理线程池:从源码结构看,音频解码/切块不与其他前端逻辑共享线程池,而是由
VLLM_MAX_AUDIO_PREPROCESS_WORKERS控制独立线程池(注释指明是为了更高吞吐),并经由带信号量的 async 包装器提交,避免阻塞事件循环。 - chunk 之间的文本拼接:非流式/流式输出的 chunk 文本用
asr_inter_chunk_separator连接——语言码在no_space_languages(默认{"ja", "zh"},即中日文)时用空串拼接,其余语言用单个空格(serving.py#L82-L90)。若你的模型对无空格语言有不同约定,可覆盖该 ClassVar。
七、任务自动暴露与 Handler 初始化
vLLM 会自动探测并广播转录支持,不需要任何额外注册。任务推断逻辑为:
if supports_transcription(model):
if model.supports_transcription_only:
return ["transcription"]
supported_tasks.append("transcription")
当 transcription 在支持任务列表中时,服务端同时初始化 transcribe 与 translate 两个 handler:
state.openai_serving_transcription = OpenAIServingTranscription(...) if "transcription" in supported_tasks else None
state.openai_serving_translation = OpenAIServingTranslation(...) if "transcription" in supported_tasks else None
实际实现中,OpenAIServingTranscription 定义在 vllm/entrypoints/speech_to_text/transcription/serving.py,两者共同继承 SpeechToTextBaseServing(构造时即以 task_type 区分,并调用 get_speech_to_text_config(model_config, task_type) 初始化配置)。除模型类在注册表中可被解析外,实现 SupportsTranscription 即完成全部接线。
八、树内参考实现
仓库中已有多个可直接对照的完整实现,覆盖两类模式:
- Whisper(encoder–decoder,纯音频):vllm/model_executor/models/whisper.py——encoder_prompt/decoder_prompt 双 prompt、语言必填校验(默认回落
en)、显式语言检测的典型样例; - Voxtral(decoder-only,音频嵌入 + LLM):vllm/model_executor/models/voxtral.py。注意需安装
mistral-common[audio]依赖; - Gemma3n(decoder-only,固定指令 prompt):vllm/model_executor/models/gemma3n_mm.py;
- Qwen3-Omni(多模态音频嵌入):vllm/model_executor/models/qwen3_omni_moe_thinker.py。
更多可运行模型清单见 支持模型列表。
九、用 API 验证你的模型
模型实现 SupportsTranscription 后,可直接测试两个端点(API 模仿 OpenAI 格式):
- 转写(ASR):
curl -s -X POST \
-H "Authorization: Bearer $VLLM_API_KEY" \
-H "Content-Type: multipart/form-data" \
-F "file=@/path/to/audio.wav" \
-F "model=$MODEL_ID" \
http://localhost:8000/v1/audio/transcriptions
- 翻译(源语言 → 英语,除非模型支持其他目标语言):
curl -s -X POST \
-H "Authorization: Bearer $VLLM_API_KEY" \
-H "Content-Type: multipart/form-data" \
-F "file=@/path/to/audio.wav" \
-F "model=$MODEL_ID" \
http://localhost:8000/v1/audio/translations
仓库 examples/speech_to_text 目录提供了现成的 Python 客户端,可直接复用或改造:
- OpenAI 转写客户端:examples/speech_to_text/openai/openai_transcription_client.py
- OpenAI 翻译客户端:examples/speech_to_text/openai/openai_translation_client.py
- Realtime 麦克风/客户端示例:examples/speech_to_text/realtime/openai_realtime_microphone_client.py、examples/speech_to_text/realtime/openai_realtime_client.py
十、接入检查清单
- 若你的模型内部(processor/encoder)已处理切块,将返回的
SpeechToTextConfig中min_energy_split_window_size置为None,关闭服务端切块; - 实现
get_num_audio_tokens可免额外前向传播即提升流式 usage 指标(prompt_tokens)的准确性; - 多语言模型务必让
supported_languages与模型真实能力对齐——类定义时即会校验语言码合法性,运行期对"合法但不支持"的语言仅告警、对非法语言直接报错; supports_transcription_only=True的纯音频模型(如 Whisper)不要暴露文本生成任务;- 中日文等无空格语言如需特殊拼接行为,覆盖
no_space_languagesClassVar 即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00