ghana-speech-id
Language identification for 41 Ghanaian and West African languages, over speech transcripts.
It sits on top of Omnilingual ASR: that model turns audio into text, and this one says which language the text is in.
audio ──[sherpa-onnx + omniASR CTC]──▶ transcript ──[this]──▶ language
One head, in 300m/, built on the omniASR CTC 300M front end.
GhanaSpeechId.load() finds it with no argument — there is nothing to choose.
| in-domain | out-of-domain | size |
|---|---|---|
| 95.30% | 77.6% | 8.2 MB |
How much audio to give it
Five seconds minimum, ten for the best result. This matters more than the choice of
variant, the feature count, or anything else you can tune. Measured on
ghana-speech-eval
without its bible_* configs — the out-of-domain set described under Evaluation below — on
real audio of each length, not on truncated transcripts, which flatter short input by about
six points because the recogniser still had the whole clip:
| audio | accuracy | clips scored | mean characters | empty transcripts |
|---|---|---|---|---|
| 3 s | 0.506 | 57,840 | 19.7 | 4.6% |
| 5 s | 0.657 | 32,816 | 33.4 | 0.7% |
| 7 s | 0.759 | 20,439 | 47.7 | 0.1% |
| whole clips (mean 9.7 s) | 0.777 | 13,963 | 80.0 | 0.9% |
Five seconds is a floor, not a target — it buys 0.66, well short of what the model can do. The curve then flattens: seven seconds is within two points of whole clips averaging 9.7 s, so ten seconds is where the returns run out.
Short audio does not simply produce less text, it produces worse text. Three seconds yields 19.7 characters on average and 4.6% of clips come back empty, against 80 characters and 0.9% for whole clips — the recogniser needs context and starves without it.
Check that most of it is speech. A recording that is half silence carries half the evidence its duration suggests. The reference service runs silero VAD and rejects anything below 80% speech before transcribing.
What is in this repository
| path | what it is | size |
|---|---|---|
300m/ |
the language-ID head — the model this card describes | 8.2 MB |
asr/ |
the Omnilingual ASR recogniser that produces the transcripts it reads | 279 MB |
The head classifies text and cannot read audio, so asr/ is mirrored here to keep both in
one place. That model is Meta's, not ours, released under Apache 2.0 — asr/LICENSE and
asr/ATTRIBUTION.md travel with it.
Usage
import soundfile as sf
import sherpa_onnx
from ghana_speech_id import GhanaSpeechId
model, tokens = GhanaSpeechId.download_recogniser() # asr/, 279 MB, once
rec = sherpa_onnx.OfflineRecognizer.from_omnilingual_asr_ctc(model=model, tokens=tokens)
lid = GhanaSpeechId.load() # 300m/, 8.2 MB
wav, sr = sf.read("clip.wav", dtype="float32")
s = rec.create_stream()
s.accept_waveform(sr, wav)
rec.decode_stream(s)
print(lid.classify(s.result.text))
load() pulls only the head; the recogniser downloads when you ask for it and not before.
On device there is no Python: sherpa-onnx produces the transcript and the head runs in onnxruntime through a C API, with Kotlin and Swift bindings. See the repository.
Evaluation
In-domain holds out the last 15% of each language by id. The audio is scripture read in order, so a contiguous tail approximates holding out whole books; a random split scores about two points higher, and that gap is passage-local memorisation.
Out-of-domain is
ghana-speech-eval
without its bible_* configs, since those are the training domain. Five unrelated domains,
13,963 scored clips.
| finance | jw | lds | unicef | waxal |
|---|---|---|---|---|
| 42% | 76% | 81% | 92% | 89% |
How it is built
A linear classifier over character n-grams of the transcript, with tf-idf folded into the ONNX graph. Trained on 40-character windows (about 3.3 s) rather than whole transcripts, which is worth +1.1 points out of domain and +4.7 at one second of speech. Inference classifies the whole transcript in one pass.
Limitations
Closed set. The head always names one of its 41 classes. Ga, Ahanta and Ikposo are not among them and return their nearest relative — Ga as Dangme, Ahanta as Nzema. The top-1 minus top-2 margin gives a rejection signal, but a weak one: at 80% of in-set answers retained it rejects about half of out-of-set speech.
Fante collapses into Twi out of domain, 0.12–0.52 across three configs, despite 0.98 F1 on clean text. Recognition noise erases an Akan boundary the head can otherwise learn.
No English class. The available Ghanaian English corpus is low-passed — 93% of its energy below 1 kHz — and a real ASR returns nothing for 82% of it.
Domain still matters. 95% in-domain against 78% out of domain, and finance recordings are the weakest at 42%.
Licence
Code Apache-2.0. Model weights and data CC BY-NC 4.0, following the source corpora.
- Downloads last month
- 13