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
Download benchmark/num_norm.py from iapp/OpenThai2.0-ASR-Streaming: direct link, hf CLI and curl.
- Browser
- Download file 4.98 kB
-
https://huggingface.co/iapp/OpenThai2.0-ASR-Streaming/resolve/main/benchmark/num_norm.py
- Command line
-
hf download hf://iapp/OpenThai2.0-ASR-Streaming/benchmark/num_norm.py
-
curl -L -o num_norm.py https://huggingface.co/iapp/OpenThai2.0-ASR-Streaming/resolve/main/benchmark/num_norm.py
4.98 kB
| """Numeric equivalence for Thai ASR scoring. SCORING ONLY -- never touch training targets. | |
| WHY | |
| --- | |
| FLEURS-th references write numbers as Arabic DIGITS in 21.5% of utterances. | |
| Whisper-family models do inverse text normalisation and emit digits too | |
| (Pathumma 19.2%, distill-whisper 20.1%, faster-whisper 25.8%, Qwen3 21.5%), | |
| while NeMo RNNT models emit SPOKEN THAI (ChindaASR ~0%, typhoon-asr-realtime 0.0%). | |
| Raw scoring therefore rewards output CONVENTION, not recognition. Measured on | |
| v3-arm/FLEURS: CER 20.34 on digit-bearing refs vs 8.49 on the rest. | |
| CRITICAL IMPLEMENTATION NOTE | |
| ---------------------------- | |
| Thai has no word delimiters, so REGEX SUBSTRING MATCHING IS CATASTROPHIC here. | |
| A first version matched number words inside ordinary words -- | |
| สามารถ(able)->'3ารถ' พันธุ์(species)->'1000ธุ์' โกหก(lie)->'โก6' | |
| สัมพันธ์(relationship)->'สัม1000ธ์' | |
| and made 239/1021 utterances WORSE. Conversion MUST run on word-segmenter | |
| tokens and only on tokens that are EXACTLY numerals. pythainlp newmm keeps | |
| สามารถ / พันธุ์พืช / โกหก / สัมพันธ์ whole while splitting สี่|หมื่น|คน. | |
| """ | |
| import re | |
| from pythainlp.tokenize import word_tokenize | |
| _D = {'ศูนย์':0,'หนึ่ง':1,'เอ็ด':1,'สอง':2,'สาม':3,'สี่':4,'ห้า':5, | |
| 'หก':6,'เจ็ด':7,'แปด':8,'เก้า':9} | |
| _M = {'สิบ':10,'ร้อย':100,'พัน':1000,'หมื่น':10000,'แสน':100000,'ล้าน':1000000} | |
| # newmm emits numeral COMPOUNDS as single tokens (ยี่สิบ, หนึ่งร้อย, สิบเอ็ด...). | |
| # A token counts as numeric only if numeral morphemes cover it ENTIRELY -- that | |
| # full-match requirement is what makes สามารถ / พันธุ์ / โกหก / สัมพันธ์ safe. | |
| _MORPH = sorted(list(_D) + list(_M) + ['ยี่'], key=len, reverse=True) | |
| _FULL = re.compile('^(?:' + '|'.join(map(re.escape, _MORPH)) + ')+$') | |
| _SPLIT = re.compile('(' + '|'.join(map(re.escape, _MORPH)) + ')') | |
| _THAI_DIGIT = str.maketrans('๐๑๒๓๔๕๖๗๘๙', '0123456789') | |
| def _is_num(tok): | |
| return bool(_FULL.match(tok)) | |
| def _morphs(tok): | |
| return [m for m in _SPLIT.split(tok) if m] | |
| def _seq(tokens): | |
| """Parse a run of numeral tokens -> canonical digit string. | |
| positional สี่ หมื่น -> 40000 | |
| digit-wise สอง ศูนย์ ศูนย์ หก -> 2006 (years/IDs: no multiplier, >=2 digits) | |
| """ | |
| tokens = [m for t in tokens for m in _morphs(t)] | |
| tokens = ['สอง' if t == 'ยี่' else t for t in tokens] | |
| if not tokens: | |
| return '' | |
| if not any(t in _M for t in tokens): | |
| if len(tokens) >= 2: | |
| return ''.join(str(_D[t]) for t in tokens) | |
| return str(_D[tokens[0]]) | |
| total = 0; cur = 0; last = None | |
| for t in tokens: | |
| if t in _D: | |
| cur = _D[t]; last = 'd' | |
| else: | |
| m = _M[t] | |
| if m == 1000000: | |
| total = (total + max(cur, 1)) * m; cur = 0 | |
| else: | |
| total += (cur if cur else 1) * m; cur = 0 | |
| last = 'm' | |
| return str(total + cur) | |
| def normalize_numbers(text): | |
| text = text.translate(_THAI_DIGIT) | |
| toks = word_tokenize(text, engine='newmm') | |
| out = []; run = [] | |
| for t in toks: | |
| if _is_num(t): | |
| run.append(t) | |
| else: | |
| if run: | |
| out.append(_seq(run)); run = [] | |
| out.append(t) | |
| if run: | |
| out.append(_seq(run)) | |
| return ''.join(out) | |
| if __name__ == '__main__': | |
| CASES = [ | |
| ('สี่หมื่น','40000'), ('ยี่สิบ','20'), ('สองศูนย์ศูนย์หก','2006'), | |
| ('หนึ่งร้อยยี่สิบสาม','123'), ('สิบเอ็ด','11'), ('หนึ่งร้อย','100'), ('สองพันยี่สิบห้า','2025'), ('ห้าล้าน','5000000'), | |
| ('สามพันสองร้อย','3200'), ('เก้า','9'), ('๑๒๓','123'), ('40000','40000'), | |
| # REGRESSION GUARDS -- these broke the regex version | |
| ('สามารถตรวจจับ','สามารถตรวจจับ'), ('พันธุ์พืช','พันธุ์พืช'), | |
| ('โกหกหรือไม่','โกหกหรือไม่'), ('ความสัมพันธ์','ความสัมพันธ์'), | |
| ('ไม่มีตัวเลข','ไม่มีตัวเลข'), | |
| ] | |
| bad = 0 | |
| for src, want in CASES: | |
| got = normalize_numbers(src); ok = got == want; bad += not ok | |
| print(f"{'ok ' if ok else 'FAIL'} {src!r} -> {got!r}" + ('' if ok else f" want {want!r}")) | |
| print(f"\n{len(CASES)-bad}/{len(CASES)} pass") | |
| raise SystemExit(1 if bad else 0) | |