Initial commit: FunASR Speech Recognition Toolkit
Update API Documentation / build-api-docs (push) Has been cancelled

Add complete FunASR codebase including models, runtime, and documentation.
This commit is contained in:
freedakgmail
2026-07-09 22:38:58 +08:00
commit 6116b1f3c6
3683 changed files with 990984 additions and 0 deletions
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
# MIT License (https://opensource.org/licenses/MIT)
# To install requirements: pip install -U "qwen-asr==0.0.6" "transformers==4.57.6" accelerate
from funasr import AutoModel
# Initialize Qwen3-ASR model
# hub: "ms" for ModelScope (default), "hf" for HuggingFace
model = AutoModel(
model="Qwen/Qwen3-ASR-1.7B",
hub="hf",
device="cuda:0",
dtype="bf16",
)
# Chinese speech recognition
res = model.generate(
input="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_zh.wav",
language="Chinese",
)
print("Chinese:", res[0]["text"])
# English speech recognition
res = model.generate(
input="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_en.wav",
language="English",
)
print("English:", res[0]["text"])
# Auto language detection (supports 52 languages)
res = model.generate(
input="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_zh.wav",
)
print("Auto:", res[0]["text"], "| Language:", res[0].get("language", ""))
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
# Copyright FunASR (https://github.com/alibaba-damo-academy/FunASR). All Rights Reserved.
# MIT License (https://opensource.org/licenses/MIT)
# To install requirements: pip install -U "qwen-asr==0.0.6" "transformers==4.57.6" accelerate
from funasr import AutoModel
# Download from ModelScope (recommended for Chinese users)
model = AutoModel(
model="Qwen/Qwen3-ASR-1.7B",
hub="ms",
device="cuda:0",
dtype="bf16",
)
res = model.generate(
input="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_zh.wav",
language="Chinese",
)
print(res[0]["text"])
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
# coding=utf-8
"""
Qwen3-ASR streaming WebSocket service.
把官方 example_qwen3_asr_vllm_streaming.py 的原生流式 API
(init_streaming_state / streaming_transcribe / finish_streaming_transcribe)
包成一个 WebSocket 服务,协议与 Fun-ASR-Nano 的 serve_realtime_ws.py 一致,
因此可以直接用同一个 bench_streaming_ws.py 压测,对比。
协议:
1. 客户端连接 ws://host:port
2. 客户端发文本 "START" → 服务端回 {"event": "started"}
3. 客户端发二进制 int16 PCM 块(16kHz 单声道)
4. 服务端随转写增长发 {"partial": "<当前文本>"}
5. 客户端发文本 "STOP" → 服务端回
{"is_final": true, "sentences": [{"text": "<最终文本>"}]}
然后 {"event": "stopped"}
架构对齐 serve_realtime_ws.py:单 asyncio 事件循环,streaming_transcribe /
finish_streaming_transcribe 同步调用、阻塞整个循环——这样压测出来的并发特性
才和 Fun-ASR-Nano 那条同口径可比。生产扩展同样靠 多进程 + CUDA MPS + nginx
(见 vllm_guide §6.7)。
关于 VAD 参见配套说明文档
依赖:
pip install -U "qwen-asr[vllm]==0.0.6" "transformers==4.57.6" websockets numpy
启动:
python serve_qwen3_asr_ws.py --port 10095 --gpu-memory-utilization 0.8
# 可选:--chunk-size-sec 控制流式块大小(默认 2.0)。值越小出字越快/越勤,
# 但并发开销越大(实测 1.0 比 2.0 明显更吃并发)。
"""
import asyncio
import argparse
import json
import logging
import numpy as np
import websockets
from qwen_asr import Qwen3ASRModel
# websockets 默认会对每个连接打 INFO 级 "connection open/closed",压测时刷屏;
# 提到 WARNING 关掉这条噪音(不影响连接行为,纯日志)。
logging.getLogger("websockets").setLevel(logging.WARNING)
SAMPLE_RATE = 16000
# 全局只加载一次;所有连接共用模型,各自持有独立的 streaming state。
asr = None
# 流式块大小(秒),由 --chunk-size-sec 设置,handle_client 里 init_streaming_state 用。
# 默认 2.0(官方 example 值);值越小出字越勤、并发开销越大。
CHUNK_SIZE_SEC = 2.0
def int16_pcm_to_float32(pcm_bytes: bytes) -> np.ndarray:
"""bench 发来的是 int16 小端 PCMQwen3-ASR 的 streaming_transcribe 吃 float32 [-1,1)。"""
return np.frombuffer(pcm_bytes, dtype=np.int16).astype(np.float32) / 32768.0
async def handle_client(ws, path=None): # path 兼容老版本 websockets 的两参回调
state = None
last_partial = None
try:
async for msg in ws:
# ---- 文本控制消息 ----
if isinstance(msg, str):
if msg == "START":
# 每个连接一份独立 state;参数同官方 example
state = asr.init_streaming_state(
unfixed_chunk_num=2,
unfixed_token_num=5,
chunk_size_sec=CHUNK_SIZE_SEC,
)
last_partial = None
await ws.send(json.dumps({"event": "started"}))
elif msg == "STOP":
if state is not None:
# 同步收尾,阻塞循环(与 serve_realtime_ws.py 句尾 finalize 同口径)
asr.finish_streaming_transcribe(state)
final_text = (state.text or "").strip()
await ws.send(json.dumps({
"is_final": True,
"sentences": [{"text": final_text}] if final_text else [],
}))
await ws.send(json.dumps({"event": "stopped"}))
break
# 其它文本忽略
# ---- 二进制音频块 ----
else:
if state is None:
continue # 还没 START,丢弃
seg = int16_pcm_to_float32(msg)
# 同步调用,阻塞整个事件循环 —— 这正是要复刻的单循环架构
asr.streaming_transcribe(seg, state)
text = state.text or ""
# 只在文本变化时发 partial,避免刷屏(不影响 bench 的首词延迟统计)
if text != last_partial:
last_partial = text
await ws.send(json.dumps({"partial": text}))
except websockets.exceptions.ConnectionClosed:
pass
except Exception:
logging.exception("Unexpected error in Qwen3-ASR WebSocket handler")
async def amain(args):
global asr, CHUNK_SIZE_SEC
CHUNK_SIZE_SEC = args.chunk_size_sec
print(f"Loading {args.model} (gpu_memory_utilization={args.gpu_memory_utilization}, chunk_size_sec={CHUNK_SIZE_SEC}) ...")
# Streaming is vLLM-only and no forced aligner supported.(官方 example 注释)
asr = Qwen3ASRModel.LLM(
model=args.model,
gpu_memory_utilization=args.gpu_memory_utilization,
max_new_tokens=32, # 流式用小值,同官方 example
)
print(f"Serving on ws://{args.host}:{args.port} (Ctrl-C to stop)")
async with websockets.serve(
handle_client, args.host, args.port, max_size=10 * 1024 * 1024
):
await asyncio.Future() # run forever
def main():
p = argparse.ArgumentParser()
p.add_argument("--host", default="0.0.0.0")
p.add_argument("--port", type=int, default=10095)
p.add_argument("--model", default="Qwen/Qwen3-ASR-1.7B")
p.add_argument("--gpu-memory-utilization", type=float, default=0.8,
dest="gpu_memory_utilization")
p.add_argument("--chunk-size-sec", type=float, default=2.0,
dest="chunk_size_sec",
help="流式块大小(秒),传给 init_streaming_state。默认 2.0;越小出字越勤但并发开销越大(实测 1.0 比 2.0 明显更吃并发)")
args = p.parse_args()
asyncio.run(amain(args))
if __name__ == "__main__":
main()
@@ -0,0 +1,171 @@
# serve_qwen3_asr_ws.py 说明与已知问题
本文档记录 `serve_qwen3_asr_ws.py` 的几个容易困惑/踩坑的点。代码里对应位置只留一行
指向本文的精简注释,细节看这里。
---
## 1. 关于 VAD
> 在开源仓库 github.com/QwenLM/Qwen3-ASR 没有关于 `VAD` 内容。但商用 [Qwen3-ASR的文档/示例](https://help.aliyun.com/zh/model-studio/qwen-asr-realtime-interaction-process)里**是有 VAD 设置**,例如:
> ```json
> "turn_detection": { "type": "server_vad", "threshold": 0.2, "silence_duration_ms": 800 }
> ```
VAD 在 ASR 里其实有两种完全不同的用途,容易混为一谈:
- **A. 切段用的 VAD(给非流式 encoder 喂分段)**:像 Fun-ASR-Nano 这类模型 encoder 是
**非流式**的(一次要看完整一段),必须靠 VAD 把连续音频切成一句句、每句整体编码解码。
这种 VAD 对 Fun-ASR-Nano 这类模型 **技术必需**——不切就没法编码。
- **B. 端点/轮次检测用的 VAD(判断"这一轮说完了没")**:检测说话人停顿(如静音 800ms)
来判定"一句话/一轮结束",从而触发"锁定文本 / 发 is_final / 该回应了"。这是**产品行为**
层面的需求,和 encoder 能不能流式无关。
**Qwen3-ASR 的开源流式 API**(本服务用的 `qwen-asr[vllm]`
`init_streaming_state` / `streaming_transcribe`):
- **不需要 A 类(切段)VAD**:它是**增量式**流式——每次只吃新增的一小段音频、状态向前
滚动,连续转写,不存在"先切句再解码"。"哪些字定了、哪些会变"由 `unfixed_chunk_num` /
`unfixed_token_num` 表示(尾部 N 个 chunk/token 算"未定"、会被后续音频修正,其余视为
已确认)——这相当于内置的 partial/锁定机制,取代了 A 类 VAD 的切段职责。所以在开源
流式 API 的代码里搜不到 vad,是因为它**这一层不做切段**。
- **仍然需要 B 类(端点)VAD —— 只是开源 API 自己不带**:自动判断"用户停顿=这一轮结束"
这件事,`streaming_transcribe` 本身不管。商用服务在 ASR **之外**包了一层 `server_vad`
来做(就是上面那段 `turn_detection`)。本服务目前是用客户端显式发 **`STOP`** 来代替
这个端点判断(bench 里音频放完即发 STOP)。**若要在真实场景自动断句/断轮,需要自己在
本服务之外接一个 VAD / 端点检测**(角色等同商用的 server_vad),而不是去 Qwen3-ASR
内部找——它的开源流式 API 不含这一层。
**一句话**Qwen3-ASR 增量流式**省掉了"切段 VAD"A**,但**"端点/轮次 VAD"B)这个
职责依然存在**,商用版用 `server_vad` 实现、本服务用手动 `STOP` 代替。两者不矛盾。
### 1.1 官方佐证:商用 Qwen-ASR-Realtime 的"VAD 模式 / Manual 模式"
阿里云百炼的实时语音识别(Qwen-ASR-Realtime)文档明确把"断句由谁做"分成两种模式,
本质就是 `session.turn_detection` 开还是关:
- **VAD 模式(默认,`turn_detection` 配置为 server_vad)**:服务端自动检测语音起点/终点
来断句,客户端只管持续发音频流,服务端在"检测到一句话结束"时自动返回最终结果。流程中
服务端会发 `input_audio_buffer.speech_started` / `speech_stopped` 等事件——这就是上面说的
**B 类端点 VAD**,由服务端那一层(server_vad)实现,**不是** ASR 内核在切段。
- **Manual 模式(`turn_detection` 设为 null)**:由**客户端**控制断句——发完一整句音频后,
客户端发 `input_audio_buffer.commit` 通知服务端边界。适用于客户端能明确判断语句边界的
场景(如"按住说话"、聊天发语音)。
> 对应关系:本服务 `serve_qwen3_asr_ws.py` 用客户端显式发 **`STOP`** 来标记一轮结束,
> 等价于商用的 **Manual 模式**`turn_detection=null`,由客户端控制边界)。若要做成"服务端
> 自动断句",就是去实现商用 **VAD 模式** 的那一层端点检测(server_vad),加在本服务的
> 增量转写之外,而不是在 Qwen3-ASR 转写内核里找。
>
> 文档:实时语音识别(Qwen-ASR-Realtime)交互流程
> help.aliyun.com/zh/model-studio/qwen-asr-realtime-interaction-process: ”服务端自动检测语音的起点和终点(断句)。开发者只需持续发送音频流,服务端会在检测到一句话结束时自动返回最终识别结果。此模式适用于实时对话、会议记录等场景。“
### 1.2 `chunk-size-sec` 控制流式块大小
`chunk-size-sec` 控制流式块大小(默认 2.0)。值越小出字越快/越勤,但并发开销越大(实测 1.0 比 2.0 在L20 上,29秒音频 48路并发,1.0 全部失败,2.0 全部通过)。
---
## 2 必须用 vllm 0.14,不要用 0.19rope_scaling / thinker_config 警告)
本服务需要vllm加速, `qwen-asr[vllm]`[它锁定 `vllm==0.14.0`](https://github.com/QwenLM/Qwen3-ASR/blob/main/pyproject.toml)。若换成更新的vllm版本, 比如 0.19.x,启动会**出现**
```
Unrecognized keys in `rope_scaling` for 'rope_type'='default':
{'mrope_section', 'mrope_interleaved', 'interleaved'}
thinker_config is None. Initializing thinker model with default values
```
**根因**vllm 在 0.14 → 0.19 之间 transformers 两版都是 4.57.6),config 解析里的
`patch_rope_scaling_dict` 会把 `rope_type``'mrope'` 改写成 `'default'`(它把 mrope
当 legacy、假设由 vllm 内部消化 `mrope_section` 等字段):
```python
elif rope_scaling["rope_type"] == "mrope":
assert "mrope_section" in rope_scaling
rope_scaling["rope_type"] = "default" # ← 改写
```
但 Qwen3-ASR 自带的 `Qwen3ASRThinkerTextRotaryEmbedding` 期望从 `rope_scaling` 里读到
`"mrope"` 才走多模态 RoPE 分支:
```python
self.rope_type = config.rope_scaling.get("rope_type", "default")
```
被 vllm 改写成 `"default"` 后,它走了普通 RoPE`mrope_section` / `mrope_interleaved` /
`interleaved` 这几个键没人认领 → 打印 "Unrecognized keys" 警告,且音频/文本的多模态
位置编码退化。`thinker_config is None` 那条同源:0.19 的加载路径没正确解析 Qwen3-ASR 的
thinker 子配置,回退到默认参数。
**影响与抉择**:在 0.19 上服务"能起、也能出字"(两条是 WARNING/INFO 不是 ERROR),抽查
几条转写也"看着正常";但位置编码退化对长音频/复杂内容可能有害,且**未做 CER 量化对比**,
无法判定等价。保守起见固定用 `qwen-asr[vllm]` 自带的 `vllm==0.14.0`
### 2.1 vllm 加速需要用 Qwen3ASRModel
funasr 的`AutoModelVLLM` 不能加速 Qwen3-ASR , 必须要用 `from qwen_asr import Qwen3ASRModel`
> 这解答 `#3026` 的问题
### 2.2 关于模型下载
**官方推荐做法([Qwen3-ASR README](https://github.com/QwenLM/Qwen3-ASR#released-models-description-and-download)**:如果运行环境不能在线下载,或无法访问 Hugging Face,先手动把权重下载到本地目录,再把本地路径传给 `--model`
```bash
# ModelScope(国内推荐)
pip install -U modelscope
modelscope download --model Qwen/Qwen3-ASR-1.7B --local_dir ./Qwen3-ASR-1.7B
# 启动时指定本地模型目录
python serve_qwen3_asr_ws.py --model ./Qwen3-ASR-1.7B ...
```
注意:仅设置 `VLLM_USE_MODELSCOPE=True` 并安装 `modelscope` 只能接管一部分下载流程。vLLM 会从 ModelScope 拉取 config、tokenizer、merges、vocab、`model.safetensors.index.json` 等文件,但真正的权重 `model.safetensors` 仍可能回到 `huggingface.co` 拉取:
这看起来是 Qwen-ASR 当前下载链路的一个问题:
```
Downloading Model from https://www.modelscope.cn to directory: /home/vllm/.cache/modelscope/hub/models/Qwen/Qwen3-ASR-1.7B
2026-06-28 10:15:56,301 - modelscope - INFO - Got 10 files, start to download ...
...
2026-06-28 10:15:57,023 - modelscope - INFO - Finish downloading 10 files for repo 'Qwen/Qwen3-ASR-1.7B'███████████| 12.2k/12.2k [00:00<00:00, 38.6kB/s]
...
INFO 06-28 10:15:59 [model.py:530] Resolved architecture: Qwen3ASRForConditionalGeneration
'(MaxRetryError('HTTPSConnectionPool(host=\'huggingface.co\', port=443): Max retries exceeded with url: /Qwen/Qwen3-ASR-1.7B/resolve/main/model.safetensors (Caused by NewConnectionError("HTTPSConnection(host=\'huggingface.co\', port=443): Failed to establish a new connection: [Errno 101] Network is unreachable"))'), '(Request ID: f4f58ad5-e161-42ec-baad-cb2b8dbb63b9)')' thrown while requesting HEAD https://huggingface.co/Qwen/Qwen3-ASR-1.7B/resolve/main/model.safetensors
```
---
## 3. tokenizer 的 `fix_mistral_regex` 警告(无害,可忽略)
启动时可能出现:
```
The tokenizer you are loading from '.../Qwen3-ASR-1.7B' with an incorrect regex
pattern ... This will lead to incorrect tokenization. You should set the
`fix_mistral_regex=True` flag when loading this tokenizer to fix this issue.
```
**原因**Qwen3-ASR 的 tokenizer 沿用了一类带已知 regex 问题的分词器实现,底层库检测到
该 regex 模式后给出提醒,建议加 `fix_mistral_regex=True` 修正切分。本服务通过 qwen-asr
的高层 API 加载模型、并不直接构造 tokenizer,没有暴露这个开关,所以这条提醒按原样打印。
**影响**:实测对中文 ASR 转写结果无可见影响(**抽查**多条转写正常,未做 CER 量化)。该
regex 修正主要影响某些特殊 token 的边界切分,对语音转写路径未观察到差异。属于"提醒级"
噪音,可忽略。若要彻底消除,需在更底层自行加载 tokenizer 时传 `fix_mistral_regex=True`
但 qwen-asr 高层 API 当前不直接支持,且无实测必要。
---
### 顺带:另外两条启动日志(均无害)
- `Error retrieving safetensors: Repo id must be in the form ...`:把本地模型路径当成 HF
仓库 id 去查线上元数据,失败后重试 2 次、回退本地加载,不影响功能。可设环境变量
`HF_HUB_OFFLINE=1` 消除。
- `Downcasting torch.float32 to torch.bfloat16`:权重以 fp32 存、按 bf16 加载,正常省显存 /
提速,bf16 与 fp32 指数位同宽,精度几乎无损。这是 INFO 不是错误。
@@ -0,0 +1,205 @@
# serve_qwen3_asr_ws.py — Notes & Known Issues
This document records the confusing points / gotchas of `serve_qwen3_asr_ws.py`. The code
only keeps one-line comments pointing here; details live in this file.
---
## 1. About VAD
> The open-source repo github.com/QwenLM/Qwen3-ASR contains nothing about `VAD`. However,
> the commercial [Qwen3-ASR docs/examples](https://help.aliyun.com/zh/model-studio/qwen-asr-realtime-interaction-process)
> **do have VAD settings**, e.g.:
> ```json
> "turn_detection": { "type": "server_vad", "threshold": 0.2, "silence_duration_ms": 800 }
> ```
VAD actually serves two completely different purposes in ASR, which are easy to conflate:
- **A. Segmentation VAD (feeding chunks to a non-streaming encoder)**: models like
Fun-ASR-Nano have a **non-streaming** encoder (it needs to see a whole segment at once),
so a VAD is required to cut the continuous audio into sentences, each encoded/decoded as a
whole. This kind of VAD is **technically mandatory** for such models — without cutting,
there is no way to encode.
- **B. Endpoint / turn-detection VAD (deciding "has this turn finished?")**: detects speaker
pauses (e.g. 800ms of silence) to decide "a sentence/turn has ended", triggering "lock the
text / emit is_final / time to respond". This is a **product-behavior** need, unrelated to
whether the encoder is streaming.
For **Qwen3-ASR's open-source streaming API** (this service uses `qwen-asr[vllm]`'s
`init_streaming_state` / `streaming_transcribe`):
- **No need for type-A (segmentation) VAD**: it is **incremental** streaming — each call only
consumes the newly added slice of audio, the state rolls forward, transcribing continuously;
there is no "cut first, then decode" step. "Which characters are final vs. still changing" is
expressed by `unfixed_chunk_num` / `unfixed_token_num` (the last N chunks/tokens are
"unfixed" and may be corrected by later audio; the rest are treated as confirmed) — this acts
as a built-in partial/locking mechanism that replaces type-A VAD's segmentation role. That is
why you can't find `vad` in the open-source streaming API: it simply **doesn't do
segmentation at this layer**.
- **Still needs type-B (endpoint) VAD — the open-source API just doesn't ship one**:
automatically deciding "user paused = this turn ended" is not handled by
`streaming_transcribe` itself. The commercial service wraps a `server_vad` layer **outside**
the ASR (the `turn_detection` block above) to do it. This service currently uses an explicit
**`STOP`** message from the client to substitute for that endpoint decision (in the bench,
STOP is sent once the audio finishes). **To auto-segment/auto-endpoint in a real scenario,
you need to add a VAD / endpoint detector outside this service** (the same role as commercial
`server_vad`), rather than looking for one inside Qwen3-ASR — its open-source streaming API
does not include this layer.
**In one line**: Qwen3-ASR's incremental streaming **removes the "segmentation VAD" (A)**, but
the **"endpoint / turn VAD" (B) responsibility still exists** — the commercial version
implements it via `server_vad`, while this service substitutes a manual `STOP`. The two are not
contradictory.
### 1.1 Official corroboration: the commercial Qwen-ASR-Realtime "VAD mode / Manual mode"
Aliyun Bailian's real-time ASR (Qwen-ASR-Realtime) docs explicitly split "who does the
segmentation" into two modes — essentially whether `session.turn_detection` is on or off:
- **VAD mode (default, `turn_detection` set to server_vad)**: the server automatically detects
speech start/end to segment; the client just keeps streaming audio, and the server returns the
final result automatically when it "detects a sentence has ended". In the flow the server emits
`input_audio_buffer.speech_started` / `speech_stopped` events — this is exactly the **type-B
endpoint VAD** above, implemented by that server-side (server_vad) layer, **not** the ASR core
doing segmentation.
- **Manual mode (`turn_detection` set to null)**: the **client** controls segmentation — after
sending a full sentence of audio, the client sends `input_audio_buffer.commit` to tell the
server the boundary. Suitable for scenarios where the client can clearly determine sentence
boundaries (e.g. "push-to-talk", sending a voice message in a chat app).
> Mapping: this service `serve_qwen3_asr_ws.py` uses an explicit client **`STOP`** to mark the
> end of a turn, which is equivalent to the commercial **Manual mode** (`turn_detection=null`,
> client-controlled boundaries). To make it "server auto-segmentation", you would implement that
> endpoint-detection layer of the commercial **VAD mode** (server_vad) — added outside this
> service's incremental transcription, not found inside the Qwen3-ASR transcription core.
>
> Docs: Real-time Speech Recognition (Qwen-ASR-Realtime) interaction flow
> (help.aliyun.com/zh/model-studio/qwen-asr-realtime-interaction-process)
### 1.2 `chunk-size-sec` controls the streaming chunk size
`chunk-size-sec` controls the streaming chunk size (default 2.0). Smaller = faster/more frequent
output, but higher concurrency cost (measured on L20, 29s audio, 48-way concurrency: 1.0 →
all failed, 2.0 → all passed).
---
## 2. Use vllm 0.14, not 0.19 (rope_scaling / thinker_config warnings)
This service needs vllm acceleration via `qwen-asr[vllm]`, which
[pins `vllm==0.14.0`](https://github.com/QwenLM/Qwen3-ASR/blob/main/pyproject.toml). Switching to
a newer vllm (e.g. 0.19.x) will produce at startup:
```
Unrecognized keys in `rope_scaling` for 'rope_type'='default':
{'mrope_section', 'mrope_interleaved', 'interleaved'}
thinker_config is None. Initializing thinker model with default values
```
**Root cause**: between vllm 0.14 → 0.19 (transformers is 4.57.6 in both, so it's ruled out),
the config parsing in `patch_rope_scaling_dict` rewrites `rope_type` from `'mrope'` to
`'default'` (treating mrope as legacy and assuming vllm internally consumes the `mrope_section`
etc. fields):
```python
elif rope_scaling["rope_type"] == "mrope":
assert "mrope_section" in rope_scaling
rope_scaling["rope_type"] = "default" # ← rewrite
```
But Qwen3-ASR's own `Qwen3ASRThinkerTextRotaryEmbedding` expects to read `"mrope"` from
`rope_scaling` to take the multimodal RoPE branch:
```python
self.rope_type = config.rope_scaling.get("rope_type", "default")
```
After vllm rewrites it to `"default"`, it takes the plain RoPE branch, and the keys
`mrope_section` / `mrope_interleaved` / `interleaved` are left unclaimed → the "Unrecognized
keys" warning, with degraded multimodal (audio/text) positional encoding. The
`thinker_config is None` line has the same origin: 0.19's load path doesn't correctly parse
Qwen3-ASR's thinker sub-config and falls back to default parameters.
**Impact & decision**: on 0.19 the service "starts and does emit text" (both lines are
WARNING/INFO, not ERROR), and spot-checked transcripts "look fine"; but the degraded positional
encoding may be harmful for long/complex audio, and **no CER quantitative comparison was done**,
so equivalence cannot be established. To be safe, stick with the `vllm==0.14.0` that
`qwen-asr[vllm]` ships.
### 2.1 vllm acceleration requires Qwen3ASRModel
funasr's `AutoModelVLLM` cannot accelerate Qwen3-ASR; you must use
`from qwen_asr import Qwen3ASRModel`.
> This answers the question in `#3026`.
### 2.2 Downloading model weights
The [Qwen3-ASR README](https://github.com/QwenLM/Qwen3-ASR#released-models-description-and-download) recommends pre-downloading weights when the runtime cannot access remote model hubs. Download the model to a local directory first, then pass that path as `--model`:
```bash
# Option 1: ModelScope (recommended in Mainland China)
pip install -U modelscope
modelscope download --model Qwen/Qwen3-ASR-1.7B --local_dir ./Qwen3-ASR-1.7B
# Option 2: Hugging Face
pip install -U "huggingface_hub[cli]"
huggingface-cli download Qwen/Qwen3-ASR-1.7B --local-dir ./Qwen3-ASR-1.7B
# Start the WebSocket server with the pre-downloaded weights
python serve_qwen3_asr_ws.py --model ./Qwen3-ASR-1.7B ...
```
Note: setting `VLLM_USE_MODELSCOPE=True` together with `pip install modelscope` only covers part of the download path. The vLLM layer fetches config, tokenizer, merges, vocab, and `model.safetensors.index.json` from ModelScope (log: `Downloading Model from https://www.modelscope.cn ... Finish downloading 10 files`), but the step that resolves the actual weights `model.safetensors` may still go back to `huggingface.co`:
This appears to be an issue in the current Qwen-ASR download path:
```
Downloading Model from https://www.modelscope.cn to directory: /home/vllm/.cache/modelscope/hub/models/Qwen/Qwen3-ASR-1.7B
2026-06-28 10:15:56,301 - modelscope - INFO - Got 10 files, start to download ...
Downloading [configuration.json]: 100%|
...
INFO 06-28 10:15:59 [model.py:530] Resolved architecture: Qwen3ASRForConditionalGeneration
'(MaxRetryError('HTTPSConnectionPool(host=\'huggingface.co\', port=443): Max retries exceeded with url: /Qwen/Qwen3-ASR-1.7B/resolve/main/model.safetensors (Caused by NewConnectionError("HTTPSConnection(host=\'huggingface.co\', port=443): Failed to establish a new connection: [Errno 101] Network is unreachable"))'), '(Request ID: f4f58ad5-e161-42ec-baad-cb2b8dbb63b9)')' thrown while requesting HEAD https://huggingface.co/Qwen/Qwen3-ASR-1.7B/resolve/main/model.safetensors
```
---
## 3. tokenizer `fix_mistral_regex` warning (harmless, ignorable)
At startup you may see:
```
The tokenizer you are loading from '.../Qwen3-ASR-1.7B' with an incorrect regex
pattern ... This will lead to incorrect tokenization. You should set the
`fix_mistral_regex=True` flag when loading this tokenizer to fix this issue.
```
**Cause**: Qwen3-ASR's tokenizer reuses a tokenizer implementation with a known regex issue; the
underlying library detects that regex pattern and warns, suggesting `fix_mistral_regex=True` to
fix the splitting. This service loads the model via qwen-asr's high-level API and does not
construct the tokenizer directly, so it does not expose this flag — hence the warning prints
as-is.
**Impact**: measured to have no visible effect on Chinese ASR transcripts (**spot-checked**
multiple transcripts as normal; no CER quantification). That regex fix mainly affects the
boundary splitting of certain special tokens, and no difference was observed on the speech
transcription path. It's "notice-level" noise and can be ignored. To eliminate it entirely you'd
pass `fix_mistral_regex=True` when loading the tokenizer at a lower level, but the qwen-asr
high-level API doesn't directly support that, and there's no measured need.
---
### Aside: two other startup log lines (both harmless)
- `Error retrieving safetensors: Repo id must be in the form ...`: it treats the local model
path as an HF repo id to fetch online metadata, fails, retries twice, and falls back to local
loading — no functional impact. Can be silenced with the env var `HF_HUB_OFFLINE=1`.
- `Downcasting torch.float32 to torch.bfloat16`: weights are stored as fp32 and loaded as bf16,
normally saving VRAM / speeding things up; bf16 has the same exponent width as fp32, so
precision loss is negligible. This is INFO, not an error.