Speech recognition API service centered on Qwen3-ASR, with CUDA vLLM and CPU Rust backends, OpenAI API compatibility, Alibaba Cloud Speech API compatibility, and a Paraformer realtime websocket capability.
- Web Demo: https://asr.vect.one
- Email: pengzhia@gmail.com
- WeChat:
v1.0.3removes the voiceprint database and sqlite-vec dependency, unifies offline deployment underHF_HUB_OFFLINE, and reduces the default deployment configuration to the settings in.env.example.
v1.0.0introduced a large breaking refactor relative to the earliermainbranch. If you are upgrading frommain, read the release notes before reusing old deployment assumptions.Key breaking changes:
- Python dependency management is now
uv-based (pyproject.toml+uv.lock);requirements*.txtare gone- Runtime stack changed to
CUDA -> official vLLM,CPU/macOS -> vendored QwenASR RustMLX/ Apple Silicon GPU path has been removed;mpsis normalized tocpu- macOS / Apple Silicon now defaults to
qwen3-asr-0.6b; setQWEN3_ASR_MODELto override itENABLED_MODELShas been removed- Voiceprint APIs and persistent speaker identity matching have been removed
- Hybrid Runtime Stack - Uses auto-selected Qwen3-ASR for offline inference and Paraformer realtime for websocket streaming
- Speaker Diarization - Automatic multi-speaker identification using CAM++ model
- OpenAI API Compatible - Supports
/v1/audio/transcriptionsendpoint, works with OpenAI SDK - Alibaba Cloud API Compatible - Supports Alibaba Cloud Speech RESTful API and WebSocket streaming protocol
- WebSocket Streaming - Real-time streaming speech recognition with low latency
- Smart Far-Field Filtering - Automatically filters far-field sounds and ambient noise in streaming ASR
- Intelligent Audio Segmentation - VAD-based greedy merge algorithm for automatic long audio splitting
- GPU Batch Processing - Batch inference support, 2-3x faster than sequential processing
- Resource-Aware Runtime - Auto-selects the appropriate Qwen3-ASR model for the current machine
- Qwen3-ASR provides the official model family and multimodal/vLLM usage guidance
- QwenASR provides the CPU Rust backend vendored by this project
# Copy and edit configuration
cp .env.example .env
# Edit .env to set API_KEY (optional)
# Start service (GPU version)
docker-compose up -d
# Or CPU version
docker-compose -f docker-compose-cpu.yml up -d
# Multi-GPU auto mode (one instance per visible GPU)
CUDA_VISIBLE_DEVICES=0,1,2,3 docker-compose up -dService URLs:
- API Endpoint:
http://localhost:17003 - API Docs:
http://localhost:17003/docs
docker run (alternative):
# GPU version
docker run -d --name qwen3-asr \
--gpus all \
-p 17003:8000 \
-e CUDA_VISIBLE_DEVICES=0,1,2,3 \
-e API_KEY=your_api_key \
-v ./models/modelscope:/root/.cache/modelscope \
-v ./models/huggingface:/root/.cache/huggingface \
-v ./data:/app/data \
quantatrisk/qwen3-asr:gpu-latest
# CPU version
docker run -d --name qwen3-asr \
-p 17003:8000 \
-v ./models/modelscope:/root/.cache/modelscope \
-v ./models/huggingface:/root/.cache/huggingface \
-v ./data:/app/data \
quantatrisk/qwen3-asr:cpu-latestNote: GPU images default to CUDA 12.8/cu128 for Blackwell-capable GPUs. Developers can rebuild
Dockerfile.gpufor CUDA 12.6, CUDA 13.0, or another backend by overriding Docker build args. CPU images now supportqwen3-asr-0.6bvia the bundled QwenASR Rust backend. The default CPU image uses a portable Rust target; setQWENASR_RUST_TARGET_CPU=nativeonly for self-built, host-specific images. On CUDA vLLM and CPU Rust,word_timestamps=truenow triggers the forced aligner automatically. On macOS / Apple Silicon, Qwen3-ASR now runs through the Rust CPU backend.
Custom GPU backend builds:
# Default GPU build: CUDA 12.8 / PyTorch cu128
docker build -t qwen3-asr:gpu-cu128 -f Dockerfile.gpu .
# CUDA 12.6 build for older deployments
docker build -t qwen3-asr:gpu-cu126 -f Dockerfile.gpu \
--build-arg PYTORCH_BASE_IMAGE=pytorch/pytorch:2.10.0-cuda12.6-cudnn9-runtime \
--build-arg PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu126 \
--build-arg CUDA_NVCC_PACKAGE=cuda-nvcc-12-6 \
--build-arg TORCH_CUDA_ARCH_LIST="8.0;8.6;8.9" \
.
# CUDA 13.0 build when your driver/toolchain requires it
docker build -t qwen3-asr:gpu-cu130 -f Dockerfile.gpu \
--build-arg PYTORCH_BASE_IMAGE=pytorch/pytorch:2.10.0-cuda13.0-cudnn9-runtime \
--build-arg PYTORCH_CUDA_INDEX=https://download.pytorch.org/whl/cu130 \
--build-arg CUDA_NVCC_PACKAGE=cuda-nvcc-13-0 \
--build-arg TORCH_CUDA_ARCH_LIST="12.0+PTX" \
.Offline Deployment: Use the helper script to prepare the current runtime model package, then copy to the offline machine:
# 1. Prepare models
./scripts/prepare-models.sh
# 2. Copy the package to offline server
scp qwen3-asr-models-*.tar.gz user@server:/opt/qwen3-asr/
# 3. On offline server, extract and start
tar -xzvf qwen3-asr-models-*.tar.gz
docker-compose up -dDetailed deployment instructions: Deployment Guide
System Requirements:
- Python 3.10+
- CUDA 12.8+ for the default GPU image; CUDA 12.6 / 13.0 can be built with Docker args
- FFmpeg (audio format conversion)
Installation:
Runtime dependency locks now default to the GPU stack at the repo root, with CPU kept as a specialized environment:
| Mode | Command | Notes |
|---|---|---|
| GPU (default) | uv sync |
Syncs the root pyproject.toml and uv.lock into .venv, including CUDA 12.8/cu128 torch/torchaudio/torchvision |
| CPU (specialized) | ./scripts/sync_cpu_env.sh |
Syncs the dedicated CPU lock in environments/cpu/pyproject.toml into .venv |
# Clone project
cd qwen3-asr
# Install dependencies (Linux/CUDA)
uv sync
# Start service
source .venv/bin/activate
python start.pymacOS / Apple Silicon local development:
./scripts/sync_cpu_env.sh
source .venv/bin/activate
python start.pyInteractive local terminals use the startup UI automatically. Containers and multi-worker deployments use normal logs.
Current runtime behavior on the mainline codebase:
DEVICE=autoresolves tocuda:0when CUDA is available, otherwisecpuDEVICE=mpsis normalized tocpuLinux + CUDAuses officialvLLMLinux + CPUuses vendoredQwenASRRustmacOS / Apple Siliconalso uses vendoredQwenASRRust- macOS / Apple Silicon defaults to
qwen3-asr-0.6b qwen3-asr-1.7bon macOS is only used whenQWEN3_ASR_MODEL=qwen3-asr-1.7bword_timestamps=trueworks on the current offline CUDA and CPU Rust paths- WebSocket streaming does not currently return word-level timestamps
- CAM++ speaker diarization remains required and still follows
DEVICE; on CPU its main hotspot is speaker verification embedding
| Endpoint | Method | Function |
|---|---|---|
/v1/audio/transcriptions |
POST | Audio transcription (OpenAI compatible) |
/v1/models |
GET | Offline model list |
Request Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
file |
file | Preferred when provided | Audio/video file |
audio_address |
string | Optional | Audio/video URL (HTTP/HTTPS). Ignored when file is also provided |
language |
string | Auto-detect | Language code (zh/en/ja) |
enable_speaker_diarization |
bool | true |
Enable speaker diarization |
word_timestamps |
bool | false |
Return word-level timestamps when the backend supports them. Qwen CUDA vLLM and CPU Rust automatically use the forced aligner when enabled. |
response_format |
string | verbose_json |
Output format |
prompt |
string | - | Prompt text (reserved) |
temperature |
float | 0 |
Sampling temperature (reserved) |
Audio / Video Input Methods:
- File Upload: Use
fileparameter to upload an audio file or a video container with an audio track - URL Download: Use
audio_addressparameter to provide an audio/video URL, service will download automatically - Precedence: If both
fileandaudio_addressare provided, the service usesfileand ignoresaudio_address
Usage Examples:
# Using OpenAI SDK
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="your_api_key")
with open("audio.wav", "rb") as f:
transcript = client.audio.transcriptions.create(
file=f,
response_format="verbose_json" # Get segments and speaker info
)
print(transcript.text)# Using curl
curl -X POST "http://localhost:8000/v1/audio/transcriptions" \
-H "Authorization: Bearer your_api_key" \
-F "file=@audio.wav" \
-F "model=qwen3-asr-0.6b" \
-F "response_format=verbose_json" \
-F "enable_speaker_diarization=true"Supported Response Formats: json, text, srt, vtt, verbose_json
| Endpoint | Method | Function |
|---|---|---|
/stream/v1/asr |
POST | Speech recognition (long audio support) |
/stream/v1/asr/models |
GET | Declared model/capability entries |
/stream/v1/asr/health |
GET | Health check |
/ws/v1/asr |
WebSocket | Streaming ASR (Alibaba Cloud protocol compatible) |
/ws/v1/asr/funasr |
WebSocket | FunASR streaming (backward compatible) |
/ws/v1/asr/qwen |
WebSocket | Qwen3-ASR streaming |
Request Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
audio_address |
string | https://media.cdn.vect.one/podcast_demo.mp4 (docs example) |
Audio/video URL (optional; ignored when body content is uploaded) |
sample_rate |
int | 16000 |
Sample rate |
enable_speaker_diarization |
bool | true |
Enable speaker diarization |
word_timestamps |
bool | false |
Return word-level timestamps when the backend supports them. Qwen CUDA vLLM and CPU Rust automatically use the forced aligner when enabled. |
vocabulary_id |
string | - | Hotword context (for example: word1 word2). Deprecated: numeric weights are unsupported and ignored. |
Usage Examples:
# Basic usage
curl -X POST "http://localhost:8000/stream/v1/asr" \
-H "Content-Type: application/octet-stream" \
--data-binary @audio.wav
# With parameters
curl -X POST "http://localhost:8000/stream/v1/asr?enable_speaker_diarization=true" \
-H "Content-Type: application/octet-stream" \
--data-binary @audio.wavResponse Example:
{
"task_id": "xxx",
"status": 200,
"message": "SUCCESS",
"result": "Speaker1 content...\nSpeaker2 content...",
"duration": 60.5,
"processing_time": 1.234,
"segments": [
{
"text": "Today is a nice day.",
"start_time": 0.0,
"end_time": 2.5,
"speaker_id": "Speaker1",
"word_tokens": [
{"text": "Today", "start_time": 0.0, "end_time": 0.5},
{"text": "is", "start_time": 0.5, "end_time": 0.7},
{"text": "a nice day", "start_time": 0.7, "end_time": 1.5}
]
}
]
}Multi-speaker automatic identification based on CAM++ model:
- Enabled by Default -
enable_speaker_diarization=true - Automatic Detection - No preset speaker count needed, model auto-detects
- Speaker Labels - Response includes
speaker_idfield (e.g., "Speaker1", "Speaker2") - Smart Merging - Two-layer merge strategy to avoid isolated short segments:
- Layer 1: Accumulate merge same-speaker segments < 10 seconds
- Layer 2: Accumulate merge continuous segments up to 60 seconds
- Subtitle Support - SRT/VTT output includes speaker labels
[Speaker1] text content
Disable speaker diarization:
# OpenAI API
-F "enable_speaker_diarization=false"
# Alibaba Cloud API
?enable_speaker_diarization=falseAutomatic long audio segmentation:
- VAD Voice Detection - Detect voice boundaries, filter silence
- Greedy Merge - Accumulate voice segments, ensure each segment does not exceed
MAX_SEGMENT_SEC(default 60s) - Silence Split - Force split when silence between voice segments exceeds 3 seconds
- Batch Inference - Multi-segment parallel processing, 2-3x performance improvement in GPU mode
FunASR Model Limitations (using /ws/v1/asr or /ws/v1/asr/funasr):
- ✅ Real-time speech recognition, low latency
- ✅ Sentence-level timestamps
- ❌ Word-level timestamps (not implemented on the FunASR realtime path)
- ❌ Confidence scores (not implemented)
- Audio ingress is bounded to 10 seconds and applies WebSocket backpressure instead of dropping audio. Send incrementally and continue reading recognition events.
- This path uses Paraformer, while offline endpoints use Qwen3-ASR. Compare latency within the same path, not their total throughput.
Qwen3-ASR Streaming (using /ws/v1/asr/qwen):
- ✅ Multi-language real-time recognition
- ✅ CUDA vLLM and CPU Rust both support the current streaming path
- ❌ Word-level timestamps are not available in the current streaming path
| Runtime | Backend | Offline | WebSocket Streaming | Word Timestamps Offline | Word Timestamps Streaming | Maturity |
|---|---|---|---|---|---|---|
| Linux + NVIDIA GPU | Official vLLM 0.19.0 | ✅ | ✅ | ✅ | ❌ | Production-oriented |
| CPU / macOS | QwenASR Rust | ✅ | ✅ | ✅ (forced aligner) | ❌ | Recommended local fallback |
| Model ID | Name | Description | Features |
|---|---|---|---|
qwen3-asr-1.7b |
Qwen3-ASR 1.7B | High-performance multilingual ASR, 52 languages + dialects; CUDA uses vLLM | Offline/Realtime |
qwen3-asr-0.6b |
Qwen3-ASR 0.6B | Lightweight multilingual ASR; CUDA uses vLLM, CPU/macOS uses Rust backend | Offline/Realtime |
| Capability ID | Runtime | Description |
|---|---|---|
paraformer-large |
FunASR realtime | Chinese websocket realtime stack with realtime punctuation |
Runtime selection:
- VRAM >= 32GB: Select
qwen3-asr-1.7b - VRAM < 32GB: Select
qwen3-asr-0.6b - No CUDA: Select the vendored Rust-backed
qwen3-asr-0.6b - macOS / Apple Silicon: Always default to
qwen3-asr-0.6b, regardless of memory size - Environment override: Set
QWEN3_ASR_MODEL=qwen3-asr-1.7borQWEN3_ASR_MODEL=qwen3-asr-0.6bto bypass automatic selection paraformer-largerealtime capability is always prepared for websocket streaming
At startup the service checks the current runtime model plan and downloads missing models by default. Set HF_HUB_OFFLINE=1 only for strictly offline deployments with a prepared cache.
Settings in .env.example:
| Variable | Default | Description |
|---|---|---|
NGINX_PORT |
17003 |
Host port exposed by Docker Compose |
API_KEY |
- | API authentication key (optional, unauthenticated if not set) |
CUDA_VISIBLE_DEVICES |
0 |
Visible GPU list; one backend instance is started per visible GPU |
QWEN3_ASR_MODEL |
auto | Force qwen3-asr-1.7b or qwen3-asr-0.6b instead of VRAM-based selection |
HF_HUB_OFFLINE |
unset | Set to 1 only after preparing ./models for offline deployment |
HF_ENDPOINT |
unset | Online Hugging Face mirror endpoint, for example https://hf-mirror.com |
After starting the service:
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
- Deployment Guide: Detailed Docs
- Qwen3-ASR: Qwen3-ASR GitHub
- FunASR: FunASR GitHub
- Chinese README: 中文文档
This project uses the MIT License - see LICENSE file for details.
Issues and Pull Requests are welcome to improve the project!

