mohammedaly22 commited on
Commit
b506941
Β·
verified Β·
1 Parent(s): 9959871

Document the pyctcdecode numpy<2.0.0 pin workaround, and the new graceful LM-degradation behavior

Browse files
Files changed (1) hide show
  1. README.md +444 -435
README.md CHANGED
@@ -1,435 +1,444 @@
1
- ---
2
- license: mit
3
- language:
4
- - ar
5
- - en
6
- tags:
7
- - speech
8
- - asr
9
- - automatic-speech-recognition
10
- - ctc
11
- - conformer
12
- - egyptian-arabic
13
- - code-switching
14
- - arabic
15
- - audio
16
- - pytorch
17
- - kenlm
18
- - language-model
19
- - streaming
20
- library_name: metro-asr
21
- pipeline_tag: automatic-speech-recognition
22
- datasets:
23
- - AlaaSamir/custom-egy-tts
24
- - OmarAhmedSobhy/egyption-with-emotion-dataset
25
- - MightyStudent/Egyptian-ASR-MGB-3
26
- - MAdel121/arabic-egy-cleaned
27
- - MAdel121/Continuation-egy-for-ultravox-v1
28
- - Raniahossam33/Egyptian_TTS3RS
29
- - ahmedbasemdev/egyptain-tts-dataset
30
- - MohamedRashad/arabic-english-code-switching
31
- - librispeech_asr
32
- metrics:
33
- - wer
34
- - cer
35
- model-index:
36
- - name: Metro-ASR Small
37
- results:
38
- - task:
39
- type: automatic-speech-recognition
40
- name: Speech Recognition
41
- dataset:
42
- type: custom
43
- name: Egyptian Arabic + Code-Switching Test Set
44
- config: all
45
- split: test
46
- metrics:
47
- - type: wer
48
- value: 46.85
49
- name: WER (All)
50
- - type: cer
51
- value: 28.41
52
- name: CER (All)
53
- - type: wer
54
- value: 37.24
55
- name: WER (Arabic)
56
- - type: cer
57
- value: 17.45
58
- name: CER (Arabic)
59
- - type: wer
60
- value: 36.32
61
- name: WER (Code-Switching)
62
- - type: cer
63
- value: 17.44
64
- name: CER (Code-Switching)
65
- ---
66
-
67
- <h1 align="center">Metro-ASR Small</h1>
68
-
69
- <p align="center">
70
- <strong>Non-autoregressive CTC speech recognition for Egyptian Arabic and Arabic–English<br>
71
- code-switching, with a detachable n-gram language head you can retrain on text alone.</strong>
72
- </p>
73
-
74
- <p align="center">
75
- <a href="https://github.com/MohammedAly22/metro-asr"><img src="https://img.shields.io/badge/GitHub-Repository-E8232A?style=for-the-badge&logo=github" alt="GitHub"></a>
76
- <a href="https://pypi.org/project/metro-asr/"><img src="https://img.shields.io/pypi/v/metro-asr?style=for-the-badge&logo=pypi&logoColor=white&color=E8232A" alt="PyPI"></a>
77
- <a href="https://huggingface.co/spaces/mohammedaly22/metro-asr"><img src="https://img.shields.io/badge/%F0%9F%A4%97_Space-Demo-E8232A?style=for-the-badge" alt="Space"></a>
78
- <a href="https://mohammedaly22.github.io/metro-asr/"><img src="https://img.shields.io/badge/%F0%9F%94%8A_Report-Listen%20%26%20Compare-E8232A?style=for-the-badge" alt="Evaluation report"></a>
79
- </p>
80
-
81
- ## What this is
82
-
83
- Metro-ASR separates the two things a speech recogniser has to know β€” **what the audio sounds
84
- like** and **what the words are likely to be** β€” into two artefacts trained and shipped
85
- separately. This repository holds the first one: **Metro-Small**, a 61.6M-parameter Conformer
86
- acoustic model trained with CTC. It is non-autoregressive β€” one forward pass turns an utterance
87
- into a matrix of per-frame log-probabilities, with no decoder loop and no dependence on
88
- previously emitted tokens β€” which is why it runs at 40–55Γ— real time on a laptop CPU with no
89
- GPU involved anywhere in this card's examples.
90
-
91
- The second artefact, the language model, is an n-gram over text. It never sees audio, trains
92
- in minutes on a laptop, and plugs into the decoder at run time β€” including domain-specific
93
- variants you build yourself from nothing but text (see
94
- [The language head](#the-language-head) below). The general-purpose one is included in this
95
- repository as `lm_5gram.bin`.
96
-
97
- For the full architecture writeup, training-from-scratch instructions, and how to build your
98
- own domain language head, see the
99
- **[GitHub repository](https://github.com/MohammedAly22/metro-asr)**. For real transcripts,
100
- audio players, and a measured comparison of greedy vs. beam+LM decoding across three
101
- interchangeable language heads, see the
102
- **[interactive evaluation report](https://mohammedaly22.github.io/metro-asr/)**.
103
-
104
- <p align="center">
105
- <img src="images/architecture.svg" alt="Metro-ASR architecture" width="100%">
106
- </p>
107
-
108
- ---
109
-
110
- ## Architecture
111
-
112
- Twelve identical Conformer-style blocks, each a Macaron sandwich of two half-weighted
113
- feed-forward networks around an attention module and a convolution module:
114
-
115
- <p align="center">
116
- <img src="images/metro-block.svg" alt="Inside one Metro block" width="100%">
117
- </p>
118
-
119
- | Component | Details | Why |
120
- |---|---|---|
121
- | Encoder | Conformer, 12 layers, d_model=384, 6 heads | β€” |
122
- | Position encoding | RoPE (rotary), bias-free Q/K projections | attention depends on relative offset, so it doesn't break on utterances longer than any seen in training |
123
- | Feed-forward | SwiGLU, expansion 3Γ—, applied twice at half weight | a learned gate beats a plain ReLU/GELU FFN at equal parameter count |
124
- | Normalization | RMSNorm, pre-norm | cheaper than LayerNorm, keeps gradients well-behaved as depth grows |
125
- | Convolution | SE-gated depthwise separable, kernel 31 | local context (~1.24 s) to complement attention's global view |
126
- | Regularization | Stochastic depth, rate 0.05 | deeper layers dropped more often during training |
127
- | Auxiliary loss | Intermediate CTC at layer 6, weight 0.3 | mid-stack layers get gradient directly; discarded at inference |
128
- | Tokenizer | BPE (SentencePiece), vocab 5,000 | trained on a deliberately balanced Arabic/English corpus so English words survive as whole tokens |
129
- | Decoding | CTC greedy, or beam search + KenLM | see below |
130
- | Parameters | 61,586,320 (61.6M) | β€” |
131
-
132
- **Frame rate.** 16 kHz audio β†’ 80-bin log-Mel (100 fps) β†’ Conv2D Γ—4 subsampling β†’ 25 fps through
133
- the encoder and the CTC head. Subsampling by 4 before the first block cuts attention's quadratic
134
- cost 16Γ— before a single block runs; one output token covers 40 ms of audio.
135
-
136
- ---
137
-
138
- ## The language head
139
-
140
- <p align="center">
141
- <img src="images/language-head.svg" alt="The detachable language head" width="100%">
142
- </p>
143
-
144
- CTC's per-frame independence produces a specific, recognisable error pattern: doubled
145
- syllables, dropped affixes, malformed English fragments β€” the model heard correctly and wrote
146
- something that isn't a word. A word-level n-gram model fixes this during beam search, because
147
- it knows which *sequences* are plausible, without ever having heard a single second of audio:
148
-
149
- <p align="center">
150
- <img src="images/decoding.svg" alt="Greedy vs. beam search with the language head" width="100%">
151
- </p>
152
-
153
- The language head is a separate file with no learned interaction with the acoustic weights.
154
- Swap it, and nothing about `model.pt` changes:
155
-
156
- ```python
157
- engine = MetroASREngine.from_pretrained("small")
158
-
159
- engine.load_lm("lm/medical_head.bin") # swap language heads at run time,
160
- print(engine.transcribe("call.wav", beam_search=True).text) # same acoustic weights throughout
161
- ```
162
-
163
- **This is measured, not asserted.** The GitHub repo ships two extra language heads β€”
164
- technical and medical β€” built from real text (Egyptian medical chat/QA, Egyptian Arabic
165
- Wikipedia's technical articles, real Arabic-English code-switching text) plus synthesised
166
- domain-term carrier phrases, and decodes 11 real clips with all three heads against human
167
- references. Headline results from the
168
- **[full interactive report](https://mohammedaly22.github.io/metro-asr/)**:
169
-
170
- | Test set | Greedy | General head | Technical head | Medical head |
171
- |---|---:|---:|---:|---:|
172
- | Technical clips (WER) | 34.8% | 26.2% | **24.1%** | 26.2% |
173
- | Medical clips (WER) | 44.9% | **34.7%** | 38.8% | **34.7%** |
174
- | General speech (WER) | 25.6% | **24.7%** | 36.9% | 32.5% |
175
-
176
- Two things worth reading out of that table. The domain heads win in their own domain despite
177
- being 4-grams built from far less text than the general 5-gram β€” domain fit beats scale for
178
- this component. And the technical head actively *hurts* on general speech (36.9% vs. greedy's
179
- 25.6%) β€” a language head is a strong prior, and matching it to your traffic matters. (These
180
- numbers are from a small, hard 11-clip demo set β€” unscripted, overlapping speech, dense
181
- code-switching β€” and are not the same evaluation as the Performance section below; see the
182
- report for methodology.)
183
-
184
- Building your own head takes text and minutes, no GPU:
185
-
186
- ```bash
187
- python scripts/train_lm.py --corpus my_domain.txt --out lm/my_domain_4gram.arpa --order 4
188
- ```
189
-
190
- Full walkthrough β€” including a from-scratch corpus-building example for a technical and a
191
- medical domain β€” in the GitHub README's
192
- [Domain-specialised heads](https://github.com/MohammedAly22/metro-asr#domain-specialised-heads)
193
- section.
194
-
195
- ---
196
-
197
- ## Model variants
198
-
199
- <p align="center">
200
- <img src="images/scaling.svg" alt="Metro-ASR family scaling" width="100%">
201
- </p>
202
-
203
- Three sizes share one block definition and one training recipe β€” only width, depth and
204
- vocabulary change. **Small (this repository) is the only one currently trained.** Medium and
205
- Large exist as configs in the GitHub repo with exact parameter counts, but no weights β€”
206
- see [Scaling to Medium and Large](https://github.com/MohammedAly22/metro-asr#scaling-to-medium-and-large)
207
- for what training them actually requires (data volume most of all).
208
-
209
- | | Params | d_model | Layers | BPE vocab | Status |
210
- |---|---:|---:|---:|---:|---|
211
- | **Small** | 61.6M | 384 | 12 | 5,000 | **Released β€” this repo** |
212
- | Medium | 247.4M | 512 | 24 | 8,000 | Config only |
213
- | Large | 747.8M | 768 | 32 | 16,000 | Config only |
214
-
215
- ---
216
-
217
- ## Performance
218
-
219
- Held-out test-set WER/CER (the numbers in this card's metadata):
220
-
221
- | Split | WER (%) | CER (%) |
222
- |---|---:|---:|
223
- | All | 46.85 | 28.41 |
224
- | Arabic only | 37.24 | 17.45 |
225
- | Code-switching | 36.32 | 17.44 |
226
-
227
- **Speed** β€” measured, Intel Core Ultra 7 155H, 4 CPU threads, PyTorch 2.13 CPU build, fp32,
228
- minimum of 15 runs after warm-up:
229
-
230
- <p align="center">
231
- <img src="images/latency.svg" alt="Measured CPU latency" width="100%">
232
- </p>
233
-
234
- | Audio length | Latency | RTF | Faster than real time |
235
- |---|---:|---:|---:|
236
- | 1 s | 37 ms | 0.037 | 27Γ— |
237
- | 5 s | 99 ms | 0.020 | 51Γ— |
238
- | **10 s** | **181 ms** | **0.018** | **55Γ—** |
239
- | 30 s | 686 ms | 0.023 | 44Γ— |
240
-
241
- Beam search with the 5-gram head adds roughly 10–250 ms per utterance depending on length
242
- (RTF β‰ˆ 0.024 overall). Loading the 5.9 GB binary itself takes about 3.4 s, once, at startup.
243
-
244
- ---
245
-
246
- ## Usage
247
-
248
- ### Install
249
-
250
- ```bash
251
- pip install metro-asr # greedy decoding only
252
- pip install "metro-asr[lm]" # + KenLM beam search
253
- ```
254
-
255
- ### Quick start
256
-
257
- ```python
258
- from metro_asr import MetroASREngine
259
-
260
- engine = MetroASREngine.from_pretrained("small") # auto-downloads weights + tokenizer, caches locally
261
- result = engine.transcribe("audio.wav")
262
- print(result.text)
263
- ```
264
-
265
- The 5.9 GB language model is **not** downloaded by this call.
266
-
267
- ### With the language head (beam search)
268
-
269
- ```python
270
- engine = MetroASREngine.from_pretrained("small", lm_path="auto") # also fetches lm_5gram.bin
271
- result = engine.transcribe("audio.wav", beam_search=True)
272
- print(result.text)
273
- ```
274
-
275
- ### From a manual download
276
-
277
- If you've already run `snapshot_download` (or `git clone`d this repo) into a local directory,
278
- point `from_pretrained` at that directory instead β€” nothing gets re-downloaded, and it works
279
- fully offline:
280
-
281
- ```python
282
- from huggingface_hub import snapshot_download
283
- from metro_asr import MetroASREngine
284
-
285
- snapshot_download(repo_id="MohammedAly22/metro-asr-small", local_dir="checkpoints")
286
-
287
- engine = MetroASREngine.from_pretrained("checkpoints", lm_path="auto")
288
- ```
289
-
290
- > [!TIP]
291
- > This is the fix for a common mistake: calling `from_pretrained("checkpoints")` used to be
292
- > interpreted as a HuggingFace repo id named literally "checkpoints" and fail with
293
- > *Repository Not Found*. Current versions check for an existing local directory first β€” update
294
- > if you hit that error.
295
-
296
- ### Without the package β€” loading the raw PyTorch model
297
-
298
- ```python
299
- import torch
300
- from metro_asr.utils.config import load_config
301
- from metro_asr.model.metro import MetroASR
302
- from metro_asr.model.tokenizer import build_tokenizer
303
-
304
- config = load_config("checkpoints/config.yaml")
305
- tokenizer = build_tokenizer(config, "checkpoints") # must run before MetroASR.from_config β€”
306
- model = MetroASR.from_config(config) # it fixes the CTC head's vocab size
307
- ckpt = torch.load("checkpoints/model.pt", map_location="cpu", weights_only=False)
308
- model.load_state_dict(ckpt["model_state_dict"])
309
- model.eval()
310
- ```
311
-
312
- ### Batch transcription
313
-
314
- ```python
315
- results = engine.transcribe_batch(["audio1.wav", "audio2.wav", "audio3.wav"])
316
- for r in results:
317
- print(f"{r.text} (RTF={r.rtf:.4f})")
318
- ```
319
-
320
- ### Streaming and serving
321
-
322
- `engine.transcribe_stream(chunk_generator)` yields incremental transcriptions from any audio
323
- generator, and `scripts/serve.py` in the GitHub repo wraps the same engine in a Flask REST API
324
- (`/transcribe`, `/transcribe/batch`, `/health`, `/info`). See the README's
325
- [Streaming](https://github.com/MohammedAly22/metro-asr#streaming) and
326
- [Serving](https://github.com/MohammedAly22/metro-asr#serving) sections, or the runnable
327
- [streaming_server.ipynb](https://github.com/MohammedAly22/metro-asr/blob/main/examples/streaming_server.ipynb)
328
- notebook.
329
-
330
- ---
331
-
332
- ## Files in this repository
333
-
334
- | File | Description | Size |
335
- |---|---|---:|
336
- | `model.pt` | Checkpoint β€” weights + AdamW optimizer state | 705 MB |
337
- | `config.yaml` | Model architecture configuration | <1 KB |
338
- | `bpe.model` | SentencePiece BPE tokenizer | 316 KB |
339
- | `bpe.vocab` | Human-readable vocabulary listing | 70 KB |
340
- | `lm_5gram.bin` | KenLM 5-gram general-purpose language head (optional) | 5.9 GB |
341
-
342
- Weights alone are 235 MB; `model.pt` is larger because it also carries optimizer state so
343
- training can be resumed from it. Strip that for deployment:
344
-
345
- ```python
346
- import torch
347
- ckpt = torch.load("model.pt", map_location="cpu", weights_only=False)
348
- torch.save({"model_state_dict": ckpt["model_state_dict"], "config": ckpt["config"]},
349
- "model_inference.pt")
350
- ```
351
-
352
- ---
353
-
354
- ## Training
355
-
356
- - **Audio data:** 130K+ clips from the 8 audio datasets listed in this card's metadata, plus
357
- additional Egyptian Arabic content, covering Arabic-only and Arabic-English code-switching
358
- speech.
359
- - **Text data (for the shipped language head):** ~1.9M Egyptian Arabic sentences plus
360
- Arabic-English code-switching text, upsampled to balance against the larger Arabic-only
361
- portion.
362
- - **Recipe:** CTC loss + 0.3-weighted auxiliary CTC at layer 6, AdamW (Ξ² = 0.9, 0.98), linear
363
- warmup into cosine decay, SpecAugment, speed perturbation (0.9Γ—/1.0Γ—/1.1Γ—), 443K steps,
364
- batch size 32 with 4Γ— gradient accumulation, bf16.
365
- - **Hardware:** single GPU.
366
-
367
- Full step-by-step instructions to reproduce this from scratch β€” tokenizer training, language
368
- model training, data preparation, the acoustic training loop, and how to scale the recipe to
369
- Medium/Large β€” are in the GitHub README's
370
- [Training from scratch](https://github.com/MohammedAly22/metro-asr#training-from-scratch)
371
- section.
372
-
373
- ### Fine-tuning this checkpoint
374
-
375
- Adapting to a new domain or accent starts from these weights, not from scratch β€” 10-20Γ— lower
376
- learning rate, encoder frozen for the first few thousand steps so the CTC head adapts first:
377
-
378
- ```bash
379
- python scripts/finetune.py \
380
- --checkpoint checkpoints/model.pt \
381
- --tokenizer-dir checkpoints \
382
- --dataset your/dataset-id \
383
- --lr 5e-5 --max-steps 30000 --freeze-steps 3000
384
- ```
385
-
386
- Runnable end-to-end in
387
- [fine_tuning.ipynb](https://github.com/MohammedAly22/metro-asr/blob/main/examples/fine_tuning.ipynb)
388
- (Colab, needs a GPU), or see
389
- [Fine-tuning](https://github.com/MohammedAly22/metro-asr#fine-tuning) in the README.
390
-
391
- ---
392
-
393
- ## Limitations
394
-
395
- - **Dialect.** Trained on Egyptian Arabic; Modern Standard Arabic and other dialects β€” Gulf,
396
- Levantine, Maghrebi β€” degrade, Maghrebi most of all.
397
- - **Code-switching is Arabic-English only,** and it's the weakest part of the system:
398
- - Technical/domain vocabulary degrades under plain greedy decoding β€” see
399
- [The language head](#the-language-head) above for the fix and the measured improvement.
400
- - A lone English word surrounded by Arabic is harder than a full English clause, which
401
- tends to survive intact.
402
- - Acronyms and short initialisms (`CNN`, `MRI`, `AIC`) are acoustically ambiguous and
403
- unreliable without a matching language head.
404
- - Roughly 12K code-switching training utterances were available against ~130K Arabic-only
405
- ones β€” the imbalance is a data problem, not an architectural one.
406
- - **Clip length.** Trained and evaluated on 0.5–30 s; segment longer recordings before
407
- transcribing.
408
- - **Output is lowercase and unpunctuated**, matching the training transcripts.
409
- - **Not a streaming model in the strict sense** β€” the encoder is bidirectional, so a chunk
410
- must be complete before it can be decoded. `transcribe_stream` is chunked offline decoding,
411
- with a floor latency of one chunk.
412
- - **The language head is large.** The shipped 5-gram is 5.9 GB resident in RAM; a smaller
413
- 4-gram (like the domain heads above) trades some accuracy for a much smaller footprint.
414
- - **The language head asserts priors.** It corrects toward what its training text considers
415
- likely β€” which is exactly what makes it useful, and exactly how it gets unfamiliar proper
416
- nouns wrong. Train a head on your own text if this matters for your use case.
417
- - **Medium and Large are unreleased.** Their configs exist; no weights do.
418
-
419
- ---
420
-
421
- ## Citation
422
-
423
- ```bibtex
424
- @software{metro_asr_2025,
425
- title = {Metro-ASR: Non-Autoregressive Speech Recognition for Egyptian Arabic
426
- and Code-Switching with a Detachable N-gram Language Head},
427
- author = {Mohammed Aly},
428
- year = {2025},
429
- url = {https://github.com/MohammedAly22/metro-asr}
430
- }
431
- ```
432
-
433
- ## License
434
-
435
- MIT β€” see [LICENSE](https://github.com/MohammedAly22/metro-asr/blob/main/LICENSE).
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - ar
5
+ - en
6
+ tags:
7
+ - speech
8
+ - asr
9
+ - automatic-speech-recognition
10
+ - ctc
11
+ - conformer
12
+ - egyptian-arabic
13
+ - code-switching
14
+ - arabic
15
+ - audio
16
+ - pytorch
17
+ - kenlm
18
+ - language-model
19
+ - streaming
20
+ library_name: metro-asr
21
+ pipeline_tag: automatic-speech-recognition
22
+ datasets:
23
+ - AlaaSamir/custom-egy-tts
24
+ - OmarAhmedSobhy/egyption-with-emotion-dataset
25
+ - MightyStudent/Egyptian-ASR-MGB-3
26
+ - MAdel121/arabic-egy-cleaned
27
+ - MAdel121/Continuation-egy-for-ultravox-v1
28
+ - Raniahossam33/Egyptian_TTS3RS
29
+ - ahmedbasemdev/egyptain-tts-dataset
30
+ - MohamedRashad/arabic-english-code-switching
31
+ - librispeech_asr
32
+ metrics:
33
+ - wer
34
+ - cer
35
+ model-index:
36
+ - name: Metro-ASR Small
37
+ results:
38
+ - task:
39
+ type: automatic-speech-recognition
40
+ name: Speech Recognition
41
+ dataset:
42
+ type: custom
43
+ name: Egyptian Arabic + Code-Switching Test Set
44
+ config: all
45
+ split: test
46
+ metrics:
47
+ - type: wer
48
+ value: 46.85
49
+ name: WER (All)
50
+ - type: cer
51
+ value: 28.41
52
+ name: CER (All)
53
+ - type: wer
54
+ value: 37.24
55
+ name: WER (Arabic)
56
+ - type: cer
57
+ value: 17.45
58
+ name: CER (Arabic)
59
+ - type: wer
60
+ value: 36.32
61
+ name: WER (Code-Switching)
62
+ - type: cer
63
+ value: 17.44
64
+ name: CER (Code-Switching)
65
+ ---
66
+
67
+ <h1 align="center">Metro-ASR Small</h1>
68
+
69
+ <p align="center">
70
+ <strong>Non-autoregressive CTC speech recognition for Egyptian Arabic and Arabic–English<br>
71
+ code-switching, with a detachable n-gram language head you can retrain on text alone.</strong>
72
+ </p>
73
+
74
+ <p align="center">
75
+ <a href="https://github.com/MohammedAly22/metro-asr"><img src="https://img.shields.io/badge/GitHub-Repository-E8232A?style=for-the-badge&logo=github" alt="GitHub"></a>
76
+ <a href="https://pypi.org/project/metro-asr/"><img src="https://img.shields.io/pypi/v/metro-asr?style=for-the-badge&logo=pypi&logoColor=white&color=E8232A" alt="PyPI"></a>
77
+ <a href="https://huggingface.co/spaces/mohammedaly22/metro-asr"><img src="https://img.shields.io/badge/%F0%9F%A4%97_Space-Demo-E8232A?style=for-the-badge" alt="Space"></a>
78
+ <a href="https://mohammedaly22.github.io/metro-asr/"><img src="https://img.shields.io/badge/%F0%9F%94%8A_Report-Listen%20%26%20Compare-E8232A?style=for-the-badge" alt="Evaluation report"></a>
79
+ </p>
80
+
81
+ ## What this is
82
+
83
+ Metro-ASR separates the two things a speech recogniser has to know β€” **what the audio sounds
84
+ like** and **what the words are likely to be** β€” into two artefacts trained and shipped
85
+ separately. This repository holds the first one: **Metro-Small**, a 61.6M-parameter Conformer
86
+ acoustic model trained with CTC. It is non-autoregressive β€” one forward pass turns an utterance
87
+ into a matrix of per-frame log-probabilities, with no decoder loop and no dependence on
88
+ previously emitted tokens β€” which is why it runs at 40–55Γ— real time on a laptop CPU with no
89
+ GPU involved anywhere in this card's examples.
90
+
91
+ The second artefact, the language model, is an n-gram over text. It never sees audio, trains
92
+ in minutes on a laptop, and plugs into the decoder at run time β€” including domain-specific
93
+ variants you build yourself from nothing but text (see
94
+ [The language head](#the-language-head) below). The general-purpose one is included in this
95
+ repository as `lm_5gram.bin`.
96
+
97
+ For the full architecture writeup, training-from-scratch instructions, and how to build your
98
+ own domain language head, see the
99
+ **[GitHub repository](https://github.com/MohammedAly22/metro-asr)**. For real transcripts,
100
+ audio players, and a measured comparison of greedy vs. beam+LM decoding across three
101
+ interchangeable language heads, see the
102
+ **[interactive evaluation report](https://mohammedaly22.github.io/metro-asr/)**.
103
+
104
+ <p align="center">
105
+ <img src="images/architecture.svg" alt="Metro-ASR architecture" width="100%">
106
+ </p>
107
+
108
+ ---
109
+
110
+ ## Architecture
111
+
112
+ Twelve identical Conformer-style blocks, each a Macaron sandwich of two half-weighted
113
+ feed-forward networks around an attention module and a convolution module:
114
+
115
+ <p align="center">
116
+ <img src="images/metro-block.svg" alt="Inside one Metro block" width="100%">
117
+ </p>
118
+
119
+ | Component | Details | Why |
120
+ |---|---|---|
121
+ | Encoder | Conformer, 12 layers, d_model=384, 6 heads | β€” |
122
+ | Position encoding | RoPE (rotary), bias-free Q/K projections | attention depends on relative offset, so it doesn't break on utterances longer than any seen in training |
123
+ | Feed-forward | SwiGLU, expansion 3Γ—, applied twice at half weight | a learned gate beats a plain ReLU/GELU FFN at equal parameter count |
124
+ | Normalization | RMSNorm, pre-norm | cheaper than LayerNorm, keeps gradients well-behaved as depth grows |
125
+ | Convolution | SE-gated depthwise separable, kernel 31 | local context (~1.24 s) to complement attention's global view |
126
+ | Regularization | Stochastic depth, rate 0.05 | deeper layers dropped more often during training |
127
+ | Auxiliary loss | Intermediate CTC at layer 6, weight 0.3 | mid-stack layers get gradient directly; discarded at inference |
128
+ | Tokenizer | BPE (SentencePiece), vocab 5,000 | trained on a deliberately balanced Arabic/English corpus so English words survive as whole tokens |
129
+ | Decoding | CTC greedy, or beam search + KenLM | see below |
130
+ | Parameters | 61,586,320 (61.6M) | β€” |
131
+
132
+ **Frame rate.** 16 kHz audio β†’ 80-bin log-Mel (100 fps) β†’ Conv2D Γ—4 subsampling β†’ 25 fps through
133
+ the encoder and the CTC head. Subsampling by 4 before the first block cuts attention's quadratic
134
+ cost 16Γ— before a single block runs; one output token covers 40 ms of audio.
135
+
136
+ ---
137
+
138
+ ## The language head
139
+
140
+ <p align="center">
141
+ <img src="images/language-head.svg" alt="The detachable language head" width="100%">
142
+ </p>
143
+
144
+ CTC's per-frame independence produces a specific, recognisable error pattern: doubled
145
+ syllables, dropped affixes, malformed English fragments β€” the model heard correctly and wrote
146
+ something that isn't a word. A word-level n-gram model fixes this during beam search, because
147
+ it knows which *sequences* are plausible, without ever having heard a single second of audio:
148
+
149
+ <p align="center">
150
+ <img src="images/decoding.svg" alt="Greedy vs. beam search with the language head" width="100%">
151
+ </p>
152
+
153
+ The language head is a separate file with no learned interaction with the acoustic weights.
154
+ Swap it, and nothing about `model.pt` changes:
155
+
156
+ ```python
157
+ engine = MetroASREngine.from_pretrained("small")
158
+
159
+ engine.load_lm("lm/medical_head.bin") # swap language heads at run time,
160
+ print(engine.transcribe("call.wav", beam_search=True).text) # same acoustic weights throughout
161
+ ```
162
+
163
+ **This is measured, not asserted.** The GitHub repo ships two extra language heads β€”
164
+ technical and medical β€” built from real text (Egyptian medical chat/QA, Egyptian Arabic
165
+ Wikipedia's technical articles, real Arabic-English code-switching text) plus synthesised
166
+ domain-term carrier phrases, and decodes 11 real clips with all three heads against human
167
+ references. Headline results from the
168
+ **[full interactive report](https://mohammedaly22.github.io/metro-asr/)**:
169
+
170
+ | Test set | Greedy | General head | Technical head | Medical head |
171
+ |---|---:|---:|---:|---:|
172
+ | Technical clips (WER) | 34.8% | 26.2% | **24.1%** | 26.2% |
173
+ | Medical clips (WER) | 44.9% | **34.7%** | 38.8% | **34.7%** |
174
+ | General speech (WER) | 25.6% | **24.7%** | 36.9% | 32.5% |
175
+
176
+ Two things worth reading out of that table. The domain heads win in their own domain despite
177
+ being 4-grams built from far less text than the general 5-gram β€” domain fit beats scale for
178
+ this component. And the technical head actively *hurts* on general speech (36.9% vs. greedy's
179
+ 25.6%) β€” a language head is a strong prior, and matching it to your traffic matters. (These
180
+ numbers are from a small, hard 11-clip demo set β€” unscripted, overlapping speech, dense
181
+ code-switching β€” and are not the same evaluation as the Performance section below; see the
182
+ report for methodology.)
183
+
184
+ Building your own head takes text and minutes, no GPU:
185
+
186
+ ```bash
187
+ python scripts/train_lm.py --corpus my_domain.txt --out lm/my_domain_4gram.arpa --order 4
188
+ ```
189
+
190
+ Full walkthrough β€” including a from-scratch corpus-building example for a technical and a
191
+ medical domain β€” in the GitHub README's
192
+ [Domain-specialised heads](https://github.com/MohammedAly22/metro-asr#domain-specialised-heads)
193
+ section.
194
+
195
+ ---
196
+
197
+ ## Model variants
198
+
199
+ <p align="center">
200
+ <img src="images/scaling.svg" alt="Metro-ASR family scaling" width="100%">
201
+ </p>
202
+
203
+ Three sizes share one block definition and one training recipe β€” only width, depth and
204
+ vocabulary change. **Small (this repository) is the only one currently trained.** Medium and
205
+ Large exist as configs in the GitHub repo with exact parameter counts, but no weights β€”
206
+ see [Scaling to Medium and Large](https://github.com/MohammedAly22/metro-asr#scaling-to-medium-and-large)
207
+ for what training them actually requires (data volume most of all).
208
+
209
+ | | Params | d_model | Layers | BPE vocab | Status |
210
+ |---|---:|---:|---:|---:|---|
211
+ | **Small** | 61.6M | 384 | 12 | 5,000 | **Released β€” this repo** |
212
+ | Medium | 247.4M | 512 | 24 | 8,000 | Config only |
213
+ | Large | 747.8M | 768 | 32 | 16,000 | Config only |
214
+
215
+ ---
216
+
217
+ ## Performance
218
+
219
+ Held-out test-set WER/CER (the numbers in this card's metadata):
220
+
221
+ | Split | WER (%) | CER (%) |
222
+ |---|---:|---:|
223
+ | All | 46.85 | 28.41 |
224
+ | Arabic only | 37.24 | 17.45 |
225
+ | Code-switching | 36.32 | 17.44 |
226
+
227
+ **Speed** β€” measured, Intel Core Ultra 7 155H, 4 CPU threads, PyTorch 2.13 CPU build, fp32,
228
+ minimum of 15 runs after warm-up:
229
+
230
+ <p align="center">
231
+ <img src="images/latency.svg" alt="Measured CPU latency" width="100%">
232
+ </p>
233
+
234
+ | Audio length | Latency | RTF | Faster than real time |
235
+ |---|---:|---:|---:|
236
+ | 1 s | 37 ms | 0.037 | 27Γ— |
237
+ | 5 s | 99 ms | 0.020 | 51Γ— |
238
+ | **10 s** | **181 ms** | **0.018** | **55Γ—** |
239
+ | 30 s | 686 ms | 0.023 | 44Γ— |
240
+
241
+ Beam search with the 5-gram head adds roughly 10–250 ms per utterance depending on length
242
+ (RTF β‰ˆ 0.024 overall). Loading the 5.9 GB binary itself takes about 3.4 s, once, at startup.
243
+
244
+ ---
245
+
246
+ ## Usage
247
+
248
+ ### Install
249
+
250
+ ```bash
251
+ pip install metro-asr # greedy decoding only
252
+ pip install "metro-asr[lm]" # + KenLM beam search
253
+ pip install -U "numpy>=2.0" # see note below
254
+ ```
255
+
256
+ > [!WARNING]
257
+ > `pyctcdecode`'s only PyPI release pins `numpy<2.0.0` in its own metadata, even though it runs
258
+ > fine under numpy 2.x. Installing the `[lm]` extra will downgrade numpy to satisfy that β€” on an
259
+ > environment that already had numpy 2.x with other packages built against it (Colab, most fresh
260
+ > installs today), that breaks those packages with `numpy.dtype size changed`. The third line
261
+ > above fixes it. If `[lm]` isn't installed at all, `lm_path="auto"` now degrades to greedy with
262
+ > a warning rather than crashing engine construction.
263
+
264
+ ### Quick start
265
+
266
+ ```python
267
+ from metro_asr import MetroASREngine
268
+
269
+ engine = MetroASREngine.from_pretrained("small") # auto-downloads weights + tokenizer, caches locally
270
+ result = engine.transcribe("audio.wav")
271
+ print(result.text)
272
+ ```
273
+
274
+ The 5.9 GB language model is **not** downloaded by this call.
275
+
276
+ ### With the language head (beam search)
277
+
278
+ ```python
279
+ engine = MetroASREngine.from_pretrained("small", lm_path="auto") # also fetches lm_5gram.bin
280
+ result = engine.transcribe("audio.wav", beam_search=True)
281
+ print(result.text)
282
+ ```
283
+
284
+ ### From a manual download
285
+
286
+ If you've already run `snapshot_download` (or `git clone`d this repo) into a local directory,
287
+ point `from_pretrained` at that directory instead β€” nothing gets re-downloaded, and it works
288
+ fully offline:
289
+
290
+ ```python
291
+ from huggingface_hub import snapshot_download
292
+ from metro_asr import MetroASREngine
293
+
294
+ snapshot_download(repo_id="MohammedAly22/metro-asr-small", local_dir="checkpoints")
295
+
296
+ engine = MetroASREngine.from_pretrained("checkpoints", lm_path="auto")
297
+ ```
298
+
299
+ > [!TIP]
300
+ > This is the fix for a common mistake: calling `from_pretrained("checkpoints")` used to be
301
+ > interpreted as a HuggingFace repo id named literally "checkpoints" and fail with
302
+ > *Repository Not Found*. Current versions check for an existing local directory first β€” update
303
+ > if you hit that error.
304
+
305
+ ### Without the package β€” loading the raw PyTorch model
306
+
307
+ ```python
308
+ import torch
309
+ from metro_asr.utils.config import load_config
310
+ from metro_asr.model.metro import MetroASR
311
+ from metro_asr.model.tokenizer import build_tokenizer
312
+
313
+ config = load_config("checkpoints/config.yaml")
314
+ tokenizer = build_tokenizer(config, "checkpoints") # must run before MetroASR.from_config β€”
315
+ model = MetroASR.from_config(config) # it fixes the CTC head's vocab size
316
+ ckpt = torch.load("checkpoints/model.pt", map_location="cpu", weights_only=False)
317
+ model.load_state_dict(ckpt["model_state_dict"])
318
+ model.eval()
319
+ ```
320
+
321
+ ### Batch transcription
322
+
323
+ ```python
324
+ results = engine.transcribe_batch(["audio1.wav", "audio2.wav", "audio3.wav"])
325
+ for r in results:
326
+ print(f"{r.text} (RTF={r.rtf:.4f})")
327
+ ```
328
+
329
+ ### Streaming and serving
330
+
331
+ `engine.transcribe_stream(chunk_generator)` yields incremental transcriptions from any audio
332
+ generator, and `scripts/serve.py` in the GitHub repo wraps the same engine in a Flask REST API
333
+ (`/transcribe`, `/transcribe/batch`, `/health`, `/info`). See the README's
334
+ [Streaming](https://github.com/MohammedAly22/metro-asr#streaming) and
335
+ [Serving](https://github.com/MohammedAly22/metro-asr#serving) sections, or the runnable
336
+ [streaming_server.ipynb](https://github.com/MohammedAly22/metro-asr/blob/main/examples/streaming_server.ipynb)
337
+ notebook.
338
+
339
+ ---
340
+
341
+ ## Files in this repository
342
+
343
+ | File | Description | Size |
344
+ |---|---|---:|
345
+ | `model.pt` | Checkpoint β€” weights + AdamW optimizer state | 705 MB |
346
+ | `config.yaml` | Model architecture configuration | <1 KB |
347
+ | `bpe.model` | SentencePiece BPE tokenizer | 316 KB |
348
+ | `bpe.vocab` | Human-readable vocabulary listing | 70 KB |
349
+ | `lm_5gram.bin` | KenLM 5-gram general-purpose language head (optional) | 5.9 GB |
350
+
351
+ Weights alone are 235 MB; `model.pt` is larger because it also carries optimizer state so
352
+ training can be resumed from it. Strip that for deployment:
353
+
354
+ ```python
355
+ import torch
356
+ ckpt = torch.load("model.pt", map_location="cpu", weights_only=False)
357
+ torch.save({"model_state_dict": ckpt["model_state_dict"], "config": ckpt["config"]},
358
+ "model_inference.pt")
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Training
364
+
365
+ - **Audio data:** 130K+ clips from the 8 audio datasets listed in this card's metadata, plus
366
+ additional Egyptian Arabic content, covering Arabic-only and Arabic-English code-switching
367
+ speech.
368
+ - **Text data (for the shipped language head):** ~1.9M Egyptian Arabic sentences plus
369
+ Arabic-English code-switching text, upsampled to balance against the larger Arabic-only
370
+ portion.
371
+ - **Recipe:** CTC loss + 0.3-weighted auxiliary CTC at layer 6, AdamW (Ξ² = 0.9, 0.98), linear
372
+ warmup into cosine decay, SpecAugment, speed perturbation (0.9Γ—/1.0Γ—/1.1Γ—), 443K steps,
373
+ batch size 32 with 4Γ— gradient accumulation, bf16.
374
+ - **Hardware:** single GPU.
375
+
376
+ Full step-by-step instructions to reproduce this from scratch β€” tokenizer training, language
377
+ model training, data preparation, the acoustic training loop, and how to scale the recipe to
378
+ Medium/Large β€” are in the GitHub README's
379
+ [Training from scratch](https://github.com/MohammedAly22/metro-asr#training-from-scratch)
380
+ section.
381
+
382
+ ### Fine-tuning this checkpoint
383
+
384
+ Adapting to a new domain or accent starts from these weights, not from scratch β€” 10-20Γ— lower
385
+ learning rate, encoder frozen for the first few thousand steps so the CTC head adapts first:
386
+
387
+ ```bash
388
+ python scripts/finetune.py \
389
+ --checkpoint checkpoints/model.pt \
390
+ --tokenizer-dir checkpoints \
391
+ --dataset your/dataset-id \
392
+ --lr 5e-5 --max-steps 30000 --freeze-steps 3000
393
+ ```
394
+
395
+ Runnable end-to-end in
396
+ [fine_tuning.ipynb](https://github.com/MohammedAly22/metro-asr/blob/main/examples/fine_tuning.ipynb)
397
+ (Colab, needs a GPU), or see
398
+ [Fine-tuning](https://github.com/MohammedAly22/metro-asr#fine-tuning) in the README.
399
+
400
+ ---
401
+
402
+ ## Limitations
403
+
404
+ - **Dialect.** Trained on Egyptian Arabic; Modern Standard Arabic and other dialects β€” Gulf,
405
+ Levantine, Maghrebi β€” degrade, Maghrebi most of all.
406
+ - **Code-switching is Arabic-English only,** and it's the weakest part of the system:
407
+ - Technical/domain vocabulary degrades under plain greedy decoding β€” see
408
+ [The language head](#the-language-head) above for the fix and the measured improvement.
409
+ - A lone English word surrounded by Arabic is harder than a full English clause, which
410
+ tends to survive intact.
411
+ - Acronyms and short initialisms (`CNN`, `MRI`, `AIC`) are acoustically ambiguous and
412
+ unreliable without a matching language head.
413
+ - Roughly 12K code-switching training utterances were available against ~130K Arabic-only
414
+ ones β€” the imbalance is a data problem, not an architectural one.
415
+ - **Clip length.** Trained and evaluated on 0.5–30 s; segment longer recordings before
416
+ transcribing.
417
+ - **Output is lowercase and unpunctuated**, matching the training transcripts.
418
+ - **Not a streaming model in the strict sense** β€” the encoder is bidirectional, so a chunk
419
+ must be complete before it can be decoded. `transcribe_stream` is chunked offline decoding,
420
+ with a floor latency of one chunk.
421
+ - **The language head is large.** The shipped 5-gram is 5.9 GB resident in RAM; a smaller
422
+ 4-gram (like the domain heads above) trades some accuracy for a much smaller footprint.
423
+ - **The language head asserts priors.** It corrects toward what its training text considers
424
+ likely β€” which is exactly what makes it useful, and exactly how it gets unfamiliar proper
425
+ nouns wrong. Train a head on your own text if this matters for your use case.
426
+ - **Medium and Large are unreleased.** Their configs exist; no weights do.
427
+
428
+ ---
429
+
430
+ ## Citation
431
+
432
+ ```bibtex
433
+ @software{metro_asr_2025,
434
+ title = {Metro-ASR: Non-Autoregressive Speech Recognition for Egyptian Arabic
435
+ and Code-Switching with a Detachable N-gram Language Head},
436
+ author = {Mohammed Aly},
437
+ year = {2025},
438
+ url = {https://github.com/MohammedAly22/metro-asr}
439
+ }
440
+ ```
441
+
442
+ ## License
443
+
444
+ MIT β€” see [LICENSE](https://github.com/MohammedAly22/metro-asr/blob/main/LICENSE).