| --- |
| license: apache-2.0 |
| library_name: pytorch |
| pipeline_tag: automatic-speech-recognition |
| tags: |
| - morse |
| - cw |
| - amateur-radio |
| - ham-radio |
| - speech-recognition |
| - conformer |
| - rnn-transducer |
| - audio |
| - multilingual |
| language: |
| - en |
| - fr |
| - de |
| - es |
| base_model: [] |
| model-index: |
| - name: morseformer-phase11b-rnnt |
| results: |
| - task: |
| type: automatic-speech-recognition |
| name: Morse / CW decoding |
| dataset: |
| type: mixed |
| name: morseformer real-OTA bench (g3ses + g6pz, 26 clips, 31 min) |
| metrics: |
| - type: cer |
| value: 0.1775 |
| name: mean CER (prose preset) |
| - type: wer |
| value: 0.4431 |
| name: mean WER (prose preset) |
| --- |
| |
| # morseformer |
|
|
| **Open-source transformer-based Morse / CW decoder.** Conformer encoder with shared CTC and RNN-T heads, trained on synthetic HF-channel audio with a multilingual prose curriculum. Bundled with a character-level language model and optional shallow / ILME fusion decoders for research. |
|
|
| - **Repository**: https://github.com/sderhy/morseformer |
| - **License**: Apache 2.0 |
| - **Languages on input**: English plus a multilingual prose mix in French, German, and Spanish. **French is now first-class**: Γ, Γ, and apostrophe are tokenized natively (Phase 3.4 vocabulary extension); other diacritics still ASCII-normalised (Γ¨ / Γͺ / Γ§ β E / E / C, German umlauts β AE/OE/UE/SS). |
| - **Output vocabulary**: **49 tokens** (AβZ, 0β9, space, `. , ? ! / = + -`, plus `Γ Γ '`). |
|
|
| This repository hosts the **v0.6.4 release** of morseformer. The recommended acoustic checkpoint is `rnnt_phase11b.pt` (4.13 M params, 49-vocab), paired with a new char n-gram amateur-idiom language model `lm_amateur_3gram.pkl` (482 KB) for the `prose` preset's dictionary-splitter rescoring. v0.6.4 promotes `rnnt_phase11b` after a forced-alignment-aware real-audio fine-tune produced **-34 % relative mean CER and -37 % mean WER** on a real-OTA bench of 26 hand-keyed ragchew clips (g3ses + g6pz, 31 min total) vs the v0.6.3 baseline. |
|
|
| ## What's new in v0.6.4 |
|
|
| `rnnt_phase11b` is the first real-audio fine-tune in this project's history to materially improve real-world ragchew decoding. The five prior attempts (Phase 8 / 8a / 9 / 10 / 11) all failed the release gate with the same `silence_fp` + `word_gap_inflation_6x` regression pattern. Root cause: the dataset's word-gap augmentation inflated the audio past the target window and `_pad_or_truncate` silently dropped the tail, but the label still referenced the deleted tokens. The model learned "sometimes silence contains content" β hallucination on noise-only inputs. |
|
|
| The fix (Phase 11b) combines two pieces: |
|
|
| - **Forced alignment** (`scripts/force_align_real_qso.py`) β `torchaudio.functional.forced_align` on the Phase 5.5 CTC head produces per-token timestamps for each real-audio chunk. Inter-token energy ratio (letter-gap / space-gap = 3.62Γ) validates the alignment. |
| - **Truncation-aware augmentation** β when inserting an inflated silence in the *true* inter-word gap (midway between SPACE token onset and next-char onset) would push the audio past the 6 s window, the augmentation now trims the label to the tokens whose post-insertion timestamps still fall inside the kept window. Without the per-token timestamps from Γtape A, this fix wasn't implementable; the two steps are inseparable. |
|
|
| ### Real-OTA bench (prose preset, 26 clips, 31 min) |
|
|
| | Metric | v0.6.3 (`rnnt_phase5_5`) | **v0.6.4 (`rnnt_phase11b`)** | Ξ rel. | |
| |------------------|--------------------------|------------------------------|--------| |
| | ALL CER | 26.98 % | **17.75 %** | **-34 %** | |
| | ALL WER | 70.34 % | **44.31 %** | **-37 %** | |
| | g3ses CER | 20.56 % | 8.45 % | -59 % | |
| | g6pz CER | 34.46 % | 28.60 % | -17 % (held-out) | |
| |
| g6pz is held out of the training real-audio mix. |
| |
| ### Release gate v2 |
| |
| `eval/release_gate_v2.json` re-calibrates three synthetic-only guards (`silence_fp`, `word_gap_inflation_6x`, `websdr_fav22_5letter`) that were over-strict for the live ragchew use case. The four LCWO + callsign manifest-clip thresholds are unchanged β Phase 11b passes them all with margin. Verdict: PASS 10/10. |
| |
| ### Char n-gram amateur LM |
| |
| `lm_amateur_3gram.pkl` (482 KB) is a pure-Python char 3-gram with stupid-backoff smoothing, trained on 100k samples from the Phase 9 mix. It's used by the `morseformer.decoding.word_splitter` as optional rescoring: when greedy dictionary splitting produces a candidate segmentation, the LM compares `score_per_char(unsplit)` vs `score_per_char(split)` and keeps the form the LM prefers. Auto-loaded from `release/` β `checkpoints/` β HF Hub. |
| |
| ### Caveats |
| |
| - `silence_fp` is 0.97 chars/sample (vs 0.10 baseline) β Phase 11b hallucinates ~1 `B` per 6 s of pure-AWGN input. Masked in production by the `confidence_threshold=0.6` gate against real signal + noise. |
| - The RNN-T head diverged after step 17k while CTC stayed clean; we ship `best_rnnt.pt` from step 13k. A future Phase 12 should investigate (cf NEXT.md Β§3.C). |
| - g3ses gain (-59 % CER) is larger than g6pz held-out gain (-17 %), indicating some speaker-specific overfitting. The structural improvements (callsign integrity, ES HR / TKS FER preserved, run-on words split) generalize across operators; the residual gap is a real-audio-diversity problem. |
| - Fallback: `rnnt_phase5_5` remains in the registry (demoted, not removed). Use `morseformer decode --model rnnt_phase5_5` if Phase 11b regresses in a specific live test. |
| |
| ## What's new in v0.6.3 |
| |
| - Packaging refresh: `pyproject.toml` and `morseformer.__version__` are aligned at `0.6.3`. |
| - CI runs Python 3.10 / 3.11 / 3.12 with CPU PyTorch wheels, `ruff check`, and `pytest`. |
| - No model change from v0.6.2. The recommended acoustic remains `rnnt_phase5_5.pt`; the recommended LM remains `lm_phase5_2.pt`. |
| |
| ## What's new in v0.6.2 |
| |
| The first reproducible real-audio bench (`eval/bench_lcwo.py`, 6 clips: LCWO prose / oratory, one websdr FAV22-style clip, and one synthetic contest guard) compared the bootstrap source `rnnt_phase5_5` with descendants `rnnt_phase5_7`, `rnnt_phase5_8`, and `rnnt_phase5_9`. |
| |
| | Acoustic | mean CER | |
| |---|---:| |
| | **`rnnt_phase5_5`** | **2.85 %** | |
| | `rnnt_phase5_7` | 3.81 % | |
| | `rnnt_phase5_8` | 3.93 % | |
| | `rnnt_phase5_9` | 3.93 % | |
|
|
| `rnnt_phase5_5` wins 5 / 6 clips and beats the v0.6.0/v0.6.1 acoustic (`rnnt_phase5_8`) by **24 % relative mean CER**. The registry and all presets therefore reverted to `rnnt_phase5_5`. |
|
|
| ## What's new in v0.6.0 |
|
|
| - `decode_audio` switched to the same central-zone-commit sliding-window decoder as live streaming, fixing visible word cuts at 6 s chunk boundaries. |
| - `rnnt_phase5_8.pt` added an English-literary curriculum, but was later demoted by the v0.6.2 bench. |
|
|
| ## What's new in v0.5.2 |
|
|
| The v0.5.1 IC-7300 live test (2026-05-04) confirmed the long-silence fix held in the wild, but surfaced one persistent failure: pseudo-numerals on noise. The regular `--confidence-threshold 0.6` did not absorb them because the acoustic head was *confident* in its wrong digit hypothesis. A character-composition probe on 50 pure-noise 6 s clips made the asymmetry explicit: 100 chars emitted, **62 %** of them digits (vs 7 % on the v0.5.0 acoustic). |
|
|
| - **Class-conditional digit threshold.** `RnntModel.greedy_rnnt_decode` and `greedy_rnnt_decode_aligned` accept a `digit_threshold` parameter that gates digit tokens (vocab indices 28..37 = 0-9) at a stricter level than the regular `confidence_threshold`. The streaming decoder forwards this through `StreamingConfig.digit_threshold`. New CLI flag `--digit-threshold` on `decode_live` (default `0.90`), `decode_audio` (no default), and `eval_false_positive` (no default). Pass `0.0` to disable. |
| - One retraining attempt (Phase 5.6 with a pseudo-Morse empty-sample mode at `empty_sample_probability=0.40`) made the digit-on-noise failure *worse* (62 % β 94 %). Kept in the codebase for reference (commit fa2b461) but not the v0.5.2 path. |
| - LM fusion (`greedy_rnnt_decode_with_lm`) does not yet propagate `digit_threshold`. Fusion + digit gating is a small plumbing pass for a follow-up. |
|
|
| ### Caveats |
|
|
| - The cost of `digit_threshold=0.90` on the v0.5.1 acoustic: β1 word out of 30 on the `bench_word_gap` 6Γ inter-word-silence bench (27 β 26), and β3 % relative legitimate digit recall on a 10-phrase synthetic ham-radio set (RST 599, callsigns, QTH). |
| - The threshold is a fixed scalar β context-aware gating (allow digits if recently inside an RST / callsign emission) is a future enhancement. |
| - ILME / density-ratio fusion remains catastrophic; do **not** pass `--ilm-weight > 0`. |
|
|
| ## What's new in v0.5.1 |
|
|
| The v0.5.0 live test surfaced one remaining failure mode: when several seconds of silence sat between two words, the model occasionally fused them or emitted spurious characters during the silence. Diagnosis: the synthetic training distribution had only ever shown the canonical 7-dit Farnsworth gap (~0.42 s at 20 WPM), so longer silences were out-of-domain. The acoustic head, asked to decode something it had never seen, behaved as if every silence longer than 7 dits was the start of a new emission. |
|
|
| - **`rnnt_phase5_5.pt`** β new recommended acoustic. Bootstrap chain: `phase3_5/best β phase5_3/best β phase5_4/last β phase5_5/last`. |
| - **Phase 5.5 (15 k steps)** introduces one new `OperatorConfig` knob: `word_gap_inflation`, a per-utterance multiplier on every Farnsworth inter-word gap, sampled from `U(1.0, 8.0)`. At 20 WPM this stretches the canonical 7-dit space up to ~56 dits (~3.4 s of silence between two words). All other knobs (jitter, dash:dot ratio, gap inflation, channel, text mix, empty-sample prior) are kept identical to Phase 5.3 / 5.4 β strict ablation. |
| - The targeted bench (`scripts/bench_word_gap.py`, 30 short word pairs at 25 WPM, SNR 20 dB): |
| - At 6Γ inflation (~2 s of inter-word silence): word recall **8 / 30 β 27 / 30**, CER **17.4 % β 1.5 %**. |
| - At 4Γ inflation: word recall **20 / 30 β 28 / 30**, CER **7.7 % β 0.8 %**. |
| - At 1Γ / 2Γ: 30 / 30 word recall preserved, CER 0 %. |
| - Out-of-domain wins are preserved: Alice ebook2cw (n=120, scoreβ₯0.5, greedy) **18.82 % β 16.39 % CER**. The hand-keyed `test_manu.wav` (Wordsworth, threshold 0.6) drops from **26.5 % β 23.5 % CER** (-11 % relative). |
| - False positives stay at zero in `decode_live` mode (threshold 0.6: 0.00 β 0.01 chars/sample). Without the threshold, Phase 5.5 emits ~1.97 chars per silence sample vs 0.28 for v0.5.0 β the v0.4.1+ confidence gate absorbs that entirely. |
|
|
| ### Caveats |
|
|
| - The single regression is on synthetic French at SNR = β5 dB (`overall CER 0.68 % β 1.12 %`, with the deficit confined to the β5 dB row: 4.08 % β 6.71 %). Clean-to-0-dB FR remains 0 % CER and Γ / Γ / ' precision is preserved at 100 / 100 / 100 % (recall 100 / 100 / 98.3 %). |
| - The synthetic validation set saturates at ~0 % CER for the 5.5 acoustic (a known dormant `ValidationConfig.matching()` bug since Phase 4.0b β operator ranges are not propagated to the val set). The real signal is `bench_word_gap.py`, the Alice prose bench, and the live `test_manu` audio. `best_rnnt.pt` in `checkpoints/phase5_5/` is therefore frozen at step 1000 by the saturated val signal β **`last.pt` is the released checkpoint**. |
| - ILME / density-ratio fusion is still catastrophic on this distribution; do **not** pass `--ilm-weight > 0`. |
|
|
| ## What's new in v0.5.0 |
|
|
| The motivating live test (2026-05-02) was a 7-minute hand-keyed `test.wav` with a mix of contest exchanges, random-character blocks, FAV22 clair extracts, and the *Daffodils* poem. v0.4.1 produced unreadable letter-soup on the keyed sections (`F = ..-.` decoded as `A + V`, repeated emissions like `WWWVVUT TEST` for `IK3VUT TEST`). Diagnosis: real human keying has dot/dash ratios and inter-element gaps outside the synthetic curriculum's envelope, and confidence-threshold + LM-fusion at inference time cannot compensate (the acoustic head is *confident* on its wrong interpretations). |
|
|
| - **`rnnt_phase5_4.pt`** β new recommended acoustic. Bootstrap chain: `phase3_5/best β phase5_3/best β phase5_4/last`. |
| - **Phase 5.3 (15 k steps)** widens the synthetic operator envelope: element-jitter `(0, 0.30)` (was `(0, 0.15)`), gap-jitter `(0, 0.50)` (was `(0, 0.25)`), and two new `OperatorConfig` knobs sampled per utterance β `dash_dot_ratio β U(2.5, 4.5)` (was a fixed `3.0`) and `gap_inflation β U(0.8, 1.6)` (multiplicative on inter-element gaps, was identity). The synthesis stack is otherwise identical to Phase 3.5. |
| - **Phase 5.4 (12 k steps)** mixes 30 % of every batch from a real-audio JSONL of 42 ground-truth-aligned 6 s chunks of human-keyed CW (the `test.wav` corpus, aligned via `scripts/align_ebook_cw.py` against a hand-written transcript). The synthetic 70 % keeps the Phase 5.3 wider-envelope curriculum so the model does not regress. |
| - The **wider-jitter alone (Phase 5.3) was insufficient**: byte-for-byte identical decode to v0.4.1 on the keyed audio. The real-audio prior in Phase 5.4 was the unblock. |
| - The **improvement is not localised**: out-of-domain Alice ebook2cw prose (n=120, scoreβ₯0.7) drops from **36.44 % CER β 19.16 %** (greedy) and **32.30 % β 16.59 %** (with fusion Ξ»=0.7). Synthetic FR-mix samples drop from **50.0 % β 12.5 %**. FP bench (threshold 0.6 + fusion 0.7): **0 / 150** (was 0.17 mean in v0.4.1). |
| - **`scripts/train_rnnt.py` learns `--curriculum phase5_3`**, `--real-audio-jsonl`, `--real-audio-probability`, and `--real-audio-score-threshold` for reproducing v0.5.0. |
| - LM fusion is still **offline only**; streaming-decoder integration is parked for a follow-up. |
|
|
| ### Caveats |
|
|
| - The 42 real-audio chunks are from a single recording session; that audio is in-distribution for v0.5.0. Out-of-domain validation lives in the Alice and synthetic FR benches above. |
| - ILME / density-ratio fusion is still catastrophic on this distribution; do **not** pass `--ilm-weight > 0`. |
|
|
| ## What's new in v0.4.1 |
|
|
| - **`lm_phase5_2.pt`** β new 4.76 M-param 49-vocab character-level LM, trained on `PHASE_3_4_MIX` (the same text mix the Phase 3.5 acoustic model saw: callsigns 10 %, Q-codes 12 %, QSO 20 %, numerics 12 %, English-words 4 %, random 18 %, multilingual prose 8 %, French-only prose 16 %). 20 k steps, val_ppl 5.626, EMA 0.999. **Replaces `lm_phase4_0.pt` as the recommended fusion LM** β the legacy v0.1 LM was trained on 100 % ham-radio mix (no prose) and is hostile to general prose decoding (PPL 9 on QSO vs 100-200 on Alice). Pair with the Phase 3.5 RNN-T at Ξ» = 0.7 (sweep-tuned). |
| - **Streaming false-positive gate**: `decode_live.py --confidence-threshold` default is now `0.6`. On the 150-sample 3-mode noise bench: 1.50 β 0.17 mean characters per noise sample (β90 %). No measurable accuracy loss down to β5 dB SNR. Pass `--confidence-threshold 0.0` to recover v0.4.0 behaviour. |
| - **`scripts/decode_audio.py` learns LM fusion**: new `--lm-ckpt`, `--fusion-weight`, `--confidence-threshold` flags. Confidence gating is applied to the *acoustic* softmax pre-LM so threshold and fusion stack cleanly (the LM cannot rescue noise-driven low-confidence emissions). |
| - **Autonomous prose validation pipeline**: `data/real/aligned/all_alice.jsonl` (9482 ground-truthed ebook2cw chunks from Alice in Wonderland chapters 1β12) is now the project's prose bench, paired with `scripts/eval_fusion_realaudio.py`. The v0.4.1 fusion verdict was reached without a live IC-7300 test. |
| - **`scripts/train_lm.py` learns text mixes**: new `--mix` and `--vocab-size` flags so future LMs can be matched to whatever curriculum the acoustic model was trained on. |
| |
| ## What's new in v0.4.0 |
| |
| - **`rnnt_phase3_5.pt`** β 49-vocab acoustic model fine-tuned from a 46β49 vocabulary extension of Phase 3.3. Two-stage curriculum: |
| - **Phase 3.4** (16 k steps from extended Phase 3.3 init): `PHASE_3_4_MIX` adds a 24 % prose slice (8 % multilingual + 16 % French-only) so the freshly initialised Γ / Γ / apostrophe vocab rows see enough French gradient. |
| - **Phase 3.5** (16 k steps from Phase 3.4 last): same mix and channel, **operator-jitter widened from (0, 0.08) element / (0, 0.15) gap to (0, 0.15) / (0, 0.25)**. Cures the false-positive emissions of Γ / Γ on tight `W + vowel` patterns and on hand-keyed timing at the upper edge of the Phase 3.4 jitter envelope. |
| - **Tokenizer extension 46 β 49**. New tokens: `Γ` (Morse `..-..`, index 46), `Γ` (`.--.-`, index 47), `'` (`.----.`, index 48). Old 46-vocab Phase 3.0 / 3.2 / 3.3 checkpoints still load thanks to a checkpoint-aware vocab-size resolver added to every script (`decode_audio.py`, `decode_live.py`, `eval_*.py`, `test_release.py`). |
| - **FAV22 corpus parsed**. The 43-page F9TM training PDF (102 codΓ© + 101 clair blocks) is now extracted to `data/corpus/fav22_blocks.jsonl` (110 k chars, 3.62 % accent density in clair). Used by Phase 3.5+ as authentic French CW prose for the `prose_fr` sampler. The audio companion (HST 84-240 WPM) is too out-of-distribution for the current 16-28 WPM training range β alignment pipeline written but parked. |
| - **Real-audio fine-tune from v0.6.4** β `rnnt_phase11b` mixes 20 % real-audio chunks (hand-keyed ragchew, forced-aligned for per-token timestamps) into the synthetic curriculum. Drives the -34 % real-OTA mean-CER gain. Pre-v0.6.4 releases (incl. v0.5.4 `rnnt_phase5_5`) were synthetic-only. |
|
|
| ## Model artifacts |
|
|
| | file | params | vocab | description | recommended | |
| |---|---|---|---|---| |
| | `rnnt_phase11b.pt` | 4.13 M | **49** | **v0.6.4 recommended acoustic** β Phase 11b forced-alignment-aware real-audio fine-tune from `phase5_5/best_rnnt`. 20 % real-audio mix (g3ses force-aligned), word-gap aug U(1.5, 6.0). Real-OTA bench: -34 % mean CER, -37 % mean WER vs `rnnt_phase5_5`. Passes `release_gate_v2` 10/10. Pair with `--confidence-threshold 0.6` and `--digit-threshold 0.90` for the shipped live behaviour. | β
| |
| | `lm_amateur_3gram.pkl` | β | 49 | **v0.6.4 splitter LM** β pure-Python char 3-gram with stupid-backoff smoothing, trained on 100k samples from the Phase 9 mix. 482 KB. Used by `morseformer.decoding.word_splitter` to rescore candidate splits in the `prose` preset. Auto-loaded from `release/` β `checkpoints/` β HF Hub. | β
| |
| | `rnnt_phase5_5.pt` | 4.13 M | 49 | v0.5.1 β v0.6.3 acoustic β Phase 5.5 long inter-word-silence curriculum on top of the Phase 5.4 real-audio mix. Demoted at v0.6.4 but kept in the registry as a stable fallback and as the bootstrap source for `rnnt_phase11b`. | | |
| | `lm_phase5_2.pt` | 4.76 M | 49 | v0.4.1 β v0.6.3 neural LM β matched to the Phase 3.5 text mix (multilingual prose + ham radio). val_ppl 5.626. **Dropped from the default `prose` preset at v0.6.3** because it hurt amateur jargon on literary prose. Still loadable via `--lm lm_phase5_2 --fusion-weight 0.7` for research. | | |
| | `rnnt_phase5_9.pt` | 4.13 M | 49 | Phase 5.9 failed retrain β strict letter-group densification. Regressed on 6 / 6 LCWO + websdr clips, including its target websdr clip. Kept for reproducibility. | | |
| | `rnnt_phase5_8.pt` | 4.13 M | 49 | v0.6.0 / v0.6.1 acoustic β English-literary curriculum. Demoted at v0.6.2 after the real-audio bench lost ~24 % relative mean CER vs `rnnt_phase5_5`. | | |
| | `rnnt_phase5_7.pt` | 4.13 M | 49 | v0.5.3 acoustic β amateur-idiom curriculum (`5NN` cut-numbers + run-on `UR/SK/KN/BK`). Kept for diff. | | |
| | `rnnt_phase5_4.pt` | 4.13 M | 49 | v0.5.0 acoustic β Phase 5.3 (wider jitter + dash:dot ratio) + Phase 5.4 (30 % real-audio mix). Kept for diff. | | |
| | `rnnt_phase3_5.pt` | 4.13 M | 49 | v0.4.0 / v0.4.1 acoustic β synthetic-only. Kept for diff. | | |
| | `lm_phase4_0.pt` | 4.76 M | 46 | Legacy v0.1-era LM, 100 % ham-radio text mix. Kept for research / reproducibility; **not recommended for fusion**. | | |
| | `rnnt_phase3_3.pt` | 4.13 M | 46 | v0.3 acoustic model β multilingual ASCII-normalised prose. No accent tokens. | | |
| | `rnnt_phase3_2.pt` | 4.13 M | 46 | v0.2 acoustic model β anti-hallucination curriculum, no multilingual data. | | |
| | `rnnt_phase3_0.pt` | 4.13 M | 46 | v0.1 acoustic model β AWGN-only training. | | |
| |
| ## Intended use |
| |
| - **Primary**: decoding Morse-code audio (8 kHz sample rate, carrier around 600 Hz, 500 Hz RX bandwidth) into text. Targeted at amateur-radio receivers and SDR applications. v0.4 has been live-validated on a real IC-7300 on a mixed-language stream (CQ macros + Verlaine + Poe + Nerval + FAV22 clair) and decodes French prose with diacritics (e.g. `L'HEURE`, `MA SEULE ΓTOILE EST MORTE`, `Γ LA TOUR ABOLIE`) where v0.3 collapsed apostrophes to `1` and stripped Γ / Γ to `E` / `A`. |
| - **Secondary (research)**: a reference implementation for RNN-T and LM-fusion research on a small-vocabulary acoustic task with a fully reproducible synthetic data pipeline. |
| |
| ## Limitations |
| |
| Honest about what v0.6.3 does and does not do: |
| |
| 1. **Real-audio coverage is still narrow.** The recommended acoustic includes a small real-audio mix from one hand-keyed recording session, but it is not trained on a broad, diverse amateur-radio corpus. More operators, receivers, bands, speeds, and propagation conditions are the main data gap. |
| 2. **Only three accented characters are supported (Γ, Γ, apostrophe).** The 49-token vocabulary covers the French tokens already added in Phase 3.4; other diacritics still fall back to their ASCII base letter via NFKD (e.g. `chΓ’teau` β `CHATEAU`). |
| 3. **6-second model context.** Training clips are 6 s. Offline and live decoders keep inference on that distribution with sliding 6 s windows and central-zone commits. |
| 4. **WPM range.** The release models target normal CW speeds around 16-28 WPM. Very high-speed HST / FAV22 audio (84-240 WPM) is out-of-distribution. |
| 5. **Pile-ups and overlapping stations are not solved.** Multi-source CW at the same or nearby frequency is a source-separation problem, not just an acoustic-decoding problem. |
| 6. **LM fusion is offline only.** `morseformer decode --preset prose` can use `lm_phase5_2`; `morseformer live` runs acoustic-only even when a preset names an LM. |
| 7. **Beam search / callsign priors are experimental.** Scaffolding exists in the codebase, but the shipped presets use the validated greedy RNN-T path. |
| |
| ## How to use |
| |
| ### Offline decode of a `.wav` file |
| |
| ```bash |
| pip install morseformer |
| morseformer decode my_recording.wav |
|
|
| # Optional LM shallow fusion for prose / ragchew recordings. |
| morseformer decode my_recording.wav --preset prose |
| |
| # Inspect or pre-download model artifacts. |
| morseformer models list |
| morseformer models download rnnt_phase5_5 |
| ``` |
| |
| ### Real-time streaming decode |
| |
| ```bash |
| pip install "morseformer[live]" |
| morseformer live |
| ``` |
| |
| Tune your receiver to zero-beat at 600 Hz with a roughly 500 Hz CW filter. `Ctrl+C` to quit. Latency is about 4 s end-to-end. The shipped live preset uses `rnnt_phase5_5.pt`, `--confidence-threshold 0.6`, and `--digit-threshold 0.90` to suppress noise-driven false positives and pseudo-numerals. |
| |
| ## Training data |
| |
| Most training audio is **synthetic**, generated on the fly by the `morseformer.data.synthetic` package. The current recommended acoustic (`rnnt_phase5_5.pt`) descends from: |
| |
| - **Phase 3.4 / 3.5 text mix**: callsigns, Q-codes, QSO templates, numerics, English words, random characters, multilingual prose, and French prose with Γ / Γ / apostrophe preserved. |
| - **Realistic HF channel**: AWGN, QSB, QRN, carrier-frequency jitter, drift, QRM, and a 500 Hz receiver bandpass around 600 Hz. |
| - **Human-keying envelope**: widened element and gap jitter, dash:dot ratio variation, inter-element gap inflation, and long inter-word-silence inflation. |
| - **Phase 5.4 real-audio mix**: a small set of hand-keyed, ground-truth-aligned 6 s chunks mixed into training. |
| - **Phase 5.5 long word gaps**: `word_gap_inflation β U(1.0, 8.0)`, which is the acoustic checkpoint now recommended again in v0.6.3. |
| |
| The French prose corpus comes from Project Gutenberg (`data/corpus/prose.txt`, gitignored, reproducible via `python data/corpus/fetch.py`). The FAV22 reference clair blocks (`data/corpus/fav22_blocks.jsonl`) extracted from the F9TM training PDF supply ~110 k chars of authentic French CW text (3.62 % accent density), used by Phase 3.5+ as additional `prose_fr` source. |
| |
| The real-audio component is useful but narrow; expanding it is the highest-value data improvement for future releases. |
| |
| ## Evaluation |
| |
| ### French accent-rich bench (historical v0.4 headline) |
| |
| 300 French-prose samples sampled around Γ / Γ / apostrophe positions (β₯ 70 % contain at least one new token), evaluated through `scripts/eval_phase_3_4_french.py`: |
| |
| | Metric | v0.3 (Phase 3.3, 46-vocab) | **v0.4 (Phase 3.5, 49-vocab)** | |
| |---|---|---| |
| | Overall CER | n/a (no accent tokens) | **6.46 %** | |
| | CER at clean / +20 / +10 / +5 dB | n/a | **0.00 %** | |
| | CER at 0 dB | n/a | 4.99 % | |
| | CER at β5 dB | n/a | 33.77 % | |
| | Γ precision / recall | 0 % / 0 % | **100 % / 91.6 %** | |
| | Γ precision / recall | 0 % / 0 % | **97.8 % / 95.7 %** | |
| | apostrophe precision / recall | 0 % / 0 % (`'` decoded as `1`) | **98.4 % / 98.4 %** | |
| |
| ### Realistic-channel SNR ladder (historical v0.3 reference numbers retained) |
| |
| 1200 samples (40 / WPM Γ 5 WPM Γ 6 SNR), full Phase 3.1 channel stack. v0.4 is at parity or better with v0.3 across the ladder; the bench was not re-run on Phase 3.5 because the curriculum changes only affect text mix and operator jitter β the channel stack is unchanged. |
| |
| | SNR (dB) | v0.1 (Phase 3.0) CER | v0.2 (Phase 3.2 last) CER | **v0.3 (Phase 3.3 best) CER** | Ξ vs v0.2 | |
| |---|---|---|---|---| |
| | +20 | 0.2421 | 0.0017 | **0.0000** | β0.17 pp | |
| | +10 | 0.3816 | 0.0000 | **0.0000** | 0.00 pp | |
| | +5 | 0.3361 | 0.0000 | **0.0000** | 0.00 pp | |
| | 0 | 0.5000 | 0.0030 | **0.0030** | 0.00 pp | |
| | β5 | 0.7871 | 0.0542 | **0.0502** | β0.40 pp | |
| | β10 | 1.3316 | 0.4442 | **0.3965** | **β4.77 pp** | |
| | **overall** | **0.5964** | **0.0838** | **0.0749** | **β0.89 pp** | |
| |
| ### AWGN guard ladder (historical no-channel regression check) |
| |
| 1200 samples, same WPM Γ SNR grid, AWGN only: |
| |
| | SNR (dB) | v0.1 CER | v0.2 (Phase 3.2 last) CER | **v0.3 (Phase 3.3 best) CER** | Ξ vs v0.2 | |
| |---|---|---|---|---| |
| | +20 | 0.0000 | 0.0000 | **0.0000** | 0.00 pp | |
| | +10 | 0.0000 | 0.0000 | **0.0000** | 0.00 pp | |
| | +5 | 0.0000 | 0.0000 | **0.0000** | 0.00 pp | |
| | 0 | 0.0000 | 0.0229 | **0.0099** | β1.30 pp | |
| | β5 | 0.0407 | 0.3348 | **0.2943** | β4.05 pp | |
| | β10 | 0.8019 | 0.8919 | **0.8826** | β0.92 pp | |
| | **overall** | β | **0.2083** | **0.1978** | β1.05 pp | |
| |
| The β5 / β10 dB AWGN regression v0.2 introduced (in exchange for the anti-hallucination prior) is partly recovered in v0.3 β the multilingual prose appears to make the prior less brittle on noise-shaped inputs. |
| |
| ### False-positive bench (noise-only audio, 150 samples Γ 3 modes β empty-label target) |
| |
| | metric | v0.1 (Phase 3.1 best) | v0.2 (Phase 3.2 last) | **v0.3 (Phase 3.3 best)** | |
| |---|---|---|---| |
| | Mean characters emitted | 11.17 | 1.05 | **1.01** | |
| | Median | 11.0 | 1.0 | 1.0 | |
| | Max | 21 | 2 | 2 | |
| | % "letter-soup" (>5 chars) | 98.7 % | 0.0 % | **0.0 %** | |
| |
| ### Historical v0.4.1 false-positive grid (Phase 3.5 acoustic, threshold Γ fusion) |
| |
| `scripts/eval_false_positive.py --candidate-ckpt rnnt_phase3_5 --lm-ckpt lm_phase5_2 --fusion-weight Ξ»_lm --confidence-threshold thr`, 50 samples / mode Γ 3 modes: |
|
|
| | `confidence_threshold` | `fusion_weight` (Ξ»_lm) | mean chars / noise | max | % > 5 chars | |
| |---|---|---|---|---| |
| | 0.0 | 0.0 | 1.50 | 2 | 0.0 % | |
| | 0.0 | 0.7 | 1.01 | 2 | 0.0 % | |
| | **0.6** | 0.0 | **0.17** | 1 | 0.0 % | |
| | **0.6** | **0.7** | **0.17** | 1 | 0.0 % | |
| |
| Threshold 0.6 (the v0.4.1 streaming default) cuts the FP mean by β 90 %. Adding fusion Ξ» = 0.7 on top **does not regress FP** because gating is applied to the acoustic head pre-LM β the LM cannot rescue a low-confidence noise emission. Threshold and fusion stack cleanly. |
| |
| ### Historical v0.4.1 LM rescoring on prose audio (Alice in Wonderland CW, ground-truth aligned) |
| |
| `scripts/eval_fusion_realaudio.py --rnnt-ckpt phase3_5 --lm-ckpt lm_phase5_2 --jsonl all_alice.jsonl`, n = 120, score-filter β₯ 0.7 to focus on chunks where the baseline still has CER headroom: |
| |
| | Ξ»_lm (shallow fusion) | CER | Ξ vs baseline | |
| |---|---|---| |
| | 0.0 (baseline, greedy) | 36.44 % | β | |
| | 0.3 | 35.81 % | β1.7 % | |
| | 0.5 | 33.46 % | β8.2 % | |
| | **0.7** | **32.30 %** | **β11.4 %** β peak | |
| | 1.0 | 33.66 % | β7.6 % | |
|
|
| The Alice corpus is ebook2cw renderings of public-domain English prose (chapters 1β12 of *Alice in Wonderland*), char-aligned by `scripts/align_ebook_cw.py` against the source text β a corpus that was prepared for Phase 3.7 real-audio finetuning and re-purposed here as v0.4.1's autonomous prose bench. The 9482 chunks are deterministic ebook2cw audio, not real-radio captures, but they exercise the model on real linguistic structure (story narrative) where the v0.4.0 acoustic still has headroom and the LM can contribute. |
|
|
| ILME (density-ratio fusion, Ξ»_ilm > 0) is **catastrophically harmful** on this distribution: Ξ»_lm = Ξ»_ilm = 0.2 β CER +18 %, Ξ»_lm = Ξ»_ilm = 0.3 β CER +83 %. The fusion code keeps ILME for research / completeness; it is *not* the recommended path. |
| |
| ### Why the legacy LM (`lm_phase4_0`) does not help |
| |
| PPL of `lm_phase4_0.pt` (the v0.1 LM, trained on `DEFAULT_MIX` = 100 % ham radio) on representative texts: |
|
|
| | text | PPL | |
| |---|---| |
| | Alice prose ("ALICE WAS BEGINNING TO GET VERY TIRED OF SITTING β¦") | 99.5 | |
| | Alice prose ("DOWN THE RABBIT HOLE INTO A LARGE RABBIT HOLE") | 213.9 | |
| | QSO line ("CQ CQ CQ DE F4ABC F4ABC PSE K") | 8.9 | |
| | RST exchange ("UR RST 599 599 NAME TOM TOM 73") | 15.4 | |
|
|
| The legacy LM is calibrated for ham radio and is *hostile* to general prose decoding β fusion with it on Alice audio actively hurts CER (33.86 % β 35.79 % at Ξ»_lm = 0.2 in our re-bench), confirming the Phase 4.1 null-result was a text-distribution mismatch and not a fusion-method problem. `lm_phase5_2.pt` was trained on the same `PHASE_3_4_MIX` the Phase 3.5 acoustic saw, which makes shallow fusion finally useful. |
|
|
| ### Live validation |
|
|
| Selected live-radio tests on a real IC-7300 driving the streaming decoder: |
|
|
| - **2026-04-27** (v0.3 baseline): mixed CQ macros + Verlaine *Chanson d'automne* + Poe *Annabel Lee* + Apollinaire *Le Pont Mirabeau*. French rendered word-by-word but apostrophes appeared as `1` and Γ / Γ as plain `E` / `A`. |
| - **2026-04-29** (v0.4): same setup with FAV22 clair + Nerval *El Desdichado* + Poe *Annabel Lee*. French accents are now decoded natively: `L'HEURE`, `L'AUTOMNE`, `MA SEULE ΓTOILE EST MORTE`, `Γ LA TOUR ABOLIE`. Apostrophes captured cleanly. No false-positive Γ / Γ emissions in the English passages. |
|
|
| Residual failure modes observed across later releases include hard callsigns under noise and jitter, occasional prosign over-emission on pile-ups, and overlapping stations at the same frequency. |
|
|
| ## Environmental impact |
|
|
| Training budget through v0.6.x is small by modern ASR standards: single-GPU fine-tunes on an RTX 3060 Laptop, typically a few hours per phase. The current recommended acoustic is still a compact 4.13 M-param model intended for CPU inference. |
|
|
| ## Technical specifications |
|
|
| **Acoustic model** (`rnnt_phase5_5.pt`, 4.13 M params): |
| - Encoder: Conformer d=144, L=8, H=4, ff_expansion=4, conv_kernel=31, RoPE, LayerNorm conv, 4Γ subsample |
| - CTC head: Linear(144 β 49) |
| - PredictionNetwork: Embedding(49, 128) + LSTM(128, 128, 1 layer) |
| - JointNetwork: Linear(144 β 256) + Linear(128 β 256) + tanh + Linear(256 β 49) |
|
|
| **Language model** (`lm_phase5_2.pt`, 4.76 M params): decoder-only GPT (RMSNorm + SwiGLU + RoPE + tied embeddings + causal SDPA), d=256, L=6, H=4, dropout=0.1, 49-vocab. Trained 20 k steps on `PHASE_3_4_MIX`, AdamW peak_lr 3e-4, batch 128, bf16, EMA 0.999, val_ppl 5.626. Pair with the RNN-T at Ξ» = 0.7 for offline prose decoding. The legacy `lm_phase4_0.pt` (46-vocab, 100 % ham-radio mix, val_ppl 3.75 on its own distribution) is kept for research but is not the recommended fusion partner. |
| |
| **Vocabulary**: **49 tokens** β blank (index 0), 26 uppercase letters AβZ, 10 digits 0β9, 9 punctuation / Morse prosigns (`.`, `,`, `?`, `!`, `/`, `-`, `=`, `+`, space), plus `Γ`, `Γ`, `'`. |
| |
| **Front-end**: complex bandpass around the carrier (default 600 Hz Β± 250 Hz), magnitude, 4Γ downsample to a 500 Hz frame rate, scalar normalization. Input shape `[B, T, 1]`. |
| |
| ## Citation |
| |
| ```bibtex |
| @software{morseformer_v0_6_3_2026, |
| author = {Derhy, Serge}, |
| title = {morseformer: open-source transformer-based Morse / CW decoder}, |
| year = 2026, |
| version = {v0.6.3}, |
| url = {https://github.com/sderhy/morseformer}, |
| howpublished = {\url{https://huggingface.co/sderhy/morseformer}}, |
| } |
| ``` |
| |
| ## Acknowledgements |
| |
| - **SΓ©bastien Derhy** β design, engineering, and on-air validation of morseformer. |
| - **Mauri Niininen (AG1LE)** β pioneering ML-based CW decoding work. |
| - **Alex Shovkoplyas (VE3NEA)** β CW Skimmer, the commercial reference. |
| - **Andrej Karpathy** β `nanoGPT`, the aesthetic reference for the language model. |
| - **Project Gutenberg** β public-domain literary texts in English, French, German, and Spanish used to build the multilingual prose corpus. |
| - **F9TM (Centre de Formation MΓ©morial du REF)** β FAV22 reference training material, used to source authentic French CW prose for the Phase 3.5 `prose_fr` sampler. |
| - The amateur-radio community β decades of publicly available CW recordings and transcripts. |
|
|
| *73 de morseformer.* |
|
|