Skip to main content

ghana-speech-id

Language identification for 41 Ghanaian and West African languages, as a small classifier 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

The inference library is C++ with a C API. No Python on the device.

pip install ghana-speech-id

Results

Two evaluations, and the gap between them is the point.

In-domain is held-out audio from the training corpus, split so that the last 15% of each language by id is held back — the audio is scripture read in order, so that approximates holding out whole books.

Out-of-domain is ghana-speech-eval, skipping its bible_* configs because those are the training domain. Five unrelated domains, 13,963 scored clips.

variant features in-domain out-of-domain size
300m 50k 95.30% 77.6% 8.2 MB
300m 200k 95.44% 77.7% 32.8 MB
1b 50k 95.15% 77.4% 8.2 MB
1b 200k 95.31% 77.8% 32.8 MB

Out-of-domain by domain, for the shipped 300m/50k head:

finance jw lds unicef waxal
42% 76% 81% 92% 89%

Which variant

300m unless your utterances are very short. The two are level from about three seconds of speech onward, and the 300m is a third the size and the only one sherpa-onnx can decode with at a useful rate. But the 1b is meaningfully better on very short input:

input ≈ audio 300m 1b
10 chars ~0.8 s 72.1% 74.2%
40 chars ~3.3 s 94.6% 94.6%
full 95.3% 95.2%

Speed and footprint

CPU only. There is no GPU path in the inference library and none is wanted: the head is a vocabulary lookup and one sparse gather, so a GPU would spend more time on transfers than on arithmetic. Both runtimes pin the CPU execution provider.

Single thread, measured end to end including tokenisation:

runtime per classification throughput resident load
C++ (Xeon 8558) 0.064 ms 15,700/s 36 MB
Python (Ryzen 5 4500U) 0.09–0.20 ms 5,500–10,000/s 90 MB 0.66 s cached

Latency scales mildly with transcript length — 0.091 ms at 26 characters, 0.197 ms at 72 — because the work is proportional to the number of n-grams extracted.

For context, the head is roughly four orders of magnitude cheaper than the speech recognition in front of it. On any device that can run the ASR at all, language identification is free.

Model files are 8.2 MB for the head plus 0.3 MB of vocabulary.

Using it

Python

import sherpa_onnx
from ghana_speech_id import GhanaSpeechId

rec = sherpa_onnx.OfflineRecognizer.from_omnilingual_asr_ctc(
    model="omniasr-300m/model.int8.onnx", tokens="omniasr-300m/tokens.txt")
lid = GhanaSpeechId.load()                    # variant="300m" by default

s = rec.create_stream()
s.accept_waveform(16000, wav)
rec.decode_stream(s)

print(lid.classify(s.result.text))            # Twi_twi (0.93)

C

GsidConfig cfg;
gsid_config_init(&cfg);
cfg.onnx_path   = "300m/head.onnx";
cfg.ngrams_path = "300m/ngrams.txt";
cfg.labels_path = "300m/labels.txt";
cfg.config_path = "300m/head_config.txt";

char err[512];
GsidHead *h = gsid_create(&cfg, err, sizeof err);

GsidResult r = gsid_classify(h, transcript);
if (r.index >= 0) printf("%s %.3f\n", gsid_language(h, r.index), r.confidence);
else              printf("unknown\n");

index == -1 means no n-gram matched, so there was no basis for a decision. Report it as unknown rather than naming whichever language scored least badly.

Command line

ghana-speech-id "obiara na enyi nyɛden dɛ ɔbɔbɔ no nkenyan"
ghana-speech-id --variant 1b --top 3 < transcripts.txt

Android and iOS

bindings/android has the JNI shim, a Kotlin wrapper and a CMake file. bindings/ios has a Swift wrapper and a module map, so the C API imports with no Objective-C shim and no bridging header.

Why transcripts and not phonemes

An earlier version classified IPA from a phoneme recogniser fine-tuned on this corpus. Two measurements changed the design.

Orthography beats IPA by five points. On ground-truth text the same recipe reaches 99.61% in-domain against the IPA head's 94.66% at equal model size. Spelling conventions, function words and morphology carry language identity that phonemes discard — the 176-unit inventory has no tone marks at all, and most of these languages are tonal.

The fine-tuned front end had stopped generalising. It was trained on 2,329 h of Bible audio with the encoder unfrozen from step 0, and lost the ability to read anything else. On identical JW recordings it produces 1.13 characters per second with 35% of clips empty, where the base model it was fine-tuned from produces 8.36 and none. Rebuilding on the base model took out-of-domain accuracy from 36.3% to 77.6%.

Why short training windows

The head is trained on 40-character windows with stride 20 — about 3.3 seconds of speech — rather than whole transcripts. Training on whole clips and deploying on short utterances is a mismatch: the model saw a mean of 87 characters and has to decide from 25–50.

Worth +1.1 points out of domain, and much more where it matters:

input ≈ audio whole-trained window-trained
10 chars ~0.8 s 67.4% 72.1%
20 chars ~1.6 s 86.4% 89.4%
full 95.4% 95.4%

Inference classifies the whole transcript in one pass. Voting across windows was implemented and measured: −0.6 out of domain, −0.09 in-domain, and it compresses the margins that out-of-set rejection depends on. The code remains, defaulted off.

Limitations

Closed set. The head always names one of its 41 classes. Ga, Ahanta and Ikposo are not among them and come back as their nearest relative — Ga as Dangme, Ahanta as Nzema. The top-1/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, scoring 0.12–0.52 across three configs despite 0.98 F1 on clean text. ASR noise erases an Akan boundary the head can otherwise learn.

No English class. The only Ghanaian English corpus available is low-passed — 93% of its energy below 1 kHz and 1.6% in the 2–4 kHz band where consonants live — and a real ASR returns nothing for 82% of it. English can be added from any full-band source.

Domain still matters. 95% in-domain against 78% out of domain. Finance recordings are the weakest at 42%.

Building

cmake -S . -B build -DONNXRUNTIME_ROOT=/path/to/onnxruntime
cmake --build build -j
GSID_MODEL_DIR=model ./build/gsid_selftest

Only dependency is onnxruntime. The ONNX graph uses opset-13 core operators only — no com.microsoft contrib ops — so it runs in mobile onnxruntime builds. The tf-idf arithmetic is built into the graph:

inputs   indices int64[K], counts float32[K]
         tf = 1+log(counts) → ×idf → L2 normalise → Gather(W) → ReduceSum → +b → softmax
outputs  logits float32[C], probs float32[C]

The caller supplies n-gram indices and counts. Reproducing scikit-learn's char_wb exactly is the delicate part and has two traps that fail silently rather than raising — see docs-char-tokenisation.md. Every release is checked with scripts/cpp_parity.py: sklearn, the Python package and the C++ CLI must agree on every one of 200 held-out transcripts.

Training

See scripts/, and HANDOVER.md for what is settled and what is open.

script what it does
setup_lean.sh venv and dependencies
pull_ipa.py corpus text without downloading the audio
decode_base.py transcribe with a base omniASR model via sherpa-onnx
decode_fairseq2.py same via fairseq2, for the 1B which sherpa ships int8-only
build_base_corpus.py assemble the training corpus
train_head.py train and evaluate one configuration
export_onnx.py export to ONNX, check parity against sklearn
cpp_parity.py check the C++ and Python runtimes against the trainer
ood_eval.py the out-of-domain evaluation
publish_hf.py publish both variants to the Hub

Quantisation has to follow the device, and getting it wrong is expensive:

CPU CUDA
int8 17×
fp32 13× 111×

int8 on CUDA is slower than not using the GPU at all — quantised operators have no CUDA kernels, so onnxruntime places them on CPU node by node. int8 is still right for on-device inference.

Licence

Code Apache-2.0. Models and data follow the source corpora, CC BY-NC 4.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ghana_speech_id-0.1.1.tar.gz (73.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ghana_speech_id-0.1.1-py3-none-any.whl (12.4 kB view details)

Uploaded Python 3

File details

Details for the file ghana_speech_id-0.1.1.tar.gz.

File metadata

  • Download URL: ghana_speech_id-0.1.1.tar.gz
  • Upload date:
  • Size: 73.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ghana_speech_id-0.1.1.tar.gz
Algorithm Hash digest
SHA256 02e38c30ae052512a9c6922d7c6faf390a7228fc38a4792903dea242418047ca
MD5 31ce31406d5273200bd509d6357aa1e3
BLAKE2b-256 d0b34473dc47a6ac736e7dd803fa744bfb5e62b6f0587c41601168e5d57d6f26

See more details on using hashes here.

Provenance

The following attestation bundles were made for ghana_speech_id-0.1.1.tar.gz:

Publisher: release.yml on GhanaNLP/ghana-speech-id

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ghana_speech_id-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: ghana_speech_id-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 12.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ghana_speech_id-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 777e4c16e7e379589de88e4b7f3346606f512dfd3e225e59d23b40623bb4a761
MD5 00abade160284cafacc0783b102c4580
BLAKE2b-256 1f83e8daf300d0000f144031e651d3ee2e712a83787b68b4f1e33e2e3370edea

See more details on using hashes here.

Provenance

The following attestation bundles were made for ghana_speech_id-0.1.1-py3-none-any.whl:

Publisher: release.yml on GhanaNLP/ghana-speech-id

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.0

2 files

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page