Wait when uncertain, emit when ready: append-only streaming speech recognition
Words appear tens of milliseconds after they are spoken, and are never taken back.
English | 简体中文
Speech flows in, final text flows out: captions appear as you speak, ambiguous words wait for context, and committed text is safe for downstream agents to act on.
News · Overview · Demo · Why X2 · Results · Method · Quick Start · Citation
- [2026-09] The paper is on arXiv: 2609.08672.
- [2026-10] Model weights on Hugging Face: x-square-robot/X2Streaming-ASR-4B-1009.
X2Streaming-ASR is a low-latency, append-only streaming ASR model for Chinese, English, and code-switching speech. On the evaluated benchmarks, it emits each character or word with a mean latency of only 32–109 ms in Chinese and 12–85 ms in English after the word ends, while achieving the best CER/WER on several test sets.
Built on Voxtral Mini Realtime, it splits streaming recognition into when to commit and what to commit. Every 80 ms the model chooses between LISTEN and DECODE:
- Wait when uncertain. If the latest audio is ambiguous, the model keeps listening for more context.
- Emit when ready. Once the context is sufficient, it decodes the new audio immediately.
- Never roll back. Output is append-only, so text is never revised after it appears.
- Native causal streaming. Only newly arrived audio is encoded, and history is reused through the KV cache instead of being recomputed or cut off by a sliding window.
This repository is inference-only: adaptive streaming and full-context offline inference, Transformers and vLLM backends, JSONL batch transcription, and a web demo.
Unmute for audio.
demo.mp4
- Colored tokens in the reference transcript identify the model that first produced a reliably aligned committed result: X2 in cyan, Qwen in blue, R2T2 in orange, and X-ASR in purple. Once assigned, a color never changes.
- The live error rate compares each model's committed output with subtitle cues that ended at least 1.5 seconds before the current playback position.
Fixed chunks or lookaheads make every word wait the same amount of time. Committing exactly at the acoustic boundary gets homophones wrong. Revising partial results makes text flicker and is unsafe for voice agents. Re-encoding the whole history or sliding a short window either slows down on long audio or forgets earlier speech. X2Streaming-ASR learns a per-word commit policy that avoids all four.
- Lowest latency on all 12 test sets. Mean emission latency is 12–109 ms, compared with 194–735 ms for the streaming baselines.
- Best accuracy on 6 test sets. AISHELL-1, AISHELL-3, WenetSpeech Net, GigaSpeech clean, VoxPopuli, and TED-LIUM.
- Lowest overall error rate in both languages. Pooled over all test utterances, X2Streaming-ASR reaches 5.65% CER on Chinese (1.15M characters) and 8.35% WER on English (577K words), ahead of the next best system, Confucius4-R2T2 (quality), at 5.79% and 8.43% with 8–12× higher latency. Pooled rates weight each test set by its size, so the large, harder sets dominate: GigaSpeech is about 70% of the English words (10.01% WER), and the two WenetSpeech sets are about 55% of the Chinese characters. The per-set averages are 4.65% CER and 5.64% WER.
- Adaptive waiting beats fixed lookahead. On AISHELL-1, a fixed lookahead needs 396 ms of mean latency to reach 1.58% CER; X2Streaming-ASR reaches 1.53% at 32 ms.
All systems are evaluated with a single hard commit and no rollback. Latency is measured from the forced-aligned end of each character or word to its emission.
Chinese scores are CER (%). English scores are WER (%). Latency is in milliseconds.
| Dataset | X2Streaming-ASR | X-ASR (160 ms) | Confucius4-R2T2 (speed) | Confucius4-R2T2 (quality) | Zipformer | Paraformer | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CER | Mean | P95 | CER | Mean | P95 | CER | Mean | P95 | CER | Mean | P95 | CER | Mean | P95 | CER | Mean | P95 | |
| AISHELL-1 | 1.53 | 32 | 320 | 4.81 | 651 | 880 | 3.55 | 308 | 560 | 2.29 | 735 | 1280 | 1.93 | 472 | 800 | 3.09 | 586 | 880 |
| AISHELL-2 android | 4.27 | 81 | 400 | 4.87 | 657 | 926 | 4.59 | 318 | 560 | 3.56 | 628 | 1040 | 4.19 | 447 | 800 | 4.05 | 567 | 840 |
| AISHELL-2 ios | 3.94 | 75 | 400 | 4.77 | 654 | 922 | 4.64 | 316 | 560 | 3.58 | 620 | 1040 | 4.12 | 450 | 800 | 3.81 | 572 | 840 |
| AISHELL-2 mic | 4.14 | 79 | 480 | 4.89 | 658 | 923 | 4.59 | 322 | 560 | 3.61 | 632 | 1040 | 4.21 | 453 | 800 | 3.91 | 566 | 840 |
| AISHELL-3 | 2.08 | 57 | 400 | 5.14 | 660 | 899 | 3.98 | 321 | 560 | 2.85 | 672 | 1120 | 2.95 | 465 | 800 | 3.72 | 576 | 849 |
| WenetSpeech Meeting | 8.67 | 74 | 320 | 11.28 | 595 | 810 | 8.63 | 320 | 560 | 7.37 | 685 | 1200 | 7.88 | 436 | 800 | 10.06 | 582 | 880 |
| WenetSpeech Net | 7.88 | 109 | 400 | 8.63 | 563 | 800 | 9.52 | 310 | 480 | 8.48 | 570 | 960 | 8.53 | 409 | 720 | 8.36 | 563 | 880 |
| Dataset | X2Streaming-ASR | X-ASR (160 ms) | Confucius4-R2T2 (speed) | Confucius4-R2T2 (quality) | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| WER | Mean | P95 | WER | Mean | P95 | WER | Mean | P95 | WER | Mean | P95 | |
| LibriSpeech test-clean | 2.20 | 29 | 240 | 3.86 | 574 | 800 | 2.83 | 194 | 320 | 2.11 | 472 | 800 |
| LibriSpeech test-other | 5.50 | 85 | 480 | 9.64 | 585 | 800 | 6.38 | 201 | 320 | 4.81 | 463 | 720 |
| GigaSpeech (clean) | 10.01 | 43 | 320 | 10.47 | 592 | 847 | 10.69 | 204 | 400 | 10.21 | 501 | 880 |
| VoxPopuli | 7.07 | 12 | 320 | 11.25 | 603 | 880 | 8.97 | 204 | 400 | 7.26 | 523 | 880 |
| TED-LIUM | 3.42 | 15 | 240 | 3.81 | 590 | 880 | 3.98 | 196 | 320 | 3.45 | 497 | 880 |
See the paper for complete benchmark settings, baselines, and ablations.
Every 80 ms audio token, the model predicts Wait (keep listening) or Emit (start decoding). After Emit, it writes the text for the new audio and ends with Eos.
X2Streaming-ASR keeps the causal audio encoder, adapter, and decoder-only language model of Voxtral Realtime, but replaces its fixed delay with a learned decision:
- LISTEN. For each new 80 ms audio token, predict Wait if the context is insufficient or ambiguous, or Emit if it is enough to commit.
- DECODE. After Emit, generate the text for the newly heard audio until
Eos, then return to LISTEN. - Reuse history. The encoder runs incrementally and the language model keeps its KV cache, so no chunk triggers recomputation of earlier audio. Within the configured context length, nothing is discarded by a sliding window.
The commit policy is trained in three stages: streaming recognition, a supervised warm start from probed commit times, and GRPO with character-level credit assignment that rewards accuracy first and latency second. See the paper for details.
| Item | Description |
|---|---|
| Base model | Voxtral Mini Realtime |
| Languages | Chinese, English, and code-switching |
| Streaming output | Append-only hard commits |
| Inference modes | Adaptive streaming and full-context offline |
| Backends | Transformers and vLLM |
| Model weights | x-square-robot/X2Streaming-ASR-4B-1009 |
Linux, Python 3.11, and a CUDA-compatible NVIDIA GPU are recommended.
git clone https://github.com/ZwlinZw/X2StreamingASR.git
cd X2StreamingASR
conda create -n x2streamingasr python=3.11 -y
conda activate x2streamingasr
pip install -r requirements.txtInstall optional vLLM and web-demo dependencies when needed:
pip install -r requirements-vllm.txt -r demo/requirements.txtModel weights are on Hugging Face: x-square-robot/X2Streaming-ASR-4B-1009.
Download and set MODEL_PATH:
pip install -U huggingface_hub
huggingface-cli download x-square-robot/X2Streaming-ASR-4B-1009 --local-dir ./X2Streaming-ASR-4B-1009
export MODEL_PATH="$(pwd)/X2Streaming-ASR-4B-1009"Or use any local checkpoint directory:
export MODEL_PATH=/path/to/X2Streaming-ASR-4B-1009# Adaptive streaming
python infer.py \
--config configs/infer.yaml \
--model_path "$MODEL_PATH" \
--language zh \
--audio /path/to/audio.wav
# Full-context offline
python infer.py \
--config configs/infer_offline.yaml \
--model_path "$MODEL_PATH" \
--language en \
--audio /path/to/audio.wavImportant
Set --language to match your audio. The model is conditioned on a
language tag: zh for Chinese, en for English, and cs for
Chinese-English code-switching. Without --language, the tag is guessed from
the input path and falls back to zh unless the path contains
librispeech, gigaspeech, tedlium, or voxpopuli, so English audio
should always be run with --language en. One tag is used for the whole run,
so split mixed-language JSONL files by language or use cs.
Each input line must contain one of audio, wav, or wav_path:
{"audio": "/path/to/first.wav"}
{"wav": "/path/to/second.wav"}python infer.py \
--config configs/infer.yaml \
--model_path "$MODEL_PATH" \
--language en \
--input_file audio.jsonl \
--output_file results.jsonlThe output JSONL contains the audio path, recognized text, inference mode, and optional timestamps or adaptive emit records. A reference transcript is not required.
BACKEND=transformers \
MODE=adaptive \
LANGUAGE=zh \
REPO_ID="$MODEL_PATH" \
bash demo/run.shOpen http://localhost:7860. LANGUAGE sets the default language tag (zh,
en, or cs; default zh); use LANGUAGE=en for English speech.
For vLLM, convert and serve the model first:
python tools/convert_hf_to_mistral.py "$MODEL_PATH"
vllm serve "$MODEL_PATH" \
--host 0.0.0.0 \
--port 8000 \
--tokenizer-mode mistral \
--config-format mistral \
--enforce-eager \
--gpu-memory-utilization 0.9Then launch the demo proxy:
BACKEND=vllm \
MODE=adaptive \
LANGUAGE=zh \
VLLM_HOST=127.0.0.1 \
VLLM_PORT=8000 \
REPO_ID="$MODEL_PATH" \
bash demo/run.shIf X2Streaming-ASR is useful for your research, please cite:
@article{lin2026x2streaming,
title={X2Streaming-ASR: wait when uncertain, emit when ready for streaming ASR},
author={Lin, Zhiwei and Fu, Kaiqi and Wen, Rime and Liu, Zehan and Qin, Shawn and Gan, Roy and Wang, Hao and Wang, Qian},
journal={arXiv preprint arXiv:2609.08672},
year={2026}
}- Source code: Apache License 2.0
- Model weights: see MODEL_LICENSE.md
- Upstream model: mistralai/Voxtral-Mini-4B-Realtime-2602


