| --- |
| license: apache-2.0 |
| language: |
| - ln |
| tags: |
| - automatic-speech-recognition |
| - lingala |
| - wav2vec2-bert |
| - ctc |
| - low-resource |
| base_model: keystats/w2v-bert-2.0-lingala-main-best-3 |
| datasets: |
| - google/WaxalNLP |
| - KasuleTrevor/Lingala_100hrs |
| pipeline_tag: automatic-speech-recognition |
| --- |
| |
| # w2v-bert-2.0-lingala |
|
|
| A Lingala automatic speech recognition (ASR) model, further |
| fine-tuned from |
| [keystats/w2v-bert-2.0-lingala-main-best-3](https://huggingface.co/keystats/w2v-bert-2.0-lingala-main-best-3), |
| which was itself continued from |
| [keystats/w2v-bert-2.0-lingala-main-best](https://huggingface.co/keystats/w2v-bert-2.0-lingala-main-best), |
| which was fine-tuned from |
| [facebook/w2v-bert-2.0](https://huggingface.co/facebook/w2v-bert-2.0). |
|
|
| This checkpoint is the latest link in that chain: starting from |
| `main-best-3`'s weights, training continued for **1 additional epoch** |
| at a learning rate of **1e-5**, everything else — data, splits, |
| casing, filtering — kept the same. |
|
|
| ## Model description |
|
|
| `facebook/w2v-bert-2.0` — a large-scale, multilingual self-supervised |
| speech encoder pretrained with a BERT-style masked prediction |
| objective — is used as the backbone, with a character-level CTC |
| (Connectionist Temporal Classification) head. |
|
|
| **Text casing note:** training targets were kept in their raw, cased |
| form (same tokenizer/vocab used throughout this lineage since |
| `main-best`). |
|
|
| ## Training data |
|
|
| Same pool and split usage as `main-best` and `main-best-3` — WAXAL |
| `validation` held out, `test` untouched: |
|
|
| | Source | Role | |
| |---|---| |
| | [google/WaxalNLP](https://huggingface.co/datasets/google/WaxalNLP) (`lin_asr` config) | `train` split pooled into training, `validation` split held out untouched as the fixed evaluation benchmark | |
| | [KasuleTrevor/Lingala_100hrs](https://huggingface.co/datasets/KasuleTrevor/Lingala_100hrs) | All splits pooled into training | |
|
|
| **WAXAL's `validation` split is the only data used for evaluation, |
| and it was never included in training, across this entire lineage.** |
|
|
| ## Training procedure |
|
|
| - **Base checkpoint:** `keystats/w2v-bert-2.0-lingala-main-best-3` |
| (continued fine-tuning) |
| - **Architecture:** `Wav2Vec2BertForCTC`, `add_adapter=True` |
| - **Processor:** same `Wav2Vec2BertProcessor` / tokenizer used since |
| `main-best` (character-level, raw/cased text, `|` word delimiter, |
| `[PAD]` as CTC blank) |
| - **Sample rate:** 16 kHz mono |
| - **Epochs:** 1 additional epoch on top of `main-best-3` |
| - **Learning rate:** 1e-5, cosine schedule, 10% warmup |
| - **Effective batch size:** 32 (per-device batch size 4 × gradient |
| accumulation 8) |
| - **Precision:** fp16, gradient checkpointing enabled |
| - **Regularization:** attention/hidden/feature-projection dropout 0.05 |
| - **Data filtering:** same CTC-feasibility filtering used throughout |
| this lineage |
| - **Seed:** 42 (deterministic — same seed for Python/NumPy/PyTorch/CUDA) |
|
|
| ## Evaluation results |
|
|
| Evaluated on the WAXAL Lingala `validation` split, greedy decoding |
| vs. greedy + KenLM (`keystats/waxal-kenlm-models-best`). Adding the |
| KLM gives a consistent, meaningful WER/CER improvement over greedy |
| decoding alone — pair the two for the best results. |
|
|
| ## How to use |
|
|
| ### Option 1 — model alone (greedy decoding) |
|
|
| ```python |
| import torch |
| import librosa |
| from transformers import Wav2Vec2BertForCTC, Wav2Vec2BertProcessor |
| |
| MODEL_ID = "keystats/w2v-bert-2.0-lingala" |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" |
| |
| processor = Wav2Vec2BertProcessor.from_pretrained(MODEL_ID) |
| model = Wav2Vec2BertForCTC.from_pretrained(MODEL_ID).to(DEVICE).eval() |
| |
| audio_array, sr = librosa.load("path/to/audio.wav", sr=16000, mono=True) |
| inputs = processor(audio_array, sampling_rate=16000, return_tensors="pt") |
| with torch.no_grad(): |
| logits = model(input_features=inputs.input_features.to(DEVICE)).logits |
| |
| predicted_ids = torch.argmax(logits, dim=-1) |
| transcription = processor.batch_decode(predicted_ids)[0] |
| |
| print(transcription) # cased, punctuated Lingala text |
| ``` |
|
|
| ### Option 2 — model + KLM (recommended, higher accuracy) |
|
|
| ```python |
| # pip install pyctcdecode |
| # pip install https://github.com/kpu/kenlm/archive/master.zip |
| |
| import torch |
| import librosa |
| from huggingface_hub import hf_hub_download |
| from transformers import Wav2Vec2BertForCTC, Wav2Vec2BertProcessor |
| from pyctcdecode import build_ctcdecoder |
| |
| MODEL_ID = "keystats/w2v-bert-2.0-lingala" |
| KLM_REPO_ID = "keystats/waxal-kenlm-models-best" |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" |
| |
| processor = Wav2Vec2BertProcessor.from_pretrained(MODEL_ID) |
| model = Wav2Vec2BertForCTC.from_pretrained(MODEL_ID).to(DEVICE).eval() |
| |
| klm_path = hf_hub_download(repo_id=KLM_REPO_ID, repo_type="dataset", |
| filename="lingala/lingala_5gram_correct-best.arpa") |
| |
| def build_vocab_list(tokenizer, vocab_size): |
| vocab_dict = tokenizer.get_vocab() |
| vocab_list = [None] * vocab_size |
| for tok, idx in sorted(vocab_dict.items(), key=lambda kv: kv[1]): |
| if idx < vocab_size: |
| vocab_list[idx] = tok |
| pad_id = tokenizer.pad_token_id |
| if pad_id is not None and pad_id < len(vocab_list): |
| vocab_list[pad_id] = "" |
| word_delim = getattr(tokenizer, "word_delimiter_token", None) |
| if word_delim: |
| delim_id = vocab_dict.get(word_delim) |
| if delim_id is not None: |
| vocab_list[delim_id] = " " |
| return vocab_list |
| |
| vocab_list = build_vocab_list(processor.tokenizer, model.config.vocab_size) |
| decoder = build_ctcdecoder( |
| vocab_list, |
| kenlm_model_path=klm_path, |
| alpha=0.5, |
| beta=0.7, |
| ) |
| |
| audio_array, sr = librosa.load("path/to/audio.wav", sr=16000, mono=True) |
| inputs = processor(audio_array, sampling_rate=16000, return_tensors="pt") |
| with torch.no_grad(): |
| logits = model(input_features=inputs.input_features.to(DEVICE)).logits |
| |
| transcription = decoder.decode(logits.cpu().numpy()[0], beam_width=100) |
| print(transcription) |
| ``` |
|
|
| ## Intended uses & limitations |
|
|
| - Intended for transcribing spoken Lingala audio into cased, |
| punctuated text. |
| - As a CTC-based model, it assumes single-speaker, forward-only |
| audio and has no mechanism for overlapping speech from multiple |
| speakers. |
| - This is the fourth checkpoint in a continued fine-tuning lineage |
| (`main-best` → `main-best-3` → this model). If earlier checkpoints |
| had already converged, this additional low-LR epoch may yield only |
| marginal gains — compare validation metrics against `main-best-3` |
| before choosing between them. |
|
|
|
|
| ## Citation |
|
|
| ```bibtex |
| @misc{keystats_wav2vec2bert_lingala_final, |
| title={w2v-bert-2.0-lingala: A Lingala ASR model, continued fine-tune of w2v-bert-2.0-lingala-main-best-3}, |
| author={keystats}, |
| year={2026}, |
| howpublished={\url{https://huggingface.co/keystats/w2v-bert-2.0-lingala}} |
| } |
| |
| @misc{waxal, |
| title={WAXAL: A Multilingual African Speech Dataset}, |
| author={Google}, |
| howpublished={\url{https://huggingface.co/datasets/google/WaxalNLP}} |
| } |
| |
| @misc{kasule_lingala_100hrs, |
| title={Lingala\_100hrs}, |
| author={KasuleTrevor}, |
| howpublished={\url{https://huggingface.co/datasets/KasuleTrevor/Lingala_100hrs}} |
| } |
| |
| @inproceedings{w2vbert2, |
| title={Seamless: Multilingual Expressive and Streaming Speech Translation}, |
| author={Seamless Communication and others}, |
| year={2023}, |
| howpublished={\url{https://huggingface.co/facebook/w2v-bert-2.0}} |
| } |
| ``` |