Automatic Speech Recognition
NeMo
Thai
English
speech
streaming
Thai
FastConformer
Transducer
NeMo
Nemotron
Instructions to use iapp/OpenThai2.0-ASR-Streaming with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- NeMo
How to use iapp/OpenThai2.0-ASR-Streaming with NeMo:
import nemo.collections.asr as nemo_asr asr_model = nemo_asr.models.ASRModel.from_pretrained("iapp/OpenThai2.0-ASR-Streaming") transcriptions = asr_model.transcribe(["file.wav"]) - Notebooks
- Google Colab
- Kaggle
Model card; benchmark outputs and scorer; serve/: long recordings cut at pauses, WS /v1/live
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +279 -1
- benchmark/README.md +47 -0
- benchmark/chinda_norm.py +114 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_fleurs.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_gs2.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_gs2_long.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_isan.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_bt10m.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_bt3m.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_con123.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_jbl.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_lav123.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_tv_long.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_tvspeech.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_320ms_fleurs.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_320ms_gs2.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_320ms_isan.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_320ms_lotus_con123.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_320ms_tvspeech.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_fleurs.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_gs2.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_gs2_long.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_gs2_long_split.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_isan.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_lotus_con123.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_tv_long.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_tv_long_split.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_560ms_tvspeech.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_80ms_fleurs.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_80ms_gs2.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_80ms_isan.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_80ms_lotus_con123.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_80ms_tvspeech.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_th-mix_1120ms_lotus_con123.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_th-mix_1120ms_tvspeech.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_th-mix_560ms_lotus_con123.jsonl +0 -0
- benchmark/hypotheses/openthai2.0-asr-streaming_th-mix_560ms_tvspeech.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_fleurs.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_gs2.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_gs2_long.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_isan.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_bt10m.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_bt3m.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_con123.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_jbl.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_lav123.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_tv_long.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_tvspeech.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_320ms_fleurs.jsonl +0 -0
- benchmark/hypotheses/typhoon-asr-streaming-0.6b_320ms_gs2.jsonl +0 -0
README.md
CHANGED
|
@@ -2,8 +2,21 @@
|
|
| 2 |
license: other
|
| 3 |
license_name: openmdw-1.1
|
| 4 |
license_link: https://openmdw.ai/license/1-1/
|
|
|
|
| 5 |
language:
|
| 6 |
- th
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
extra_gated_heading: "Register to download OpenThai2.0-ASR-Streaming · ลงทะเบียนเพื่อดาวน์โหลด"
|
| 8 |
extra_gated_description: "Free. Fill in the form once and access is granted at once. · ดาวน์โหลดฟรี กรอกแบบฟอร์มครั้งเดียว ได้รับสิทธิ์ทันที"
|
| 9 |
extra_gated_prompt: >-
|
|
@@ -36,4 +49,269 @@ extra_gated_button_content: "Submit · ส่งข้อมูล"
|
|
| 36 |
|
| 37 |
# OpenThai2.0-ASR-Streaming
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
license: other
|
| 3 |
license_name: openmdw-1.1
|
| 4 |
license_link: https://openmdw.ai/license/1-1/
|
| 5 |
+
base_model: nvidia/nemotron-3.5-asr-streaming-0.6b
|
| 6 |
language:
|
| 7 |
- th
|
| 8 |
+
- en
|
| 9 |
+
library_name: nemo
|
| 10 |
+
pipeline_tag: automatic-speech-recognition
|
| 11 |
+
tags:
|
| 12 |
+
- automatic-speech-recognition
|
| 13 |
+
- speech
|
| 14 |
+
- streaming
|
| 15 |
+
- Thai
|
| 16 |
+
- FastConformer
|
| 17 |
+
- Transducer
|
| 18 |
+
- NeMo
|
| 19 |
+
- Nemotron
|
| 20 |
extra_gated_heading: "Register to download OpenThai2.0-ASR-Streaming · ลงทะเบียนเพื่อดาวน์โหลด"
|
| 21 |
extra_gated_description: "Free. Fill in the form once and access is granted at once. · ดาวน์โหลดฟรี กรอกแบบฟอร์มครั้งเดียว ได้รับสิทธิ์ทันที"
|
| 22 |
extra_gated_prompt: >-
|
|
|
|
| 49 |
|
| 50 |
# OpenThai2.0-ASR-Streaming
|
| 51 |
|
| 52 |
+
Streaming Thai speech-to-text with 0.64B parameters. It writes Thai speech down while people are still talking, 80 to 1120 ms behind the voice, from one model with four latency settings. It is a Thai fine-tune of NVIDIA **Nemotron 3.5 ASR Streaming 0.6B**, made by iApp Technology, and it is the model behind iApp's hosted [OpenThai ASR Realtime](https://iapp.co.th/docs/speech/speech-to-text/realtime) service.
|
| 53 |
+
|
| 54 |
+
**Built on NVIDIA Nemotron.** The base model is NVIDIA's [`nvidia/nemotron-3.5-asr-streaming-0.6b`](https://huggingface.co/nvidia/nemotron-3.5-asr-streaming-0.6b), released under the OpenMDW License Agreement, version 1.1. That licence is included as [`LICENSE`](LICENSE), and NVIDIA's original model card and sub-cards are kept unchanged in [`nvidia/`](nvidia/).
|
| 55 |
+
|
| 56 |
+
| | |
|
| 57 |
+
|---|---|
|
| 58 |
+
| Architecture | Cache-aware streaming FastConformer encoder + RNN-Transducer decoder, prompt-conditioned (NeMo `EncDecRNNTBPEModelWithPrompt`) |
|
| 59 |
+
| Parameters | 640.4 million |
|
| 60 |
+
| Latency settings | 80, 320, 560 and 1120 ms (`att_context_size` `[56,0]`, `[56,3]`, `[56,6]`, `[56,13]`), chosen at decode time |
|
| 61 |
+
| Language prompts | `th-TH`: everything in Thai script. `th-mix`: English words in Latin letters |
|
| 62 |
+
| Tokenizer | 14,988 pieces: NVIDIA's 13,087 multilingual pieces and 1,901 new Thai pieces |
|
| 63 |
+
| Training data | 12,373 hours of Thai speech and 961 hours of English (LibriSpeech) |
|
| 64 |
+
| Licence | OpenMDW-1.1 |
|
| 65 |
+
|
| 66 |
+
## Results
|
| 67 |
+
|
| 68 |
+

|
| 69 |
+
|
| 70 |
+
We compared it with [Typhoon ASR Streaming 0.6B](https://huggingface.co/typhoon-ai/typhoon-asr-streaming-nemotron-0.6b), the other open Thai model built on the same NVIDIA base. We decoded both ourselves, on public test sets, with the same script and settings.
|
| 71 |
+
|
| 72 |
+
- At 560 ms this model makes fewer errors on read speech (FLEURS: 8.47% of characters wrong, against 11.28%), on YouTube clips (GigaSpeech2: 15.35% against 16.33%) and in meetings (LOTUSDIS: 20.77% against 28.91%, a set that favours us).
|
| 73 |
+
- Typhoon makes fewer errors on TV talk shows (TVSpeech, its own benchmark: 10.84% against 12.41%). The two tie on the Isan dialect.
|
| 74 |
+
- Typhoon's 4-gram language model raises its English-word recall, as its card says, but adds errors on every set here.
|
| 75 |
+
- On whole recordings, decode in pieces cut at pauses, as the included server does. Decoded as one stream, this model drops words after quiet pauses.
|
| 76 |
+
|
| 77 |
+
**560 ms** (`att_context_size` `[56,6]`), character error rate in %, lower is better:
|
| 78 |
+
|
| 79 |
+
| test set | size | OpenThai2.0-ASR-Streaming | Typhoon ASR Streaming 0.6B | Typhoon + its 4-gram LM | ours − Typhoon, 95% interval |
|
| 80 |
+
|---|---|---:|---:|---:|---|
|
| 81 |
+
| FLEURS test, read speech | 1,021 clips | **8.47** | 11.28 | 11.73 | −2.82 [−3.15, −2.48], ours lower |
|
| 82 |
+
| GigaSpeech2 test, YouTube | 8,823 clips, 32 recordings | **15.35** | 16.33 | 16.62 | −0.98 [−1.31, −0.58], ours lower |
|
| 83 |
+
| TVSpeech, TV and YouTube talk | 570 clips, 18 videos | 12.41 | **10.84** | 11.05 | +1.57 [+0.90, +2.52], Typhoon lower |
|
| 84 |
+
| LOTUSDIS meetings, conference microphone ¹ | 5,516 turns, 11 meetings | **20.77** | 28.91 | 29.02 | −8.15 [−8.88, −7.46], ours lower |
|
| 85 |
+
| Isan dialect | 500 clips | 34.90 | 34.15 | 35.79 | +0.75 [−0.21, +1.69], tie |
|
| 86 |
+
|
| 87 |
+
**1120 ms** (`att_context_size` `[56,13]`), character error rate in %, lower is better:
|
| 88 |
+
|
| 89 |
+
| test set | size | OpenThai2.0-ASR-Streaming | Typhoon ASR Streaming 0.6B | Typhoon + its 4-gram LM | ours − Typhoon, 95% interval |
|
| 90 |
+
|---|---|---:|---:|---:|---|
|
| 91 |
+
| FLEURS test, read speech | 1,021 clips | **8.36** | 11.32 | 11.64 | −2.96 [−3.31, −2.65], ours lower |
|
| 92 |
+
| GigaSpeech2 test, YouTube | 8,823 clips, 32 recordings | **15.23** | 16.18 | 16.44 | −0.95 [−1.35, −0.49], ours lower |
|
| 93 |
+
| TVSpeech, TV and YouTube talk | 570 clips, 18 videos | 12.14 | **10.70** | 11.18 | +1.45 [+0.77, +2.46], Typhoon lower |
|
| 94 |
+
| LOTUSDIS meetings, conference microphone ¹ | 5,516 turns, 11 meetings | **20.30** | 28.23 | 28.64 | −7.93 [−8.66, −7.24], ours lower |
|
| 95 |
+
| Isan dialect | 500 clips | 34.49 | 33.73 | 35.68 | +0.76 [−0.28, +1.86], tie |
|
| 96 |
+
|
| 97 |
+
Bold: lower than the other model in that row with 95% confidence (paired bootstrap, see below).
|
| 98 |
+
|
| 99 |
+
**Whole recordings**, 560 ms. Each file joins every clip of one video or recording, in order, with one second of silence between clips. *One stream* decodes the file in one pass, as NeMo's script does; *cut at pauses* decodes it the way [`serve/`](serve/) does, in pieces cut where nobody speaks for 0.7 s, each decoded on its own (both models through the same code, Typhoon at its released settings).
|
| 100 |
+
|
| 101 |
+
| test set | size | one stream: ours | one stream: Typhoon | ours − Typhoon, 95% interval | cut at pauses: ours | cut at pauses: Typhoon | ours − Typhoon, 95% interval |
|
| 102 |
+
|---|---|---:|---:|---|---:|---:|---|
|
| 103 |
+
| TVSpeech, whole videos (0.5–47 min) | 18 files | 23.54 | **11.07** | +12.47 [+7.49, +17.08], Typhoon lower | 12.66 | **10.84** | +1.82 [+1.16, +2.74], Typhoon lower |
|
| 104 |
+
| GigaSpeech2 test, whole recordings (5–87 min) ² | 31 files | **23.10** | 30.07 | −6.97 [−12.06, −2.63], ours lower | **13.53** | 14.42 | −0.88 [−1.34, −0.41], ours lower |
|
| 105 |
+
|
| 106 |
+
Decoded as one stream, this model loses words after a pause of near-silence: the longer and quieter the pause, the more it drops (see [Limits](#limits)). Cut at pauses, it comes back near its clip-by-clip error on TVSpeech (12.66 whole against 12.41 clip by clip) and below it on GigaSpeech2 (13.53 against 15.35).
|
| 107 |
+
|
| 108 |
+
**By latency**, character error rate in %, ours / Typhoon (as released):
|
| 109 |
+
|
| 110 |
+
| test set | 80 ms | 320 ms | 560 ms | 1120 ms |
|
| 111 |
+
|---|---:|---:|---:|---:|
|
| 112 |
+
| FLEURS test, read speech | 8.73 / 11.78 | 8.53 / 11.67 | 8.47 / 11.28 | 8.36 / 11.32 |
|
| 113 |
+
| GigaSpeech2 test, YouTube | 16.64 / 17.75 | 15.53 / 16.64 | 15.35 / 16.33 | 15.23 / 16.18 |
|
| 114 |
+
| TVSpeech, TV and YouTube talk | 13.06 / 11.44 | 12.54 / 10.84 | 12.41 / 10.84 | 12.14 / 10.70 |
|
| 115 |
+
| LOTUSDIS meetings, conference microphone | 26.12 / 34.35 | 21.79 / 29.49 | 20.77 / 28.91 | 20.30 / 28.23 |
|
| 116 |
+
| Isan dialect | 35.84 / 35.37 | 34.72 / 34.65 | 34.90 / 34.15 | 34.49 / 33.73 |
|
| 117 |
+
|
| 118 |
+
**Typhoon with a tuned blank offset.** Our released file stores a blank offset of −1.0, chosen on a development set. Typhoon's best offset on the same development set is −2.0. With it, at 560 ms:
|
| 119 |
+
|
| 120 |
+
| test set | ours | Typhoon, offset −2.0 | ours − Typhoon, 95% interval |
|
| 121 |
+
|---|---:|---:|---|
|
| 122 |
+
| FLEURS test, read speech | 8.47 | 11.63 | −3.17 [−3.50, −2.83], ours lower |
|
| 123 |
+
| GigaSpeech2 test, YouTube | 15.35 | 15.26 | +0.10 [−0.38, +0.63], tie |
|
| 124 |
+
| TVSpeech, TV and YouTube talk | 12.41 | 10.68 | +1.73 [+1.14, +2.61], Typhoon lower |
|
| 125 |
+
| LOTUSDIS meetings, conference microphone | 20.77 | 26.71 | −5.94 [−6.50, −5.41], ours lower |
|
| 126 |
+
| Isan dialect | 34.90 | 33.87 | +1.03 [+0.24, +1.91], Typhoon lower |
|
| 127 |
+
|
| 128 |
+
**English words** on TVSpeech at 560 ms: the share of the reference's English words written in Latin letters (recall), the share of written English words that are right (precision), and their F1; exact match, ignoring case.
|
| 129 |
+
|
| 130 |
+
| | recall | precision | F1 |
|
| 131 |
+
|---|---:|---:|---:|
|
| 132 |
+
| ours, `th-mix` | 13.7 | 22.6 | 17.0 |
|
| 133 |
+
| ours, `th-TH` | 0.5 | 75.0 | 0.9 |
|
| 134 |
+
| Typhoon | 16.1 | 30.6 | 21.1 |
|
| 135 |
+
| Typhoon + 4-gram | 18.9 | 29.0 | 22.9 |
|
| 136 |
+
|
| 137 |
+
**By the method of the public Thai ASR Leaderboard** (pilot 2026.10: spoken-form normalisation, six weighted groups), 1120 ms. This is our own measurement, not an entry on that board.
|
| 138 |
+
|
| 139 |
+
| | FLEURS | Isan | TVSpeech | GigaSpeech2 | close mics | far mics | weighted |
|
| 140 |
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
| 141 |
+
| OpenThai2.0-ASR-Streaming | 8.29 | 33.85 | 11.90 | 15.08 | 22.44 | 54.99 | 22.15 |
|
| 142 |
+
| Typhoon ASR Streaming 0.6B | 11.20 | 33.30 | 10.59 | 16.18 | 30.72 | 72.11 | 26.25 |
|
| 143 |
+
| Typhoon + 4-gram | 11.54 | 35.26 | 11.07 | 16.44 | 31.07 | 73.05 | 26.91 |
|
| 144 |
+
|
| 145 |
+
How we measured:
|
| 146 |
+
- Every model was decoded by us with NeMo's cache-aware streaming script (`speech_to_text_cache_aware_streaming_infer.py`), greedy, `max_symbols_per_step=2`, prompt `th-TH` (with NeMo's default of 10 instead, Typhoon's error moves by at most 0.06 points on FLEURS, TVSpeech and GigaSpeech2). Typhoon is [`typhoon-ai/typhoon-asr-streaming-nemotron-0.6b`](https://huggingface.co/typhoon-ai/typhoon-asr-streaming-nemotron-0.6b) as released, and with its paired 4-gram ([`...-0.6b-ngram`](https://huggingface.co/typhoon-ai/typhoon-asr-streaming-nemotron-0.6b-ngram), weight 0.5, as its card says).
|
| 147 |
+
- Both sides are normalised the same way before scoring: Thai digits and spoken numbers made equal, spaces removed; then true edit distance over all characters of a set.
|
| 148 |
+
- Intervals are 95% paired bootstrap intervals that resample whole videos (TVSpeech), recordings (GigaSpeech2), speakers (Isan), meetings (LOTUSDIS) or files (joined sets). An interval that includes zero is a tie.
|
| 149 |
+
- Latency is the audio chunk the model waits for, (right context + 1) × 80 ms. Typhoon's card counts only the look-ahead, so its "1040 ms" is the 1120 ms setting here.
|
| 150 |
+
- ¹ We trained on other LOTUSDIS meetings from the same rooms and microphones, and some speakers appear in both, so that row favours us. TVSpeech is Typhoon's own benchmark. We trained on FLEURS's training and development splits, not its test split. Both models trained on GigaSpeech2's training split.
|
| 151 |
+
- ² GigaSpeech2 test recording 26 is left out of the joined set: its transcripts do not match its audio (about 70% errors for every model). It stays in the 8,823-clip set above, as published.
|
| 152 |
+
|
| 153 |
+
## Use
|
| 154 |
+
|
| 155 |
+
There are two files. Use the first unless you run the reference server.
|
| 156 |
+
|
| 157 |
+
| file | for |
|
| 158 |
+
|---|---|
|
| 159 |
+
| `openthai2.0-asr-streaming_bias-1.0.nemo` | NeMo's streaming script and your own code. The blank offset −1.0 that every number above uses is stored in the file. |
|
| 160 |
+
| `openthai2.0-asr-streaming_raw.nemo` | The reference server in [`serve/`](serve/), which sets the blank offset per mode itself. |
|
| 161 |
+
|
| 162 |
+
Both need NVIDIA NeMo (Speech) with cache-aware streaming and `EncDecRNNTBPEModelWithPrompt`. We tested NeMo Speech at commit `cf724ac`, PyTorch 2.13 and Python 3.12:
|
| 163 |
+
|
| 164 |
+
```bash
|
| 165 |
+
uv venv asr --python 3.12 && source asr/bin/activate
|
| 166 |
+
uv pip install torch==2.13.0 --index-url https://download.pytorch.org/whl/cu130 # or the CPU wheel
|
| 167 |
+
uv pip install Cython packaging
|
| 168 |
+
uv pip install --extra-index-url https://download.pytorch.org/whl/cu130 --index-strategy unsafe-best-match \
|
| 169 |
+
"nemo-toolkit[asr] @ git+https://github.com/NVIDIA-NeMo/Speech.git@cf724ac337d1ebc7d0dda1e23fb80916f52927a5"
|
| 170 |
+
hf download iapp/OpenThai2.0-ASR-Streaming --local-dir OpenThai2.0-ASR-Streaming
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
### Transcribe files with NeMo's streaming script
|
| 174 |
+
|
| 175 |
+
This is exactly how every benchmark number on this page was produced. Audio must be 16 kHz mono.
|
| 176 |
+
|
| 177 |
+
```bash
|
| 178 |
+
echo '{"audio_filepath": "a.wav", "duration": 8.8, "text": ""}' > in.jsonl # one line per file
|
| 179 |
+
python <NeMo Speech>/examples/asr/asr_cache_aware_streaming/speech_to_text_cache_aware_streaming_infer.py \
|
| 180 |
+
model_path=OpenThai2.0-ASR-Streaming/openthai2.0-asr-streaming_bias-1.0.nemo \
|
| 181 |
+
dataset_manifest=in.jsonl output_path=out batch_size=32 \
|
| 182 |
+
att_context_size="[56,6]" target_lang=th-TH strip_lang_tags=true \
|
| 183 |
+
rnnt_decoding.greedy.max_symbols_per_step=2
|
| 184 |
+
# result: out/streaming_out_*.json, one line per file, field "pred_text"
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
- `att_context_size`: `[56,0]` 80 ms, `[56,3]` 320 ms, `[56,6]` 560 ms, `[56,13]` 1120 ms.
|
| 188 |
+
- `target_lang`: `th-TH`, or `th-mix` to write English words in Latin letters.
|
| 189 |
+
- `max_symbols_per_step=2` stops rare repetition loops at no cost in accuracy.
|
| 190 |
+
- **The script decodes each file as one stream.** Cut long recordings at pauses first, or use the reference server below, which does it for you. In one stream, a pause of near-silence makes the model drop some of the speech that follows (see [Limits](#limits)).
|
| 191 |
+
- Use `dataset_manifest=`; with `audio_file=` this NeMo commit prints the text and then stops with an error.
|
| 192 |
+
- Do not use NeMo's `model.transcribe()` for this model: at this commit its data loader ignores `target_lang`, so it stops with `Unknown prompt key: 'None'`, and its default prompt mode would use the language-agnostic prompt for about half of the clips.
|
| 193 |
+
|
| 194 |
+
### Reference server: files and live streams
|
| 195 |
+
|
| 196 |
+
[`serve/`](serve/) is a small FastAPI service around the model: `POST /v1/transcribe` for files and `WS /v1/live` for live audio, with three modes and optional speaker labels. It runs on a GPU or a CPU. Both cut long audio at pauses, the way iApp's hosted service does. A file longer than 30 seconds is cut where nobody speaks for 0.7 s and each piece is decoded on its own; the response lists them in `segments`. The exception is a file with speaker labels, which is still decoded as one stream. A live stream sends a final text each time the speaker pauses.
|
| 197 |
+
|
| 198 |
+
```bash
|
| 199 |
+
uv pip install fastapi uvicorn python-multipart websockets
|
| 200 |
+
cd OpenThai2.0-ASR-Streaming/serve
|
| 201 |
+
CHINDA_DEVICE=cuda CHINDA_PRELOAD=1 python -m uvicorn app:app --port 18081 # CPU: leave out CHINDA_DEVICE
|
| 202 |
+
curl -F file=@a.wav -F mode=standard http://127.0.0.1:18081/v1/transcribe
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
| `mode` | for | what it does |
|
| 206 |
+
|---|---|---|
|
| 207 |
+
| `standard` (default) | read speech, news, lectures, dictation | blank offset −1.0; clips under 2 s decoded again at −2.5; empty output decoded again at −4.0 |
|
| 208 |
+
| `conversation` | YouTube, social media, casual talk | blank offset −2.0; empty output decoded again at −4.0 |
|
| 209 |
+
| `meeting` | several people in a room | blank offset −1.0, and speaker labels from [`nvidia/Nemotron-3-Diarization`](https://huggingface.co/nvidia/Nemotron-3-Diarization) (downloaded on first use) |
|
| 210 |
+
|
| 211 |
+
Other fields: `lang` (`th-TH` or `th-mix`) and `latency_ms` (80, 320, 560 or 1120). Each mode's settings were chosen on development data and then measured once on test data.
|
| 212 |
+
|
| 213 |
+
Live stream over WebSocket (audio as 16 kHz mono 16-bit PCM):
|
| 214 |
+
|
| 215 |
+
```python
|
| 216 |
+
import json, soundfile as sf
|
| 217 |
+
from websockets.sync.client import connect
|
| 218 |
+
x = sf.read("a.wav", dtype="float32")[0] # 16 kHz mono
|
| 219 |
+
pcm = (x.clip(-1, 1) * 32767).astype("<i2").tobytes() # 16-bit PCM, as a microphone gives it
|
| 220 |
+
with connect("ws://127.0.0.1:18081/v1/live") as ws:
|
| 221 |
+
ws.send(json.dumps({"mode": "standard", "lang": "th-TH", "latency_ms": 560})); ws.recv()
|
| 222 |
+
for i in range(0, len(pcm), 16000): # 0.5 s chunks
|
| 223 |
+
ws.send(pcm[i:i + 16000])
|
| 224 |
+
ws.send("end")
|
| 225 |
+
while (m := json.loads(ws.recv()))["type"] != "done":
|
| 226 |
+
if m["type"] == "partial":
|
| 227 |
+
print("partial:", m["text"]) # the sentence so far
|
| 228 |
+
elif m["type"] == "final":
|
| 229 |
+
print("final:", m["display_text"]) # one per pause
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
The same code without a server (run inside `serve/`):
|
| 233 |
+
|
| 234 |
+
```python
|
| 235 |
+
from chinda_asr import ChindaASR, MODES, load_audio
|
| 236 |
+
from chinda_realtime import transcribe_long
|
| 237 |
+
asr = ChindaASR(device="cuda") # or "cpu"
|
| 238 |
+
print(asr.transcribe("a.wav", policy=MODES["conversation"], lang="th-TH")["display_text"]) # a clip
|
| 239 |
+
print(transcribe_long(asr, load_audio("talk.wav"), policy=MODES["standard"])["display_text"]) # a long recording
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
All fields, environment variables and error codes: [`serve/README.md`](serve/README.md). The service has no authentication; keep it on localhost or behind your own gateway.
|
| 243 |
+
|
| 244 |
+
**Speed.** On one NVIDIA H100, files decode about 257 times faster than real time, and one copy of the model keeps up with about 20 live streams at 560 ms. On a laptop-class CPU with 8 threads, files decode about 4 times faster than real time and live streams keep up at 560 and 320 ms, with about 6.5 GB of memory.
|
| 245 |
+
|
| 246 |
+
## What the output looks like
|
| 247 |
+
|
| 248 |
+
- Thai words are not separated by spaces, and there is almost no punctuation.
|
| 249 |
+
- Numbers usually come out as digits.
|
| 250 |
+
- Hesitation sounds such as เอ่อ and อ่า are usually left out, and so are words a speaker repeats.
|
| 251 |
+
- With `th-mix`, English words a speaker says are written in Latin letters (`online`, `GDP`); with `th-TH` they are written in Thai script.
|
| 252 |
+
|
| 253 |
+
## Training
|
| 254 |
+
|
| 255 |
+
**Data.** 12,373 hours of Thai speech and 961 hours of English.
|
| 256 |
+
|
| 257 |
+
| source | hours | transcripts |
|
| 258 |
+
|---|---:|---|
|
| 259 |
+
| [GigaSpeech2](https://huggingface.co/datasets/speechcolab/gigaspeech2), Thai training split, YouTube | 10,400 | automatic (GigaSpeech2's own) |
|
| 260 |
+
| [Thai Dialect Corpus (Porjai)](https://github.com/SLSCU/thai-dialect-corpus), Central Thai only | 695 | human |
|
| 261 |
+
| iApp's own recordings: podcasts, talk shows, live streams and voice-actor recordings | 446 | human |
|
| 262 |
+
| [Common Voice](https://commonvoice.mozilla.org/th), Thai | 408 | human |
|
| 263 |
+
| WanJuan-SiLu, Thai | 201 | human |
|
| 264 |
+
| THAI SER (VISTEC-depa) | 86 | human |
|
| 265 |
+
| [LOTUSDIS](https://arxiv.org/abs/2509.18722), training split | 64 | human |
|
| 266 |
+
| [PyThaiNLP Som TTS dataset](https://huggingface.co/datasets/pythainlp/som_tts_dataset) | 21 | human |
|
| 267 |
+
| NECTEC LOTUS, TSync-1 and TSync-2 | 17 | human |
|
| 268 |
+
| [Thai Elderly Speech](https://github.com/VISAI-DATAWOW/Thai-Elderly-Speech-dataset) | 14 | human |
|
| 269 |
+
| [Gowajee](https://github.com/ekapolc/gowajee_corpus) | 14 | human |
|
| 270 |
+
| [FLEURS](https://huggingface.co/datasets/google/fleurs), Thai training and development splits | 8 | human |
|
| 271 |
+
| [LibriSpeech](https://www.openslr.org/12), English | 961 | human |
|
| 272 |
+
|
| 273 |
+
- The `th-mix` prompt was trained on the same Thai audio, with Thai-script spellings of English words changed to Latin letters by a language model whose every edit was checked character by character against the original transcript.
|
| 274 |
+
- No clip of any test set above was used in training.
|
| 275 |
+
- It is a Central Thai model: the Thai Dialect Corpus's Korat, Kham Mueang and Pattani Malay splits were left out on purpose.
|
| 276 |
+
|
| 277 |
+
**Recipe.**
|
| 278 |
+
- NVIDIA's tokenizer was extended with Thai pieces, and the decoder and joint network were trained from scratch on the new vocabulary; the encoder and the language-prompt layer start from NVIDIA's weights.
|
| 279 |
+
- 115,000 optimizer steps of 960 seconds of audio each (about 30,700 hours of audio seen), AdamW, cosine learning-rate schedule, 32-bit precision, FastEmit λ 0.01:
|
| 280 |
+
- 79,000 steps at peak learning rate 1e-3 (5,000 warm-up steps);
|
| 281 |
+
- 4,000 steps at 1e-4 on a mix weighted towards conversational speech;
|
| 282 |
+
- 20,000 steps at 1e-4 that add the `th-mix` prompt;
|
| 283 |
+
- three further 4,000-step passes at 1e-4 that refine conversational speech and the English word list.
|
| 284 |
+
- The blank offset −1.0 and every mode's setting were chosen on development sets, never on the test sets reported above.
|
| 285 |
+
|
| 286 |
+
## Limits
|
| 287 |
+
|
| 288 |
+
- **Long recordings decoded as one stream.** After a pause of near-silence the model drops some of the speech that follows, and the longer and quieter the pause, the more it drops. We rebuilt TVSpeech's videos by joining their clips, and decoded each file as one stream at 560 ms. With no gap between clips, 13.2% of characters were wrong. Gaps of 0.3, 0.5 and 1 s of silence raised that to 16.3%, 19.2% and 23.5%; 1 s gaps of room-level noise (−50 dB) gave 15.3%. Typhoon gives 11.1% on the same files. Cut recordings at pauses before decoding: the included server and iApp's hosted service do this, and the same files then score 12.66% (Typhoon 10.84%).
|
| 289 |
+
- **Conversational TV and YouTube talk.** On TVSpeech, Typhoon's own benchmark, Typhoon ASR Streaming 0.6B makes fewer errors than this model (10.84% against 12.41% at 560 ms). Hesitation sounds such as เอ่อ explain part of the gap: TVSpeech's transcripts keep them and this model leaves them out, and removing them from both sides shrinks the gap from 1.57 to 1.15 points. The rest is almost all in clips with English words: on the 353 clips without any, the gap is then 0.19 points.
|
| 290 |
+
- **English words.** With `th-TH`, English words come out in Thai script. With `th-mix`, only some come out in Latin letters: 13.7% of the English words in TVSpeech's transcripts at 560 ms, against 16.1% for Typhoon.
|
| 291 |
+
- **Dialects.** It is a Central Thai model. On the Isan test set it gets 34.90% of characters wrong, about the same as Typhoon. We did not train on Isan, and Typhoon's published training data lists none.
|
| 292 |
+
- **Far microphones.** Accuracy falls sharply with distance. On LOTUSDIS meetings at 1120 ms, the conference microphone in the middle of the table gives 20.30% errors, and the two far channels (bt10m and bt3m) 53.49% and 56.50%. Typhoon is worse on all of them (28.23%, 69.81% and 74.41%). Use a microphone near the speakers, or each person's own track.
|
| 293 |
+
- **Very short clips.** Clips under about one second sometimes come out empty. The server's `standard` mode decodes clips under 2 s a second time with a lower blank offset, which fixes most of them.
|
| 294 |
+
- **Telephone audio.** It was not trained on 8 kHz phone audio and we have not measured it there.
|
| 295 |
+
- **Names and rare words.** There is no custom vocabulary in this release; names are spelled from how they sound.
|
| 296 |
+
- **Overlapping speech.** The model writes one stream of text. When two people talk at once, it usually writes only the louder one. Speaker labels in `serve/` come from a separate diarization model and can be wrong on short turns.
|
| 297 |
+
|
| 298 |
+
## Licence and attribution
|
| 299 |
+
|
| 300 |
+
Released under the **OpenMDW License Agreement, version 1.1** ([`LICENSE`](LICENSE)), the licence of its base model. OpenThai2.0-ASR-Streaming is a Thai fine-tune of NVIDIA Nemotron 3.5 ASR Streaming 0.6B. NVIDIA's original model card and its bias, explainability, privacy and safety sub-cards are in [`nvidia/`](nvidia/), unchanged.
|
| 301 |
+
|
| 302 |
+
- "Nemotron" is a trademark of NVIDIA Corporation, used here only to name the base model. This release is not affiliated with or endorsed by NVIDIA.
|
| 303 |
+
- `serve/chinda_asr.py` adapts the decode loop of NVIDIA NeMo's `speech_to_text_cache_aware_streaming_infer.py` (Apache-2.0).
|
| 304 |
+
- Speaker labels in `serve/` use `nvidia/Nemotron-3-Diarization` (OpenMDW-1.1), which is downloaded from Hugging Face, not redistributed here.
|
| 305 |
+
|
| 306 |
+
Built by iApp Technology. Trained and evaluated on NVIDIA H100 GPUs provided by Siam AI Corporation.
|
| 307 |
+
|
| 308 |
+
## Citation
|
| 309 |
+
|
| 310 |
+
```bibtex
|
| 311 |
+
@misc{iapp2026openthaiasrstreaming,
|
| 312 |
+
title = {OpenThai2.0-ASR-Streaming: streaming Thai speech recognition},
|
| 313 |
+
author = {{iApp Technology}},
|
| 314 |
+
year = {2026},
|
| 315 |
+
url = {https://huggingface.co/iapp/OpenThai2.0-ASR-Streaming}
|
| 316 |
+
}
|
| 317 |
+
```
|
benchmark/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Benchmark outputs and scorer
|
| 2 |
+
|
| 3 |
+
Every number on the model card can be checked from this folder. We decoded every system ourselves, and here are the per-clip outputs and the scorer we used.
|
| 4 |
+
|
| 5 |
+
## What is here
|
| 6 |
+
|
| 7 |
+
| path | contents |
|
| 8 |
+
|---|---|
|
| 9 |
+
| `hypotheses/<system>_<ms>ms_<set>.jsonl` | one line per clip: `{"id", "pred_text"}`, the system's output |
|
| 10 |
+
| `hypotheses/<system>_560ms_<set>_split.jsonl` | the joined long recordings (`tv_long`, `gs2_long`) decoded in pieces cut at pauses, as `serve/` does (`chinda_realtime.transcribe_long`), both models through the same code |
|
| 11 |
+
| `ids/<set>.jsonl` | one line per clip: `{"id", "cluster"}`, the clips of each test set and the group the bootstrap resamples |
|
| 12 |
+
| `make_refs.py` | rebuilds the reference transcripts from the public datasets, keyed by the same ids |
|
| 13 |
+
| `score.py` | character error rate, and the paired difference of two systems with a 95% bootstrap interval |
|
| 14 |
+
| `chinda_norm.py`, `num_norm.py` | the normalisation `score.py` applies to both sides |
|
| 15 |
+
|
| 16 |
+
Systems: `openthai2.0-asr-streaming` (this model, `th-TH` prompt), `openthai2.0-asr-streaming_th-mix` (`th-mix` prompt), `typhoon-asr-streaming-0.6b` (Typhoon ASR Streaming 0.6B as released) and `typhoon-asr-streaming-0.6b_4gram` (the same with its published 4-gram language model, weight 0.5).
|
| 17 |
+
|
| 18 |
+
We do not redistribute the reference transcripts, because the datasets carry their own terms (GigaSpeech2 is gated). `make_refs.py` downloads only the transcript files and writes them in the format `score.py` reads.
|
| 19 |
+
|
| 20 |
+
## Test sets and ids
|
| 21 |
+
|
| 22 |
+
| set | source | id | cluster |
|
| 23 |
+
|---|---|---|---|
|
| 24 |
+
| `fleurs` | `google/fleurs`, `th_th` test | audio file name | the clip |
|
| 25 |
+
| `tvspeech` | `typhoon-ai/TVSpeech`, test | row index | YouTube video |
|
| 26 |
+
| `gs2` | `speechcolab/gigaspeech2`, Thai test | segment id (`0-0`) | recording |
|
| 27 |
+
| `isan` | `typhoon-ai/thai-dialect-isan-dataset`, test | dataset id | speaker |
|
| 28 |
+
| `lotus_con123`, `lotus_lav123`, `lotus_jbl`, `lotus_bt3m`, `lotus_bt10m` | `nectec/LOTUSDIS`, test: the same 5,516 turns on five devices (conference microphone, lapel microphones, speakerphone and the two far channels) | chunk file name | meeting |
|
| 29 |
+
| `tv_long` | TVSpeech, each video's clips joined in row order with 1 s of silence | video id | the file |
|
| 30 |
+
| `gs2_long` | GigaSpeech2 test, each recording's segments joined in segment order with 1 s of silence; recording 26 left out because its transcripts do not match its audio | recording id | the file |
|
| 31 |
+
|
| 32 |
+
## Score
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
pip install huggingface_hub pyarrow pythainlp==5.3.5 editdistance
|
| 36 |
+
python make_refs.py --out refs/ # GigaSpeech2: accept its gate on Hugging Face first
|
| 37 |
+
python score.py refs/tvspeech.jsonl hypotheses/openthai2.0-asr-streaming_560ms_tvspeech.jsonl \
|
| 38 |
+
hypotheses/typhoon-asr-streaming-0.6b_560ms_tvspeech.jsonl
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
With one hypothesis file, `score.py` prints its character error rate. With two, it also prints the difference A − B with a 95% interval from 1,000 bootstrap resamples of whole clusters (seed 1). An interval that includes zero is a tie.
|
| 42 |
+
|
| 43 |
+
`make_refs.py` cleans the FLEURS and GigaSpeech2 transcripts the way our benchmark references were cleaned when we built them: ASCII punctuation except `?` and `!` removed (decimal points too), curly quotes, dashes and `…` removed, lower case, and PyThaiNLP's `normalize`. The other sets are used as published.
|
| 44 |
+
|
| 45 |
+
Normalisation in `score.py`, the same for reference and output: Thai digits become Arabic digits, curly quotes become straight ones, `ำ` is decomposed, spoken Thai numbers become digits (on whole words found by PyThaiNLP's `newmm`), and then every space is removed. The error rate is the edit distance over all characters of a set, divided by the number of reference characters.
|
| 46 |
+
|
| 47 |
+
To score the joined long recordings with your own system, build the audio the same way: the clips of one video or recording, in order, with exactly one second of digital silence between them, 16 kHz mono.
|
benchmark/chinda_norm.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""ChindaASR v1 frozen text normalizer (2026-09-01).
|
| 3 |
+
|
| 4 |
+
Apply IDENTICALLY to training targets and every CER reference at scoring time.
|
| 5 |
+
Verified against the stock nemotron-3.5-asr-streaming-0.6b SentencePiece tokenizer
|
| 6 |
+
(data/tokenizer/tokenizer.model, vocab 13087, unk_id 0, nmt_nfkc):
|
| 7 |
+
- Arabic digits 0-9 encode WITHOUT <unk> -> Thai digits are mapped, not verbalized.
|
| 8 |
+
- nmt_nfkc decomposes U+0E33 (sara am) -> U+0E4D U+0E32 (nikhahit + sara aa),
|
| 9 |
+
matching unicodedata.normalize('NFKC', 'ำ'). We pre-decompose so text round-trips.
|
| 10 |
+
- Corpus chars that encode to <unk> (from full-corpus scan, see DATA_REPORT.md):
|
| 11 |
+
Thai digits U+0E50-0E59 (mapped to 0-9); ฯ ฦ ๎ ( ) : % ` / Gujarati ન ા and
|
| 12 |
+
invalid Thai codepoints U+0E00,0E3B,0E3C,0E3E,0E61,0E69,0E6A (all dropped).
|
| 13 |
+
"""
|
| 14 |
+
import re
|
| 15 |
+
import unicodedata
|
| 16 |
+
|
| 17 |
+
THAI_DIGITS = {ord('๐') + i: str(i) for i in range(10)} # U+0E50..U+0E59 -> 0..9
|
| 18 |
+
|
| 19 |
+
CHAR_MAP = {
|
| 20 |
+
'‘': "'", '’': "'", # curly single quotes -> ASCII
|
| 21 |
+
'“': '"', '”': '"', # curly double quotes -> ASCII
|
| 22 |
+
'–': '-', '—': '-', '―': '-', # en/em/horizontal dash -> hyphen
|
| 23 |
+
'฿': 'บาท', # ฿ -> baht (spoken form)
|
| 24 |
+
'ำ': 'ํา', # ำ -> ํ + า (matches tokenizer nmt_nfkc)
|
| 25 |
+
'เเ': 'แ', # เเ -> แ (handled via str.replace below)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
# Corpus characters that encode to <unk> under the stock tokenizer: DROP.
|
| 29 |
+
# (Decided per char; all are non-lexical or garbage. Counts logged by build script.)
|
| 30 |
+
UNK_DROP = set(
|
| 31 |
+
'ฯฦ๎' # paiyannoi, lu, yamakkan
|
| 32 |
+
'():%`/' # ASCII punctuation absent from the vocab
|
| 33 |
+
'' # invalid Thai codepoints
|
| 34 |
+
'ના' # stray Gujarati
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
ZERO_WIDTH = set('')
|
| 38 |
+
|
| 39 |
+
_WS = re.compile(r'\s+')
|
| 40 |
+
|
| 41 |
+
def _is_thai(ch: str) -> bool:
|
| 42 |
+
return 'ก' <= ch <= '๛'
|
| 43 |
+
|
| 44 |
+
def normalize(text: str, lang: str = 'th') -> str:
|
| 45 |
+
"""Frozen ChindaASR v1 normalizer. lang='en' skips Thai-specific rules."""
|
| 46 |
+
# 1. NFC + strip zero-width
|
| 47 |
+
t = unicodedata.normalize('NFC', text)
|
| 48 |
+
t = ''.join(c for c in t if c not in ZERO_WIDTH)
|
| 49 |
+
# 2. curly quotes/dashes -> ASCII (both langs)
|
| 50 |
+
for k in ('‘', '’', '“', '”', '–', '—', '―'):
|
| 51 |
+
t = t.replace(k, CHAR_MAP[k])
|
| 52 |
+
if lang.startswith('th'):
|
| 53 |
+
# 3. เเ -> แ
|
| 54 |
+
t = t.replace('เเ', 'แ')
|
| 55 |
+
# 4. Thai digits -> Arabic
|
| 56 |
+
t = t.translate(THAI_DIGITS)
|
| 57 |
+
# 5. ฿ -> บาท
|
| 58 |
+
t = t.replace('฿', 'บาท')
|
| 59 |
+
# 6. decompose sara am (ำ -> ํา) to match tokenizer nmt_nfkc
|
| 60 |
+
t = t.replace('ำ', 'ํา')
|
| 61 |
+
# 7. drop chars that are <unk> under the stock tokenizer
|
| 62 |
+
t = ''.join(c for c in t if c not in UNK_DROP)
|
| 63 |
+
# 8. lowercase Latin
|
| 64 |
+
t = t.lower()
|
| 65 |
+
# 9. collapse whitespace
|
| 66 |
+
t = _WS.sub(' ', t).strip()
|
| 67 |
+
if lang.startswith('th'):
|
| 68 |
+
# 10. delete spaces flanked by Thai chars on BOTH sides; keep Latin/digit spacing
|
| 69 |
+
out = []
|
| 70 |
+
n = len(t)
|
| 71 |
+
for i, c in enumerate(t):
|
| 72 |
+
if c == ' ' and 0 < i < n - 1 and _is_thai(t[i - 1]) and _is_thai(t[i + 1]):
|
| 73 |
+
continue
|
| 74 |
+
out.append(c)
|
| 75 |
+
t = ''.join(out)
|
| 76 |
+
t = _WS.sub(' ', t).strip()
|
| 77 |
+
return t
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class RoundTripChecker:
|
| 81 |
+
"""Assert normalized text encodes with ZERO <unk> and decodes back to itself."""
|
| 82 |
+
def __init__(self, model_file: str):
|
| 83 |
+
import sentencepiece as spm
|
| 84 |
+
self.sp = spm.SentencePieceProcessor(model_file=model_file)
|
| 85 |
+
self.unk = self.sp.unk_id()
|
| 86 |
+
|
| 87 |
+
def check(self, normalized_text: str):
|
| 88 |
+
ids = self.sp.encode(normalized_text)
|
| 89 |
+
has_unk = self.unk in ids
|
| 90 |
+
decoded = self.sp.decode(ids)
|
| 91 |
+
return (not has_unk) and decoded == normalized_text, ids
|
| 92 |
+
|
| 93 |
+
def assert_ok(self, normalized_text: str):
|
| 94 |
+
ok, ids = self.check(normalized_text)
|
| 95 |
+
if not ok:
|
| 96 |
+
bad = [self.sp.id_to_piece(i) for i in ids]
|
| 97 |
+
raise AssertionError(f'round-trip failed for {normalized_text!r}: {bad}')
|
| 98 |
+
return ids
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
if __name__ == '__main__':
|
| 102 |
+
import sys
|
| 103 |
+
tests = [
|
| 104 |
+
('น้ำเเข็ง ๕๐ ฿', 'th'),
|
| 105 |
+
('สวัสดี ครับ hello World 123', 'th'),
|
| 106 |
+
('ฯลฯ ทดสอบ', 'th'),
|
| 107 |
+
("“Quoted” — dashed", 'en'),
|
| 108 |
+
]
|
| 109 |
+
for s, lg in tests:
|
| 110 |
+
print(repr(s), '->', repr(normalize(s, lg)))
|
| 111 |
+
if len(sys.argv) > 1:
|
| 112 |
+
rt = RoundTripChecker(sys.argv[1])
|
| 113 |
+
for s, lg in tests:
|
| 114 |
+
print(repr(normalize(s, lg)), rt.check(normalize(s, lg))[0])
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_fleurs.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_gs2.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_gs2_long.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_isan.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_bt10m.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_bt3m.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_con123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_jbl.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_lotus_lav123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_tv_long.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_1120ms_tvspeech.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_320ms_fleurs.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_320ms_gs2.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_320ms_isan.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_320ms_lotus_con123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_320ms_tvspeech.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_fleurs.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_gs2.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_gs2_long.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_gs2_long_split.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_isan.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_lotus_con123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_tv_long.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_tv_long_split.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_560ms_tvspeech.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_80ms_fleurs.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_80ms_gs2.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_80ms_isan.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_80ms_lotus_con123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_80ms_tvspeech.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_th-mix_1120ms_lotus_con123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_th-mix_1120ms_tvspeech.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_th-mix_560ms_lotus_con123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/openthai2.0-asr-streaming_th-mix_560ms_tvspeech.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_fleurs.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_gs2.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_gs2_long.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_isan.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_bt10m.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_bt3m.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_con123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_jbl.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_lotus_lav123.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_tv_long.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_1120ms_tvspeech.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_320ms_fleurs.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
benchmark/hypotheses/typhoon-asr-streaming-0.6b_320ms_gs2.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|