首页
/ vLLM 语音转文本(ASR)模型接入指南:完整实现 SupportsTranscription 接口

vLLM 语音转文本(ASR)模型接入指南:完整实现 SupportsTranscription 接口

2026-09-06 17:37:30作者:滑思眉Philip

本文讲解如何在 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_languagessupports_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_sizemax_audio_clip_s 都非 None 时为 True(见 speech_to_text.py#L80-L85)。也就是说,文档中的建议——如果你的模型或 processor 已内部处理切块,就把 min_energy_split_window_size=None 放进返回的 SpeechToTextConfig——在服务端会直接关闭切块逻辑。

四、构造生成 Prompt:get_generation_prompt 的两种模式

服务端会把重采样后的波形、任务参数与请求级选项打包成一个 SpeechToTextParams 对象,你的模型只接收这一个对象并返回合法的 PromptTypeSpeechToTextParams 的完整字段为:

字段 类型 说明
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_promptdecoder_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.0max_tokens=1allowed_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_SVLLM_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 即完成全部接线。

八、树内参考实现

仓库中已有多个可直接对照的完整实现,覆盖两类模式:

更多可运行模型清单见 支持模型列表

九、用 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 客户端,可直接复用或改造:

十、接入检查清单

  • 若你的模型内部(processor/encoder)已处理切块,将返回的 SpeechToTextConfigmin_energy_split_window_size 置为 None,关闭服务端切块;
  • 实现 get_num_audio_tokens 可免额外前向传播即提升流式 usage 指标(prompt_tokens)的准确性;
  • 多语言模型务必让 supported_languages 与模型真实能力对齐——类定义时即会校验语言码合法性,运行期对"合法但不支持"的语言仅告警、对非法语言直接报错;
  • supports_transcription_only=True 的纯音频模型(如 Whisper)不要暴露文本生成任务;
  • 中日文等无空格语言如需特殊拼接行为,覆盖 no_space_languages ClassVar 即可。
登录后查看全文
热门项目推荐
相关项目推荐