本教程系统讲解自动语音识别(ASR)技术,详细介绍 OpenMOSS 团队开源的端到端语音转写模型 MOSS-Transcribe-Diarize 0.9B 及其 vLLM 高性能部署方案。通过完整示例项目 voice2text,你将学会在 WSL2 中用 vLLM 部署该模型,用 FastAPI 构建语音识别后端服务,实现"用户上传音频或用麦克风录音,输出识别文字",并对外暴露 OpenAI 兼容的语音识别接口;同时支持把识别结果转换为带时间轴的字幕,一键下载 SRT / VTT 字幕文件。教程内容覆盖 ASR 技术概览、MOSS-Transcribe-Diarize 模型详解、WSL2 环境准备与综合示例的完整链路,适合希望在本地搭建语音识别与字幕生成服务的开发者。
前置教程
如想快速开始学习本教程,你可能需要先完成以下前置教程:
- WSL2、Ubuntu与Conda安装教程,vLLM 转写服务部署在 WSL2 的 Ubuntu 中,需要你先具备 WSL2 与 conda 环境。
- Python+FastAPI在Windows环境下创建一个基础后端服务教程,本教程使用 FastAPI 构建语音识别后端服务,需要你掌握基础后端服务写法。
- OpenAI兼容API的概念与用法详解,本教程的示例服务对外暴露 OpenAI 兼容的语音识别接口,需要你了解其请求格式与设计规范。
- vLLM的安装与多卡部署大语言模型完整教程,本教程使用 vLLM 承载语音模型,需要你了解 vLLM 的基本用法。
资源下载
1. ASR 技术概览
1.1 什么是 ASR
ASR(Automatic Speech Recognition,自动语音识别) 是指将人类的语音音频自动转换为对应文字文本的技术,是人工智能语音领域的核心基础能力之一,与 TTS(文本转语音)恰好互为反向任务。
ASR 的应用场景非常广泛:
- 语音输入:手机输入法、语音笔记、会议纪要的自动转写
- 智能交互:语音助手、智能音箱、客服系统的语音理解前端
- 字幕生成:视频、直播、网课的自动字幕
- 语音翻译:先识别再翻译,构成语音翻译链路
- 无障碍服务:为听障人群提供实时字幕
现代 ASR 系统的核心处理流程如下:
采样率统一、降噪] B --> C[声学特征提取
如梅尔频谱] C --> D[声学模型与解码
音频映射为文字] D --> E[输出文本] classDef input fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px classDef process fill:#d5f5e3,stroke:#1e8449,stroke-width:2px classDef model fill:#ffecd6,stroke:#ca6f1e,stroke-width:2px classDef output fill:#ebdef0,stroke:#6c3483,stroke-width:2px class A input class B process class C,D model class E output
| 阶段 | 作用 | 说明 |
|---|---|---|
| 音频预处理 | 统一采样率、静音切除 | 保证输入质量一致 |
| 声学特征提取 | 将音频波形转为特征向量 | 常用梅尔频谱、特征滤波器组 |
| 声学模型与解码 | 将声学特征映射为文字序列 | 决定识别准确率与语种支持 |
ASR 的本质是"音频到文字"的映射,工程上的重点是选择合适的识别模型,并把它封装成易于调用的服务。
1.2 在线 ASR 与本地部署 ASR 的对比
调用 ASR 有两种主流方式:调用在线 ASR 服务 与 本地部署 ASR 模型。两者的对比如下:
| 对比项 | 在线 ASR 服务 | 本地部署 ASR(MOSS-Transcribe-Diarize) |
|---|---|---|
| 联网要求 | 需要联网调用云端服务 | 无需联网,完全离线可用 |
| 调用成本 | 按量计费或订阅 | 模型开源免费,需自备硬件 |
| 数据隐私 | 音频上传至云端 | 数据不出本地,隐私可控 |
| 部署门槛 | 低,注册即可调用 | 较高,需要配置深度学习环境 |
| 语种支持 | 视服务商而定 | 支持 50+ 种语言 |
| 识别速度 | 受网络影响 | GPU 加速后可达实时的 4 至 5 倍 |
1.3 本教程的选型
- MOSS-Transcribe-Diarize 0.9B:OpenMOSS 团队开源的端到端语音转写模型,将语音识别与说话人分离合并为单模型一次推理,直接输出带秒级时间戳与说话人标签的转写文本;支持 50+ 种语言、单次直推最长 90 分钟的长音频,并可通过热词提示提升专名识别准确率,采用 Apache-2.0 许可证。
- vLLM 推理服务:主流的大模型推理框架,原生注册了 MOSS-Transcribe-Diarize 模型,对外直接暴露 OpenAI 兼容的
POST /v1/audio/transcriptions接口,通过显存分配与批处理调度把长音频转写优化到实时的 4 至 5 倍速度。
选择建议:需要离线、隐私可控、长音频与多说话人转写的场景选择本地部署 MOSS-Transcribe-Diarize;快速原型或语音量小的场景可选用在线服务。本教程的示例项目把 vLLM 服务再封装一层,补齐文本清洗、字幕生成与前端交互能力,便于与既有系统集成。
2. MOSS-Transcribe-Diarize 模型详解
2.1 模型简介
MOSS-Transcribe-Diarize 0.9B 是 OpenMOSS 团队于 2026 年 7 月开源的端到端音频理解模型,专注长音频、多说话人场景的转写任务。传统方案需要"语音识别 + 说话人分离"两条流水线拼接,结果对齐困难且误差叠加;该模型把两个任务统一建模,一次推理同时产出文字、时间戳与说话人标签。模型在第二届 MLC-SLM Challenge(INTERSPEECH 2026)覆盖 14 种语言的赛道中获得第一名。
模型总参数量约 0.9B,由文本解码器与音频编码器两部分组成:
80 维梅尔频谱 30 秒窗口] B --> C[Whisper-Medium 音频编码器] C --> D[4 倍时间合并
与 MLP 适配层] D --> E[Qwen3-0.6B 文本解码器
自回归输出] E --> F["段式转写文本
起始时间戳 + 说话人标签 + 正文 + 结束时间戳"] classDef input fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px classDef model fill:#ffecd6,stroke:#ca6f1e,stroke-width:2px classDef output fill:#ebdef0,stroke:#6c3483,stroke-width:2px class A input class B,C,D,E model class F output
| 组件 | 规格 | 作用 |
|---|---|---|
| 音频前端 | WhisperFeatureExtractor,16kHz、80 梅尔频带、30 秒窗口 | 把波形转为梅尔频谱特征 |
| 音频编码器 | Whisper-Medium 编码器配置 | 将频谱编码为音频特征向量 |
| 音频文本桥接 | 4 倍时间合并与 MLP 适配层 | 压缩音频序列长度,对齐到文本词元空间 |
| 文本解码器 | Qwen3-0.6B 同结构因果解码器 | 自回归生成转写文本与标签 |
2.2 核心能力
- 转写与说话人分离一体:单模型一次推理同时输出文字与
[S01]、[S02]等说话人标签,无需外挂分离模型,也不用做多流水线结果对齐 - 秒级时间戳:每段语音自带起止时间戳,是自动字幕的关键能力
- 多语言识别:支持 50+ 种语言,中文效果位居开源模型第一梯队
- 长音频直推:单次推理最长支持 90 分钟录音,无需预先切片
- 热词提示:把专名写入提示词即可引导模型输出,显著减少产品名、人名、术语的错别字
- 逐字转写:可要求模型保留语气词与口误,适合视频剪辑对轴场景
- 声学事件感知:支持输出掌声、音乐等声学事件标注,为下游系统提供更丰富的音频理解
2.3 段式输出格式
模型输出的转写文本是紧凑的段式结构,每一段以起始时间戳和说话人标签开头,正文为语音内容,段末标注结束时间戳,相邻段落共用中间的时间戳:
[0.48][S01]大家好,欢迎来到本期教程[4.10][12.26][S02]今天我们聊一聊语音识别模型的本地部署[15.80][16.02][S01]好的,我们正式开始[18.76]
按正则表达式解析后得到结构化分段:
| 起始(秒) | 结束(秒) | 说话人 | 文本 |
|---|---|---|---|
| 0.48 | 4.10 | S01 | 大家好,欢迎来到本期教程 |
| 12.26 | 15.80 | S02 | 今天我们聊一聊语音识别模型的本地部署 |
| 16.02 | 18.76 | S01 | 好的,我们正式开始 |
解析用的正则表达式如下,.*? 配合 re.S 标志跨行匹配正文:
import re
pattern = re.compile(r"\[(\d+(?:\.\d+)?)\]\[S(\d+)\](.*?)\[(\d+(?:\.\d+)?)\]", re.S)
2.4 vLLM 推理服务
MOSS-Transcribe-Diarize 官方支持 vLLM 部署,vLLM 0.26.0 起原生包含该模型的注册代码,无需额外插件。推理服务通过以下参数控制资源与行为,这些取值来自实测调优后的稳定配置:
| 启动参数 | 取值 | 说明 |
|---|---|---|
--trust-remote-code |
必带 | 允许加载模型仓库中的自定义代码 |
--port |
8001 | 服务监听端口 |
--gpu-memory-utilization |
0.55 | 显存占用比例,实测整机显存峰值约 15.3GB |
--max-model-len |
32768 | 上下文长度上限,覆盖 90 分钟音频的词元数 |
--max-num-batched-tokens |
16384 | 单批词元上限,同时决定音频编码缓存大小 |
--served-model-name |
moss-asr | 对外暴露的模型名称,调用时按此名称传参 |
在 WSL2 环境中运行还需要三个环境变量:
| 环境变量 | 取值 | 说明 |
|---|---|---|
VLLM_WSL2_ENABLE_PIN_MEMORY |
1 | WSL2 默认禁用锁页内存,必须打开,否则启动报 UVA 不可用 |
VLLM_MAX_AUDIO_DECODE_DURATION_S |
5400 | 单文件音频时长上限(秒),放开到模型支持的 90 分钟 |
VLLM_MAX_AUDIO_CLIP_FILESIZE_MB |
200 | 单文件体积上限(MB),默认 25MB 只够约 15 分钟的 wav |
注意:模型目录中
generation_config.json的max_new_tokens建议改为 16384(默认值较小),否则长音频的转写文本会在生成到上限时被截断。
3. 环境准备
本章在 WSL2 的 Ubuntu 中完成模型服务的部署,Windows 侧只需要保证有 ffmpeg 与 curl 命令(用于音频抽取与接口验证,可从 ffmpeg 官网下载后加入 PATH)。
3.1 创建 conda 环境并安装 vLLM
进入 WSL2 的 Ubuntu 终端(Windows 命令行执行 wsl -d Ubuntu 即可进入),创建独立 conda 环境 vllm,依赖统一使用 uv 安装:
# 创建并激活 conda 环境(vLLM 官方推荐 Python 3.12)
conda create -n vllm python=3.12 -y
conda activate vllm
# 安装 uv(Python 包管理工具)
pip install uv
# 安装 vLLM(0.26.0 起原生包含 MOSS-Transcribe-Diarize 模型注册,自动匹配 CUDA 版本的 PyTorch)
uv pip install -U vllm --torch-backend=auto
# 安装音频解码依赖
uv pip install soundfile
3.2 下载模型权重
从本教程资源下载区块的夸克网盘地址下载模型压缩包 MOSS-Transcribe-Diarize.zip(约 1.4 GB),解压后放置到 WSL 的 ~/models/MOSS-Transcribe-Diarize 目录:
- 通过夸克网盘把压缩包下载到本地(Windows 侧下载即可)
- 解压压缩包,把得到的模型文件放入 WSL 的
~/models/MOSS-Transcribe-Diarize目录;若解压出的是同名嵌套文件夹,把文件夹内的全部内容移动到目标目录,保证最终目录中直接包含config.json、generation_config.json、model.safetensors等文件
下载完成后检查生成配置,放开单次生成的词元上限:
# 编辑 ~/models/MOSS-Transcribe-Diarize/generation_config.json
# 将 max_new_tokens 修改为 16384,防止长音频转写被截断
{
"max_new_tokens": 16384
}
如需自定义模型路径,同步修改第 3.3 节启动命令中的模型目录即可。
3.3 启动 vLLM 服务
在 WSL2 的 Ubuntu 终端中,激活环境后先导出三个环境变量,再启动服务:
conda activate vllm
# WSL2 环境必需的三个环境变量
export VLLM_WSL2_ENABLE_PIN_MEMORY=1
export VLLM_MAX_AUDIO_DECODE_DURATION_S=5400
export VLLM_MAX_AUDIO_CLIP_FILESIZE_MB=200
# 启动服务(前台运行,关闭终端服务即停)
vllm serve ~/models/MOSS-Transcribe-Diarize \
--trust-remote-code \
--port 8001 \
--gpu-memory-utilization 0.55 \
--max-model-len 32768 \
--max-num-batched-tokens 16384 \
--served-model-name moss-asr
服务为常驻进程,建议保持该终端窗口不关闭;也可以用 nohup 挂到后台运行。冷启动(模型加载到显存)约需 2 至 3 分钟,看到日志出现 Application startup complete 即启动完成。停止服务可在 WSL 内执行 fuser -k 8001/tcp。
另开一个终端检查服务健康状态:
curl http://localhost:8001/v1/models
返回以下内容说明模型已就绪:
{"object": "list", "data": [{"id": "moss-asr", "object": "model"}]}
3.4 快速验证转写
准备一段音频,用 ffmpeg 统一为 16kHz 单声道 wav 后调用转写接口(在 Windows 侧执行):
# 抽取音轨为 16kHz 单声道 wav
ffmpeg -y -i test.mp3 -vn -ac 1 -ar 16000 test.wav
# 调用 vLLM 转写接口
curl -sS http://localhost:8001/v1/audio/transcriptions \
-F "model=moss-asr" \
-F "response_format=json" \
-F "temperature=0" \
-F "file=@test.wav"
返回的 text 字段即第 2.3 节介绍的段式转写文本:
{"text": "[0.48][S01]大家好,欢迎来到本期教程[4.10][12.26][S02]今天我们聊一聊语音识别模型的本地部署[15.80]"}
提示:请求中的
prompt字段可以追加指令,例如热词提示与逐字转写要求,具体用法见第 4.4 节。
4. 综合示例:voice2text 语音识别服务
4.1 项目目标与架构
示例项目 voice2text 把 MOSS-Transcribe-Diarize 转写服务封装为一个完整的语音识别应用,实现以下目标:
- 用户上传一段音频,或用麦克风录音,服务识别并输出文字
- 支持热词提示与逐字转写开关,改善专名识别与剪辑对轴体验
- 对外暴露 OpenAI 兼容的
POST /v1/audio/transcriptions接口 - 识别结果可转换为带时间轴的字幕,一键下载 SRT / VTT 字幕文件
- 构建前端页面,支持文件上传与浏览器内麦克风录音
推理由 WSL2 内的 vLLM 服务承担,voice2text 作为客户端负责调用、解析与封装,整体架构如下:
static/index.html] -->|上传音频 / 录音 WAV| A[FastAPI 后端
main.py] A --> S[asr_service.py
调用 vLLM 服务并解析段式文本] A --> P[subtitle.py
SRT / VTT 格式化] S -->|ffmpeg 抽取 16kHz 单声道 wav| F[ffmpeg 预处理
生成临时 wav] F -->|上传 wav 转写| V[vLLM 服务 :8001
WSL2 Ubuntu] V --> M[MOSS 模型
MOSS-Transcribe-Diarize 0.9B] S -->|识别文字| U P -->|字幕文件下载| U classDef frontend fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px classDef backend fill:#d5f5e3,stroke:#1e8449,stroke-width:2px classDef engine fill:#ebdef0,stroke:#6c3483,stroke-width:2px classDef model fill:#ffecd6,stroke:#ca6f1e,stroke-width:2px class U frontend class A,P,F backend class S,V engine class M model
4.2 项目结构
voice2text/
├── config.py # 服务配置(端口、vLLM 服务地址、热词、字幕分段时长)
├── main.py # FastAPI 后端,识别与字幕接口
├── asr_service.py # ASR 识别服务(vLLM 客户端与段式文本解析)
├── subtitle.py # 字幕格式化(SRT / VTT 生成)
├── requirements.txt # 依赖管理
├── static/
│ └── index.html # 前端页面(上传 + 录音 + 字幕下载)
└── README.md # 项目说明
4.3 配置文件 config.py
所有配置统一在 config.py 中修改,包括监听端口、vLLM 服务地址、模型名称、热词与字幕参数:
# config.py(核心片段,完整代码请查看 voice2text/config.py)
PORT = 8000
# WSL2 内 vLLM 转写服务地址(承载 MOSS-Transcribe-Diarize 模型)
ASR_SERVICE_URL = "http://localhost:8001"
# vLLM 服务的模型名称(对应启动服务时的 --served-model-name)
ASR_MODEL_NAME = "moss-asr"
# 采样温度,转写任务固定为 0 保证同一音频的结果稳定
ASR_TEMPERATURE = 0
# 常驻热词(逗号分隔),产品名、人名、术语等,可显著减少专名错别字
ASR_HOTWORDS = ""
# 逐字转写:保留语气词(嗯、呃、啊)与口误,剪辑对轴场景建议开启
ASR_VERBATIM = False
# 转写请求超时(秒),长音频转写耗时数分钟,放宽到 1 小时
REQUEST_TIMEOUT_SEC = 3600
# 字幕单条最大时长(秒),值越小每条字幕越短、时间轴越细
SUBTITLE_MAX_LENGTH_S = 5
# 字幕是否去除句读标点(句号、逗号等),仅影响字幕下载,正文聚合识别不受影响
SUBTITLE_STRIP_PUNCTUATION = True
4.4 ASR 识别服务
asr_service.py 封装了对 vLLM 服务的调用逻辑。由于推理由远端服务承担,客户端的核心工作是三件事:组装提示词、预处理音频与解析段式文本。转写请求使用全局锁串行化,因为 vLLM 按整段音频推理,并发请求只会互相排队:
# asr_service.py(核心片段,完整代码请查看 voice2text/asr_service.py)
# 默认提示词:让模型输出带时间戳与说话人编号的段式文本(与官方推荐一致)
DEFAULT_PROMPT = (
"请将音频转写为文本,每一段需以起始时间戳和说话人编号"
"([S01]、[S02]、[S03]…)开头,正文为对应的语音内容,"
"并在段末标注结束时间戳,以清晰标明该段语音范围。"
)
# 逐字转写后缀:保留语气词与口误,适合剪辑对轴场景
VERBATIM_SUFFIX = "请逐字转写,保留语气词(嗯、呃、啊)和口误,不要润色或省略。"
# 整洁文稿后缀:与逐字转写二选一,实测追加指令后缀可稳定模型的段式输出格式
CONCISE_SUFFIX = "请省略语气词与口误,输出整洁文稿。"
# 段式文本解析:[起始秒][S01]正文[结束秒],相邻段落共用中间的时间戳
_SEGMENT_PATTERN = re.compile(
r"\[(\d+(?:\.\d+)?)\]\[S(\d+)\](.*?)\[(\d+(?:\.\d+)?)\]", re.S
)
def build_prompt(hotwords, verbatim):
"""组装提示词:热词前置 + 基础指令 + 逐字/整洁后缀(二选一)"""
prompt = ""
if hotwords:
prompt += f"热词提示:{hotwords}。"
prompt += DEFAULT_PROMPT
prompt += VERBATIM_SUFFIX if verbatim else CONCISE_SUFFIX
return prompt
def _extract_wav(audio_path):
"""用 ffmpeg 把任意格式音频统一为 16kHz 单声道 wav(模型要求的输入格式)"""
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
tmp.close()
subprocess.run(
["ffmpeg", "-y", "-v", "error", "-i", audio_path,
"-vn", "-ac", "1", "-ar", "16000", tmp.name],
check=True,
)
return tmp.name
def _call_service(wav_path, prompt):
"""上传 wav 到 vLLM 服务,返回模型输出的段式文本"""
with open(wav_path, "rb") as f:
resp = requests.post(
f"{ASR_SERVICE_URL}/v1/audio/transcriptions",
files={"file": ("audio.wav", f, "audio/wav")},
data={
"model": ASR_MODEL_NAME,
"response_format": "json",
"temperature": ASR_TEMPERATURE,
"prompt": prompt,
},
timeout=REQUEST_TIMEOUT_SEC,
)
resp.raise_for_status()
return resp.json().get("text", "")
def parse_segments(text):
"""解析 [起始][S01]正文[结束] 段式文本,返回带说话人的分段列表"""
segments = []
for m in _SEGMENT_PATTERN.finditer(text):
segments.append({
"start": int(float(m.group(1)) * 1000),
"end": int(float(m.group(4)) * 1000),
"speaker": f"S{m.group(2)}",
"text": m.group(3).strip(),
})
return segments
def transcribe(audio_path, hotwords=None, verbatim=None):
"""整段转写:返回拼接后的纯文本(保留标点,去除时间戳与说话人标签)"""
hot = ASR_HOTWORDS if hotwords is None else hotwords
verb = ASR_VERBATIM if verbatim is None else verbatim
segments = _transcribe(audio_path, hot, verb)
return "".join(seg["text"] for seg in segments)
def transcribe_segments(audio_path, hotwords=None, verbatim=None):
"""字幕转写:返回 [{"start": 毫秒, "end": 毫秒, "text": 文本}]"""
hot = ASR_HOTWORDS if hotwords is None else hotwords
verb = ASR_VERBATIM if verbatim is None else verbatim
segments = _transcribe(audio_path, hot, verb)
# 超过字幕时长上限的段落二次细分,切口不落入英文数字单元内部
segments = split_segments(segments, SUBTITLE_MAX_LENGTH_S * 1000)
# 字幕场景默认去除句读标点,与正文聚合识别结果区分
if SUBTITLE_STRIP_PUNCTUATION:
segments = strip_punctuation(segments)
return segments
这套设计有三个要点。其一,MOSS-Transcribe-Diarize 的能力由提示词驱动:默认提示词让模型输出段式转写文本,逐字转写与整洁文稿两个后缀二选一追加(裸的默认提示词实测会让模型偶尔退化为不带时间戳的输出,追加指令后缀后格式稳定),热词以 热词提示:词1,词2。 的形式前置到提示词开头——实测热词追加在末尾时短音频容易丢时间戳,前置则时间戳与专名写法都能稳定输出。其二,模型要求 16kHz 单声道输入,客户端统一用 ffmpeg 预处理,因此 mp3、m4a、flac 等常见格式都能直接上传。其三,解析正则利用"相邻段落共用中间时间戳"的特性,一次匹配即可同时拿到上一段的结束时间与下一段的起始时间。
parse_segments() 返回的分段携带 speaker 说话人标签,本项目生成字幕时未使用它;如果需要按说话人区分字幕(如 [S01] 前缀或分轨字幕),在 transcribe_segments() 中把标签拼入文本即可。
4.5 后端接口
main.py 是服务的核心入口,提供三个业务接口:自定义的 /api/transcribe、OpenAI 兼容的 /v1/audio/transcriptions,以及生成字幕文件的 /api/subtitle:
# main.py(核心片段,完整代码请查看 voice2text/main.py)
@app.post("/api/transcribe")
def transcribe(
file: UploadFile = File(...),
hotwords: str = Form(""),
verbatim: bool = Form(False),
):
"""自定义识别接口:上传音频文件,返回识别文字"""
audio_path = _save_audio(file) # 保存为临时文件
try:
text = asr_service.transcribe(audio_path, hotwords=hotwords or None,
verbatim=verbatim)
finally:
os.unlink(audio_path) # 识别后清理临时文件
return {"text": text}
@app.post("/v1/audio/transcriptions")
def transcriptions(
file: UploadFile = File(...),
model: str = Form("moss-asr"),
):
"""OpenAI 兼容的语音识别接口(model 参数仅做兼容)"""
audio_path = _save_audio(file)
try:
text = asr_service.transcribe(audio_path)
finally:
os.unlink(audio_path)
return {"text": text} # OpenAI 兼容返回格式
@app.post("/api/subtitle")
def generate_subtitle(
file: UploadFile = File(...),
hotwords: str = Form(""),
verbatim: bool = Form(False),
fmt: str = Form("srt"), # srt / vtt
):
"""字幕识别接口:返回可下载的字幕文件"""
audio_path = _save_audio(file)
try:
segments = asr_service.transcribe_segments(audio_path, hotwords=hotwords or None,
verbatim=verbatim)
finally:
os.unlink(audio_path)
content = subtitle.to_vtt(segments) if fmt == "vtt" else subtitle.to_srt(segments)
return Response(
content=content,
media_type="text/vtt; charset=utf-8" if fmt == "vtt"
else "application/x-subrip; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"},
)
代码要点说明:
| 功能模块 | 实现方式 | 说明 |
|---|---|---|
| 音频接收 | UploadFile(multipart) |
前端通过表单上传音频文件 |
| 临时文件 | tempfile.NamedTemporaryFile |
识别结束后清理,不落盘 |
| 自定义接口 | POST /api/transcribe |
前端使用的简单接口,支持热词与逐字转写参数 |
| 兼容接口 | POST /v1/audio/transcriptions |
与 OpenAI 规范一致,返回 {"text": ...} |
| 字幕接口 | POST /api/subtitle |
返回 SRT / VTT 文本流,Content-Disposition 标记为附件下载 |
三个接口都声明为普通 def 而非 async def:转写链路中的 ffmpeg 抽取与 vLLM 调用都是阻塞操作,FastAPI 会自动把同步接口放入线程池执行,避免长时间占用事件循环。
4.6 前端页面
前端为单页 HTML 应用,位于 static/index.html。你可以前往 voice2text/static/index.html 查看完整的示例代码。页面主要功能:
- 上传音频:选择本地音频文件(wav/mp3/m4a/flac 等)作为识别输入
- 麦克风录音:调用浏览器麦克风录音,通过 Web Audio API 实时采集并编码为 16kHz WAV
- 热词提示:输入逗号分隔的热词(如产品名、人名、术语),随请求发送给后端组装进提示词
- 逐字转写开关:勾选后保留语气词与口误,适合视频剪辑对轴;默认关闭,输出更干净的文稿
- 音频预览:识别前可播放已选音频或录音
- 识别展示:点击"开始识别",通过
fetch以 multipart 表单上传音频,展示返回的文字 - 字幕下载:识别完成后出现"下载 SRT 字幕"与"下载 VTT 字幕"按钮,点击后请求
/api/subtitle并将返回的字幕文件保存到本地
麦克风录音采用 Web Audio API 采集原始 PCM 并编码为 WAV,不依赖 WebM/Opus,因此无需额外解码即可在服务端处理。
4.7 字幕生成与下载
字幕的本质是"带时间轴的纯文本":每条字幕由起始时间、结束时间与文本三部分组成。示例项目的 subtitle.py 承担三件事:先用 split_segments() 把超过时长上限的分段按标点二次切分,再用 strip_punctuation() 去除句读标点(受 SUBTITLE_STRIP_PUNCTUATION 控制),最后把时间戳分段格式化为标准字幕文件:
# subtitle.py(核心片段,完整代码请查看 voice2text/subtitle.py)
def _format_timestamp(ms, decimal_sep):
"""毫秒转 HH:MM:SS,mmm(SRT 用逗号)或 HH:MM:SS.mmm(VTT 用点)"""
hours, rem = divmod(max(0, int(ms)), 3600_000)
minutes, rem = divmod(rem, 60_000)
seconds, millis = divmod(rem, 1000)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}{decimal_sep}{millis:03d}"
def to_srt(segments):
"""生成 SRT 格式字幕内容"""
cues = []
for i, seg in enumerate(segments, start=1):
start = _format_timestamp(seg["start"], ",")
end = _format_timestamp(seg["end"], ",")
cues.append(f"{i}\n{start} --> {end}\n{seg['text']}")
return "\n\n".join(cues) + "\n"
SRT 与 VTT 是两种最常用的字幕格式,主流播放器与视频平台均支持:
| 对比项 | SRT | VTT |
|---|---|---|
| 全称 | SubRip Text | WebVTT(Web Video Text Tracks) |
| 时间戳秒的分隔符 | 英文逗号 , |
英文句点 . |
| 文件头 | 无 | 必须以 WEBVTT 开头 |
| 典型用途 | 本地播放器、剪映、抖音等 | 网页 <video> 标签、HTML5 播放器 |
生成的字幕文件内容形如下方所示,可直接导入播放器或剪辑软件:
1
00:00:00,480 --> 00:00:04,100
大家好欢迎来到本期教程
2
00:00:12,260 --> 00:00:15,800
今天我们聊一聊语音识别模型的本地部署
也可以不经过前端页面,直接用 curl 下载字幕文件:
# fmt 可选 srt / vtt,-o 指定保存路径
curl.exe -X POST http://localhost:8000/api/subtitle -F "file=@C:/path/to/audio.mp3" -F "fmt=srt" -o audio.srt
4.8 运行与测试
第一步:确认 vLLM 服务可用
若已按第 3 章完成部署,确认 vLLM 服务仍在运行(curl http://localhost:8001/v1/models 有响应);否则先按第 3.3 节启动。
第二步:创建环境并安装依赖
conda create -n voice2text python=3.10 -y
conda activate voice2text
pip install uv
cd voice2text
# 安装服务依赖(无需 PyTorch,安装很轻量)
uv pip install -r requirements.txt --index-url https://pypi.tuna.tsinghua.edu.cn/simple
第三步:启动服务
python main.py
启动后访问 http://localhost:8000,即可看到语音识别界面。
第四步:验证功能
- 在页面中上传一段音频或点击录音,点击"开始识别",确认能正确输出文字
- 识别完成后点击"下载 SRT 字幕",用记事本打开下载的
.srt文件,确认包含时间轴与文本 - 也可使用 curl 直接验证接口:
curl.exe -X POST http://localhost:8000/api/transcribe -F "file=@C:/path/to/audio.wav"
# 带热词转写(修正专名错别字)
curl.exe -X POST http://localhost:8000/api/transcribe -F "file=@C:/path/to/audio.wav" -F "hotwords=FastAPI,vLLM"
# 验证字幕接口
curl.exe -X POST http://localhost:8000/api/subtitle -F "file=@C:/path/to/audio.mp3" -F "fmt=srt" -o audio.srt
第五步(可选):验证热词与逐字转写
- 在热词输入框填入一段口播中出现的专有名词,对比开启前后的识别结果,确认专名拼写被修正
- 勾选"逐字转写"重新识别,确认结果保留了语气词与口误
识别完成后的页面效果如下,热词 MOSS 被正确写入识别结果,并出现 SRT / VTT 字幕下载按钮:

4.9 使用 OpenAI SDK 验证接口兼容性
由于服务暴露了 OpenAI 兼容接口,可以直接使用 openai 官方 Python SDK 调用:
# 使用 openai SDK 调用本地 voice2text 服务
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:8000/v1", # 本地服务地址
api_key="not-needed", # 本地服务不校验 Key
)
with open("audio.wav", "rb") as f:
result = client.audio.transcriptions.create(
model="moss-asr",
file=f,
)
print(result.text)
把 base_url 换成 http://127.0.0.1:8001/v1 也可以直接调用 vLLM 服务本身,但拿到的是未经解析的段式原文(含时间戳与说话人标签);voice2text 服务则返回清洗后的纯文本,并额外提供字幕生成能力。
5. 总结
5.1 核心内容回顾
- ASR 技术:语音转文字,核心流程为音频预处理、声学特征提取、声学模型与解码
- MOSS-Transcribe-Diarize 0.9B:端到端转写模型,一次推理同时输出文字、秒级时间戳与说话人标签,支持 50+ 语言与 90 分钟长音频,热词提示可引导专名输出
- vLLM 部署:0.26.0 起原生支持该模型,WSL2 环境需要打开锁页内存并放开音频时长与体积上限,实测显存峰值约 15.3GB、转写速度达实时的 4 至 5 倍
- 服务封装:FastAPI 作为 vLLM 客户端,负责提示词组装、ffmpeg 音频预处理与段式文本解析,实现
/api/transcribe与 OpenAI 兼容的/v1/audio/transcriptions接口 - 字幕生成:解析出的时间戳分段经超长段二次细分与去标点后,格式化为 SRT / VTT 字幕文件供用户下载
- 前端应用:支持上传音频与麦克风录音,提供热词提示与逐字转写开关
5.2 常见问题与解答
问:服务启动后第一次转写为什么要等两三分钟?
答:vLLM 冷启动需要把模型权重加载到显存并完成预分配,约需 2 至 3 分钟(日志出现 Application startup complete 即就绪)。就绪后模型常驻显存,后续转写按音频时长计费,实测为实时的 4 至 5 倍速度。
问:启动时报显存不足或 KV cache 超预算怎么办?
答:确认 --max-model-len 为 32768(调回模型默认的 131072 会预分配过大的 KV cache 导致启动失败)。显存仍不足时,把 --gpu-memory-utilization 与 --max-num-batched-tokens 调低,代价是可容纳的单文件音频变短。
问:报错 UVA is not available?
答:WSL2 默认禁用锁页内存。启动前必须导出 VLLM_WSL2_ENABLE_PIN_MEMORY=1(见第 3.3 节)。
问:报错 Audio exceeds maximum allowed duration 或 Maximum file size exceeded?
答:分别是音频时长与体积超出服务端上限。导出 VLLM_MAX_AUDIO_DECODE_DURATION_S=5400 与 VLLM_MAX_AUDIO_CLIP_FILESIZE_MB=200 即可放开到 90 分钟(90 分钟 16kHz 单声道 wav 约 173MB)。
问:报错 exceeds the pre-allocated encoder cache size?
答:音频词元数超出音频编码缓存。有效参数是 --max-num-batched-tokens(已内置 16384,覆盖约 1.5 小时音频),报错信息提示的 --limit-mm-per-prompt 对此无效。
问:长音频转写到一半文本被截断?
答:把模型目录 generation_config.json 的 max_new_tokens 改为 16384(见第 3.2 节)。若重新下载模型,需要重新修改。
问:产品名、人名、术语总是识别错怎么办?
答:使用热词提示。在 config.py 的 ASR_HOTWORDS 配置常驻热词,或在请求时通过 hotwords 参数传入(前端热词输入框、curl 的 -F "hotwords=..." 均可),热词会以 热词提示: 的形式前置进提示词引导模型输出。
问:识别结果里语气词(嗯、呃、啊)太多或太少?
答:由逐字转写开关控制。勾选逐字转写(或请求传 verbatim=true)保留语气词与口误,适合剪辑对轴;默认关闭,输出干净文稿。两种模式都保留完整标点。
问:麦克风录音后识别为空或失败?
答:确认浏览器允许麦克风权限;录音时长过短可能识别为空,建议录制 1 秒以上。也可改用上传音频方式测试。
问:上传 mp3/m4a 无法识别?
答:服务端统一用 ffmpeg 解码为 16kHz 单声道 wav,需要 Windows PATH 中有 ffmpeg 命令。若仍失败,可先手动转码为 wav 再上传。
问:转写耗时异常长怎么办?
答:确认服务只有一个实例在运行——多个旧实例占用端口时请求会被排队,可在 WSL 内执行 fuser -k 8001/tcp 后重启;同时确认带上了热词提示,无热词时英文专名的识别质量会明显下降。
举手提问