Loading…
Loading…
Tha-ASR is a Whisper-small model fine-tuned to transcribe Khmer, English, and Khmer–English code-switched speech, where both languages are mixed in the same sentence. It writes Khmer in Khmer script and English in Latin script, with punctuation.
Compared with the previous version (v1), this release cuts the English word error rate from 11.1 % to 8.3 % and the code-switch character error rate from 14.1 % to 4.5 %, and is slightly better on Khmer.
| Architecture | Whisper small (encoder-decoder transformer, 12 + 12 layers, 242 M parameters) |
| Base model | PhanithLIM/whisper-small-aug-28-april-lightning-v1, a Khmer fine-tune of openai/whisper-small |
| Languages | Khmer (km), English (en) |
| Input | 16 kHz mono audio, up to 30 s per call (≤ 15 s recommended for Khmer) |
| Output | Text without timestamps |
| Weights | model.safetensors, float32, 967 MB |
| Code | github.com/chestharastan/Tha-ASR: training, evaluation and a local web app |
All test sets are held out: no training utterance shares its text with a test or dev sentence.
| Test set | Utterances | Metric | v1 | Tha-ASR (v2) |
|---|---|---|---|---|
FLEURS Khmer (km_kh, test) |
765 | CER | 12.74 % | 12.57 % |
| khm-asr-open (test) | 765 | CER | 12.72 % | 12.35 % |
FLEURS English (en_us, test) |
647 | WER | 11.09 % | 8.27 % |
FLEURS English (en_us, test) |
647 | CER | 5.05 % | 3.72 % |
| Khmer–English code-switch (held out) | 534 | CER | 14.08 % | 4.46 % |
How it was measured. Clean audio, beam search with 5 beams, batch size 4, max_new_tokens=440, and the language
token set to each test set's language. Khmer is scored with CER after Unicode NFC normalisation, removing
zero-width characters, and ignoring spaces and punctuation (Khmer has no reliable word spacing, so WER is not
meaningful). English is scored with WER after lower-casing and removing punctuation. Lower is better.
Read these numbers with care:
pip install torch transformers librosa
import librosa
import torch
from transformers import WhisperForConditionalGeneration, WhisperProcessor
model_id = "Thareah/Tha-ASR"
processor = WhisperProcessor.from_pretrained(model_id)
model = WhisperForConditionalGeneration.from_pretrained(model_id).eval()
audio, _ = librosa.load("speech.wav", sr=16000, mono=True)
inputs = processor(audio, sampling_rate=16000, return_tensors="pt")
with torch.inference_mode():
ids = model.generate(inputs.input_features, language="km", task="transcribe", num_beams=5, max_new_tokens=440)
print(processor.batch_decode(ids, skip_special_tokens=True)[0])
Use language="en" for English.
Whisper's built-in language detection chooses among 99 languages and can drift to Thai or Lao on Khmer speech. Comparing only the Khmer and English language tokens is more reliable:
def detect_km_en(features):
tok = processor.tokenizer
lang_ids = [tok.convert_tokens_to_ids("<|km|>"), tok.convert_tokens_to_ids("<|en|>")]
start = torch.tensor([[tok.convert_tokens_to_ids("<|startoftranscript|>")]])
with torch.inference_mode():
logits = model(input_features=features, decoder_input_ids=start).logits[0, -1]
return ("km", "en")[int(logits[lang_ids].argmax())]
language = detect_km_en(inputs.input_features)
ids = model.generate(inputs.input_features, language=language, task="transcribe", num_beams=5, max_new_tokens=440)
Cut long audio at pauses into pieces of at most 15 s, then transcribe the pieces as a batch. Khmer needs many tokens
per second of speech, and Whisper's decoder holds only 448 tokens. Splitting at pauses also avoids the repetition
loops that fixed-length chunking can cause. This example uses the Silero voice detector that comes with faster-whisper
(pip install faster-whisper):
from faster_whisper.vad import VadOptions, get_speech_timestamps
audio, _ = librosa.load("meeting.mp3", sr=16000, mono=True)
spans = get_speech_timestamps(audio, VadOptions(max_speech_duration_s=15, min_silence_duration_ms=300, speech_pad_ms=200))
pieces = [audio[s["start"]:s["end"]] for s in spans]
features = processor(pieces, sampling_rate=16000, return_tensors="pt").input_features
with torch.inference_mode():
ids = model.generate(features, language="km", task="transcribe", num_beams=5, max_new_tokens=440)
for s, text in zip(spans, processor.batch_decode(ids, skip_special_tokens=True)):
print(f"[{s['start'] / 16000:7.1f}s - {s['end'] / 16000:7.1f}s] {text.strip()}")
For many pieces, run them in batches of 8–16 to limit memory. On a GPU, load the model with
torch_dtype=torch.float16 and move it and the features to "cuda".
v1 was fine-tuned from the base model on 36.5 hours (27,966 utterances):
| Source | Language | Utterances | Hours |
|---|---|---|---|
| seanghay/km-speech-corpus | km | 14,838 | 10.4 |
| rinabuoy/khm-asr-open (train) | km | 6,712 | 8.7 |
google/fleurs en_us (train) |
en | 2,565 | 7.4 |
google/fleurs km_kh (train) |
km | 1,654 | 7.0 |
| seanghay/khmer_mpwt_speech | km | 1,850 | 1.8 |
| seanghay/khmer_grkpp_speech | km | 347 | 1.4 |
Before training, 21,615 duplicate utterances shared between these aggregated corpora were removed, along with 236
utterances whose text appears in a dev or test set. The dev set is the FLEURS km_kh and en_us dev splits
(717 utterances).
v2 (this model) continues from v1 for one more pass over a larger mix:
As in v1, rows whose text appears in any dev or test set (including the code-switch test set) were removed, as were exact repeats.
| v1 | v2 | |
|---|---|---|
| Initialised from | base model | v1 |
| Learning rate | 1e-5 | 5e-6 |
| Epochs | 1 | 1 |
| Batch | 2 × 8 gradient accumulation = 16 | same |
| Schedule | linear, 300 warm-up steps | same |
| Optimiser | AdamW 8-bit (bitsandbytes), weight decay 0.01, gradient clipping 1.0 | same |
| Precision | bf16 mixed precision, gradient checkpointing | same |
| Seed | 42 | 42 |
Targets are <|lang|><|transcribe|><|notimestamps|> text <|endoftext|>, so the model keeps its language
identification but is not trained to predict timestamps.
To make the model hold up on real microphones, the training script corrupts 70 % of clips on the fly with random combinations of: DEMAND background noise (0–25 dB SNR), other speakers in the background, simulated room reverb, speed changes (0.9–1.1×), microphone hiss, a telephone band filter, and volume changes. SpecAugment is applied on top.
The weights are released under the MIT license. The base model and OpenAI Whisper are released under Apache-2.0; check each training dataset's license before using this model commercially.
Thanks to OpenAI for Whisper, to PhanithLIM for the Khmer Whisper models this work starts from, and to the authors of the speech datasets listed above.
@misc{tha_asr_2026,
title = {Tha-ASR: Khmer and English speech recognition},
author = {Thareah},
year = {2026},
howpublished = {\url{https://huggingface.co/Thareah/Tha-ASR}}
}
PhanithLIM/whisper-small-aug-28-april-lightning-v1