ALTASR
Speech recognition for Kinyarwanda — with English, French and Swahili code-switching — by Yali Labs (ALTA Project).
ALTASR transcribes Kinyarwanda speech as it is actually spoken in Rwanda: courtrooms, clinics, government offices, markets, radio — including the English and French words and phrases speakers mix in mid-sentence. It runs offline on your own hardware, from a Raspberry-class CPU to a multi-node GPU cluster, and ships both an offline (highest accuracy) and a streaming (live captioning) model family behind one API.
pip install altasr
Extras: altasr[mic] (microphone streaming), altasr[onnx] (ONNX
export/runtime), altasr[bpe] (subword tokenizer support).
Quickstart
from altasr import ASR
asr = ASR.from_pretrained("path/to/checkpoint") # a checkpoint folder
print(asr.transcribe("recording.wav").text)
Something wrong? Run the doctor first
altasr doctor checks your Python/PyTorch/CUDA/audio stack and tells you
exactly what is broken and how to fix it; --fix applies the safe fixes
(the right PyTorch wheel for your GPU, missing audio backends) after
showing you the plan:
altasr doctor # diagnose: environment, GPU, audio backends
altasr doctor --fix # repair a broken PyTorch/CUDA install
It is the first thing to reach for when anything misbehaves — wrong device, cannot decode mp3, NCCL errors, slow inference.
Transcribing
One call handles every input kind:
from altasr import ASR
asr = ASR.from_pretrained("path/to/checkpoint")
out = asr.transcribe("recording.wav") # a file (any format)
print(out.text)
outs = asr.transcribe(["a.wav", "b.wav"]) # a batch of files
import numpy as np # a numpy array
pcm = np.zeros(16000, dtype=np.float32) # 1 s of audio @16 kHz
out = asr.transcribe(pcm, sample_rate=16000)
with open("recording.wav", "rb") as fh: # raw bytes / file obj
out = asr.transcribe(fh.read())
out = asr.transcribe("https://example.com/audio.mp3") # a URL
Whole directories go through the CLI:
altasr transcribe ./recordings --recursive --output transcripts/
Sample rates, channel counts and container formats are handled for you (mp3/m4a/ogg/flac/wav; resampling is automatic).
Long audio (hours)
Give transcribe() a one-hour hearing; internally it voice-activity
segments the audio, decodes overlapping chunks, and merges them with a
local-agreement algorithm so words at chunk boundaries are not duplicated
or lost. Memory stays flat (bounded by the chunk size, not the file
length) — about 2 GB RSS for CPU decoding of arbitrarily long files.
out = asr.transcribe("recording.wav", speakers="off")
print(out.duration, "seconds transcribed")
Live captioning (streaming)
Streaming checkpoints caption as the audio arrives — partials update live, finals are stable:
from altasr import ASR
asr = ASR.from_pretrained("path/to/streaming-checkpoint")
with asr.stream() as session:
for chunk in audio_chunks: # bytes or numpy, as they arrive
for hyp in session.push(chunk, sample_rate=16000):
print("final" if hyp.is_final else "partial", hyp.text)
print("done:", session.finish())
partial lines may still change; final lines never do — render
partials in grey and replace them, append finals. Session state is
serializable, so a dropped connection can resume where it stopped.
Offline checkpoints refuse stream() with a clear message (they still
handle long files — see above); microphone capture is
altasr stream --mic (with pip install altasr[mic]).
Speaker-attributed dialogue transcription
ALTASR does not just transcribe what was said — it can tell you who said it. Speaker detection is automatic; labels only appear when more than one speaker is actually present:
out = asr.transcribe("recording.wav") # auto-detect
print(out.text) # "SPEAKER 1: ..." style
out = asr.transcribe("recording.wav", speakers=3) # force a known count
print(out.as_dialogue("srt")) # txt | json | srt | vtt | court
Court example — a hearing with a judge, a prosecutor and a witness:
asr.transcribe("hearing.wav", speakers=3).as_dialogue("court") produces
a numbered, time-stamped record with a speaker column, ready for review.
Clinic example — a consultation: asr.transcribe("consultation.wav", speakers=2) separates clinician and patient turns so the note-taker
only corrects, never untangles.
Name the voices once and ALTASR labels them by name in every later recording:
altasr enroll add --name "Judge Mukamana" --audio judge_sample.wav
altasr enroll list
altasr enroll delete --name "Judge Mukamana"
High-stakes use — read this. ALTASR output is a draft for human review. It is not a certified record. Speech recognition makes mistakes — names, numbers, negations — and speaker attribution can be wrong, especially with overlapping speech. In legal, medical, and other high-stakes settings, a qualified human must review and certify every transcript before it is relied on. The
courtoutput format prints this disclaimer on every document it renders.
Privacy — voice profiles are biometric data.
altasr enrollstores voice embeddings (not audio) encrypted at rest with a passphrase you control (ALTASR_PROFILE_PASSPHRASE). They identify a person and are subject to biometric-data law in many jurisdictions: collect consent, set a retention period, and delete profiles withaltasr enroll deletewhen they are no longer needed. To run with no persistence at all, simply never enroll anyone — automatic diarization ("SPEAKER 1/2/…") keeps nothing between calls.
Multi-GPU and multi-node inference
Point a batch of files at every GPU in the machine — or several machines:
outs = asr.transcribe(files, devices="all") # every local GPU
outs = asr.transcribe(files, devices="cuda:0,2") # a subset
altasr transcribe ./folder --recursive --gpus all --output out/
Multi-node needs no scheduler: run the same command on each node with a
shared --work-dir — a file-based work queue hands out items exactly
once, survives worker crashes (stale claims are reclaimed), and resumes
where it stopped if you re-run the same command after an interruption.
Throughput scales close to linearly with GPUs for file batches, because
items are independent.
Output format
Every call returns a structured TranscriptionOutput:
out = asr.transcribe("recording.wav", speakers="off")
out.text # the transcript
out.segments # [Segment(start, end, text, speaker, confidence)]
out.words # word-level timestamps + confidences
out.speakers # SpeakerInfo(count, labels, mode, confidence)
out.language_spans # [LanguageSpan(lang, start_word, end_word)] rw/en/fr/sw
out.to_dict() # JSON-ready
out.save("transcript.json")
Options
| option | what it does |
|---|---|
speakers="auto"|N|"off" |
speaker attribution: detect, force a count, or plain text |
hotwords=[...] |
bias decoding toward domain terms (see below) |
decoder="auto"|"greedy"|"beam", beam_size= |
speed/accuracy trade-off |
lm_weight= |
weight of the built-in corpus language model in beam search |
sample_rate= |
required for raw numpy/bytes input |
devices=, work_dir= |
multi-GPU / resumable batch jobs |
formatting=False |
return raw lowercase output instead of the formatted text |
| VAD/chunking | long-file segmentation is automatic; altasr-vad exposes the segmenter standalone |
Readable output, automatically
Checkpoints ship with learned output formatting, applied without any extra work on your side: proper names are truecased ("kigali" → "Kigali", "diane" → "Diane"), sentences are capitalized and punctuated — pauses in the speech decide where sentences end — and numbers, RWF amounts, phone numbers, and dates are written the way people write them:
out = asr.transcribe("recording.wav", speakers="off")
print(out.text) # "Muraho Diane, turi i Kigali." — not
# "muraho diane turi i kigali"
Streaming works the same way: provisional partials stay raw (they may
still change), finalized lines arrive formatted. Pass
formatting=False anywhere to get the raw output — benchmarking tools
do this automatically so accuracy numbers stay comparable. The ITN rules
are also usable standalone:
from altasr.text.itn import inverse_normalize
print(inverse_normalize("yishyuye ibihumbi bitanu amafaranga"))
# -> "yishyuye RWF 5,000"
Complete parameter reference
Every parameter of the inference API. (transcribe_batch accepts the
same decoding/formatting parameters as transcribe.)
ASR.from_pretrained(checkpoint, ...)
| parameter | default | meaning |
|---|---|---|
checkpoint |
required | checkpoint folder path |
device |
"auto" |
auto | cpu | cuda | cuda:N |
quantize |
"" |
"int8" = dynamic int8 quantization (CPU only): 3–4× smaller, 1.5–3× faster |
adapter |
None |
path to a fine-tuned adapter file to load on top of the base model |
asr.transcribe(audio, ...)
| parameter | default | meaning |
|---|---|---|
audio |
required | file path, URL, numpy array, torch tensor, bytes, file object, or a list of paths |
sample_rate |
None |
required for raw numpy/tensor/bytes input |
speakers |
"auto" |
speaker attribution: "auto" (labels only when >1 detected) | int (force a count) | "off" (plain text) | a profiles store |
decoder |
"auto" |
auto (best available) | greedy (fastest) | beam |
beam_size |
0 |
beam width when beam decoding (0 = the default width, 8) |
hotwords |
None |
domain terms to bias toward: a list, comma string, or "@file" |
hotword_bonus |
3.0 |
per-token score bonus along hotword matches (raise if terms still lose, lower if they over-fire) |
lm_weight |
None |
weight of the checkpoint's built-in language model in beam search (None = default 0.3; 0 disables) |
oov_correction |
True |
confidence-gated fixing of out-of-vocabulary words against the learned lexicon |
formatting |
None |
None = apply the checkpoint's truecasing/punctuation/ITN automatically; False = raw lowercase output |
batch_size |
8 |
decode batch size for lists of inputs |
resample |
True |
auto-resample non-16 kHz input |
progress |
None |
callback receiving completion fraction 0–1 |
devices |
None |
spread a LIST of files across GPUs: "all" | "cuda:0,2" | int | "cpu" |
work_dir |
None |
shared work directory: makes batch jobs resumable and enables multi-node |
asr.stream(...) session (streaming checkpoints)
| parameter | default | meaning |
|---|---|---|
endpoint_blank_frames |
20 |
finalize an utterance after this many consecutive blank frames |
endpoint_silence_s |
0.8 |
finalize after this much trailing silence |
max_utterance_s |
30.0 |
force-finalize utterances longer than this |
session.push(chunk, sample_rate=None) |
– | feed audio (bytes/numpy); returns hypotheses with .text and .is_final |
session.finish() |
– | flush and return the last final text |
altasr transcribe (CLI) — --checkpoint DIR (or env
ALTASR_CHECKPOINT), --recursive, --speakers auto|N|off,
--format txt|json|srt|vtt|tsv|court, --output PATH,
--hotwords LIST|@file, --profiles STORE, --device, --gpus all|N|i,j,
--work-dir DIR, --nodes N, --node-rank R. Full help: any command +
--help.
Fine-tune on your own recordings
Adapt a base checkpoint to your domain (a courtroom, a clinic, a call
center) with altasr-finetune — you supply audio + transcripts in any
common metadata format (JSON/JSONL/CSV with audio_path + text
columns; see altasr-finetune --help for the accepted layouts):
altasr-finetune --checkpoint path/to/checkpoint \
--audio-root ./my_recordings --train my_transcripts.json \
--val my_dev.json --out-dir runs/my-domain
| flag | default | meaning |
|---|---|---|
--checkpoint DIR |
required | the base model to adapt |
--train META / --val META |
required / – | your transcript metadata file(s), repeatable |
--audio-root DIR |
– | folder your audio paths are relative to |
--freeze-layers N |
0 |
keep the first N encoder layers frozen (small datasets: freeze more) |
--no-extend-tokenizer |
off | don't add new characters found in your data |
--limit N |
0 |
fine-tune on only the first N utterances (quick trials) |
--epochs, --lr, --batch-size |
sensible defaults | standard knobs; small data wants few epochs and a low learning rate |
--set model.lora_rank=8 |
off | LoRA mode: produces a small adapter.pt instead of a full model — load it with from_pretrained(..., adapter=...) |
Every additional flag (there are many, all documented):
altasr-finetune --help. Rule of thumb: with under an hour of audio,
prefer LoRA adapters or --freeze-layers; with tens of hours, full
fine-tuning wins.
Domain adaptation without training
Two mechanisms, no GPUs required:
Hotwords — hand the decoder your domain terms (case names, drug names, place names) per call or from a file:
out = asr.transcribe("recording.wav", speakers="off",
hotwords=["diyabete", "insuline"], decoder="beam")
altasr transcribe ./clinic --hotwords @data/lexicons/health.txt
Adapters — a domain adapter is a small file (a few MB) that specializes a base checkpoint; load it at startup:
asr = ASR.from_pretrained("path/to/checkpoint", adapter="health.adapter")
Available checkpoints
Each release publishes a table like the one below alongside the download (per-condition WER is measured on held-out Rwandan speech; RTF = seconds of audio processed per second of compute, higher is faster):
| checkpoint | arch | size | WER clean | WER code-switched | RTF (CPU) | RTF (GPU) |
|---|---|---|---|---|---|---|
altasr-large |
offline | ~430 MB | see release notes | see release notes | ~1x | ~30x |
altasr-streaming |
streaming | ~180 MB | see release notes | see release notes | ~2x | ~50x |
A checkpoint is a folder (model.pt, tokenizer.json, config.json,
meta.json); pass the folder path to from_pretrained. meta.json
declares the architecture and capabilities, so the API auto-detects what
each checkpoint can do.
CLI reference
| command | purpose |
|---|---|
altasr doctor [--fix] [--distributed] |
diagnose/repair the environment |
altasr transcribe <path> [--recursive] [--gpus all] [--hotwords ...] |
transcribe files/folders |
altasr stream [--mic | file] |
live captioning |
altasr enroll add/list/delete |
named speaker profiles (encrypted) |
altasr-vad segments <file> |
voice-activity segmentation standalone |
altasr-eval --checkpoint <dir> ... |
measure accuracy on labelled audio |
altasr-bench |
latency/throughput benchmark |
altasr-export |
package a checkpoint for deployment (ONNX + parity verify, int8, non-Python bundle) |
altasr-serve |
REST + jobs + WebSocket captioning server with /health |
Every command prints full help with --help.
Integration
Python — everything above. CLI — everything above.
REST / WebSocket server — built in, standard library only:
altasr-serve --checkpoint path/to/checkpoint --port 8080
POST /transcribe— audio bytes in, JSON transcript out (synchronous)POST /jobs+GET /jobs/<id>— job semantics for long files: submit, pollqueued|running|done|failed, fetch the resultGET /stream— WebSocket live captioning: send binary PCM frames, receive JSON events{"type": "partial"|"final", "text", "speaker", "speaker_final"}so clients can render provisional vs committed text (and speaker labels) differentlyGET /health— liveness for load balancers and Docker healthchecks
Your own web app (FastAPI/Flask/Django) — load once at startup,
transcribe per request; ASR is thread-safe for inference:
from fastapi import FastAPI, UploadFile
from altasr import ASR
app = FastAPI()
asr = ASR.from_pretrained("path/to/checkpoint") # once, at startup
@app.post("/transcribe")
async def transcribe(file: UploadFile):
return asr.transcribe(await file.read()).to_dict()
ONNX — altasr-export path/to/checkpoint --format onnx --out model.onnx --verify 100 exports for runtimes without PyTorch and
verifies numerical parity against PyTorch (pip install altasr[onnx]).
--format bundle writes a fully self-contained folder — ONNX graph,
feature/vocab spec, tokenizer, C example — for embedding ALTASR in
non-Python systems via the ONNX Runtime C API.
Docker — a Dockerfile and docker-compose.yml (with a health
check) ship in the repository:
docker compose up # serves your ./ckpt folder on :8080
Non-Python example (curl against altasr-serve):
curl --data-binary @recording.wav http://localhost:8080/transcribe
curl http://localhost:8080/health
Performance and hardware
- CPU-only works. Quantize for 3–4× smaller and 1.5–3× faster with near-identical accuracy:
from altasr import ASR
asr = ASR.from_pretrained("path/to/checkpoint", device="cpu",
quantize="int8")
- GPU: any CUDA device with ≥4 GB memory decodes the large model; bf16 is used automatically on Ampere and newer.
- Memory: long files are chunked — RSS stays flat regardless of file
length. Batch decoding scales with
batch_size. - Real-time streaming needs roughly one modern CPU core per session; a single GPU serves tens of concurrent sessions (the session router enforces per-device caps and reports latency percentiles).
Troubleshooting
| symptom | do this |
|---|---|
| Poor accuracy | Checklist: right checkpoint for the domain? 16 kHz+ source? try decoder="beam"; add hotwords for domain terms; check out.words confidences to find where it fails. |
CUDA out of memory |
lower batch_size; decode on CPU; for very long files nothing is needed (chunking bounds memory). Run altasr doctor to confirm the GPU is healthy. |
| Slow inference | altasr doctor (is the GPU actually used?); quantize on CPU; batch files instead of looping; devices="all" for many files. |
| Cannot read mp3/m4a | altasr doctor --fix installs the audio backend. |
| Wrong/garbled language | the checkpoint is Kinyarwanda-centric; heavy non-rw speech needs the code-switching checkpoint from the releases page. |
stream() raises on my checkpoint |
offline checkpoints do not stream; use a *-streaming checkpoint (the error message says exactly this). |
FAQ
Which languages? Kinyarwanda first-class, including code-switched English and French insertions; Swahili support is expanding with the corpus.
Does audio leave my machine? Never. ALTASR runs fully offline; there is no telemetry.
Can I use it commercially? Yes — Apache-2.0.
How do I make it learn my domain's vocabulary? Hotword lists and adapters (above) — no training needed.
License and citation
Apache-2.0. © Yali Labs (ALTA Project).
@software{altasr,
title = {ALTASR: Kinyarwanda speech recognition with code-switching},
author = {{Yali Labs, ALTA Project}},
year = {2026},
url = {https://github.com/yalilabs/altasr}
}
Support: open a GitHub issue, or email the ALTA Project team.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file altasr-2.0.tar.gz.
File metadata
- Download URL: altasr-2.0.tar.gz
- Upload date:
- Size: 307.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9ee14a389e60410ed4312f40cc9b96d830acac4c20a71969ba920a542320fab
|
|
| MD5 |
0296bdf2973773abc0f0da0a91b7d19c
|
|
| BLAKE2b-256 |
2546e5da5846028a7b271d623f13cfa2edd67d82b1677352416d4260ef2d65a4
|
File details
Details for the file altasr-2.0-py3-none-any.whl.
File metadata
- Download URL: altasr-2.0-py3-none-any.whl
- Upload date:
- Size: 345.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
50e4c1319f7498f5c62bdd3bf506311eb5fabec3f251510db0321b8170f3fa7e
|
|
| MD5 |
33dd898ec27c9e77f858df066b36ff57
|
|
| BLAKE2b-256 |
002c44e3e673748b2a8a6b334bfa82c4af8da78cd23b95a32c7f422be0f43ade
|