Open-source transformer-based Morse/CW decoder with a built-in ham-specialised language model
Project description
morseformer
Open-source transformer-based Morse / CW decoder. Fully local. Apache 2.0.
Conformer + RNN-T Morse decoder with a real-time streaming CLI, trained on a reproducible synthetic-HF pipeline plus a forced-alignment-aware real-audio fine-tune. The current release is v0.6.4 — promotes rnnt_phase11b.pt as the recommended acoustic (-34 % relative mean CER and -37 % mean WER on a real-OTA bench vs the v0.6.3 baseline) and ships a new amateur-idiom char n-gram LM (lm_amateur_3gram.pkl, 482 KB) used by the dictionary splitter. See CHANGELOG.md for the full version history.
Why
Existing open-source CW decoders (fldigi, cwdecoder, MRP40) rely on hand-tuned DSP and threshold-based segmentation; they struggle in weak-signal conditions, QRM, QSB, and with non-ideal operator timing. The commercial reference, CW Skimmer (VE3NEA), is closed-source and built on ~2009-era Kalman filtering. As of April 2026, there is no published transformer-based CW decoder, and no open-source CW decoder with an integrated language model. morseformer fills that gap.
Quick start
# 1. Create and activate a virtual environment (Python 3.10-3.13).
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
python -m pip install --upgrade pip
# 2a. Decode an audio file (offline). First run fetches the model from HuggingFace.
pip install morseformer
morseformer decode my_recording.wav
# 2b. Or decode a live receiver in real time (PulseAudio input).
pip install "morseformer[live]"
morseformer live
That's it. No GPU needed — the decoder runs on CPU by default.
No Nvidia card? Save ~4 GB. PyTorch ships a 2-3 GB GPU build by default. On a CPU-only machine, install the lightweight CPU build before
morseformer:pip install --index-url https://download.pytorch.org/whl/cpu torch torchaudio pip install morseformer # or "morseformer[live]"
More options
# LM shallow fusion (λ=0.7) + word splitter for prose / ragchew audio.
# The splitter re-segments run-on amateur idioms (DROMCHRIS → DR OM CHRIS)
# using a built-in amateur + English dictionary.
morseformer decode my_recording.wav --preset prose
# Force the splitter on or off independently of the preset:
morseformer decode my_recording.wav --post-segment
morseformer decode my_recording.wav --preset prose --no-post-segment
# Live presets — looser for fast exchanges, tighter for very noisy bands.
morseformer live --preset contest
morseformer live --preset conservative
# Inspect / download checkpoints.
morseformer models list
morseformer models list --advanced
morseformer models download rnnt_phase11b
The four shipped presets — live (default), prose, contest, conservative — bundle the model + thresholds + optional LM behind one flag. morseformer --help lists every subcommand.
Development setup
The quick start above is for users. For development, use Python 3.12 locally;
CI covers Python 3.10-3.13. Some systems expose python3 as Python 3.14 or
newer; that can create a dev virtualenv outside the supported PyTorch /
torchaudio range. The recommended local setup is:
git clone git@github.com:sderhy/morseformer.git
cd morseformer
uv venv .venv --python 3.12
source .venv/bin/activate
uv pip install -e ".[dev,live,gui,demo]"
ruff check .
pytest -q
# CLI works against local release/ and checkpoints/ trees, no Hub fetch.
morseformer decode my_recording.wav
# Or call the underlying scripts directly for fine-grained control.
python -m scripts.decode_audio my_recording.wav \
--ckpt release/rnnt_phase11b.pt \
--confidence-threshold 0.6 \
--digit-threshold 0.90 \
--post-segment \
--post-segment-lm release/lm_amateur_3gram.pkl
Example output on a clean synthetic CQ DE F4HYY K @ 20 WPM / +20 dB SNR:
CTC : 'CQ DE F4HYY K'
RNN-T: 'CQ DE F4HYY K'
Changelog
Per-release notes (model deltas, benches, limitations, recommended decode
recipes) live in CHANGELOG.md. The latest entry is
v0.6.4; older entries go all the way back to
v0.1.0. The changelog is the project's history file; there is no separate
HISTORY.md.
Benchmarks
The v0.6.4 release passes eval/release_gate.py against
eval/release_gate_v2.json (10/10 categories: 4 LCWO prose/oratory
clips, 1 callsign clip, 1 websdr FAV22-style clip, 1 synthetic
contest guard, plus silence-FP / word-gap / latency stress tests).
Promotion was driven by a real-OTA audit of 26 hand-keyed ragchew
clips (g3ses + g6pz, 31 min total) decoded with the prose preset:
| 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 (held-out) | 34.46 % | 28.60 % | -17 % |
g6pz is held out of the training real-audio mix. v0.6.4 closes a silent-truncation bug in the real-audio augmentation that had blocked five consecutive retrains (Phase 8 / 8a / 9 / 10 / 11) — see CHANGELOG.md for the diagnosis.
Shipping a release
Every shippable acoustic must pass eval/release_gate.py, which runs the
LCWO + callsign clips, a synthetic silence false-positive guard, an
inflated word-gap guard, and a streaming-latency check against versioned
thresholds (release_gate_v1.json calibrated on rnnt_phase5_5;
release_gate_v2.json re-calibrated for v0.6.4):
python -m eval.release_gate --manifest eval/release_gate_v2.json
python -m eval.release_gate --acoustic rnnt_phase11b # gate by name
python -m eval.release_gate --ckpt-path checkpoints/<X>/best_rnnt.pt
A JSON report lands in reports/release_gate_<acoustic>_<date>.json and the
process exits 0 if every category is within its non-regression margin
(default +0.5 pp absolute), 1 otherwise. The gate is the single
ship-decision criterion: any new candidate must clear it before its
checkpoint is promoted in the registry. See
reports/technical_debt_2026-05-18.html
for the P0 that motivated this gate.
Architecture
A compact 5-stage pipeline, fully local, CPU-real-time at inference:
audio ──▶ [1] DSP front-end (complex BPF at carrier)
──▶ [2] Conformer encoder (d=144, L=8, RoPE, 4× subsample)
──▶ [3] Dual heads: CTC (framewise) + RNN-T (prediction + joint)
──▶ [4] Optional offline LM shallow fusion
──▶ text
- Encoder: 8-layer Conformer with RoPE attention, depth-wise conv module with LayerNorm, 4× time sub-sampling. ~3.9 M params. Shared between the CTC and RNN-T heads.
- CTC head: single linear on encoder output → per-frame vocab logits.
- RNN-T head: 128-dim LSTM prediction network + 256-dim joint network, blank at index 0. ~0.2 M params.
- Splitter LM (v0.6.4): char 3-gram with stupid-backoff smoothing, trained on 100k synthetic amateur samples from the Phase 9 mix. 482 KB on disk. Rescores candidate splits from
morseformer.decoding.word_splitterin theprosepreset. - Neural LM (legacy, off by default): decoder-only GPT (RMSNorm + SwiGLU + RoPE + tied embeddings + causal SDPA), d=256, L=6. ~4.8 M params. Available via
--lm lm_phase5_2for research; dropped from the defaultprosepreset at v0.6.3 because it hurt amateur jargon on literary prose.
Vocabulary: 49 tokens (blank + 26 letters + 10 digits + 9 punctuation / Morse prosigns + É, À, apostrophe).
Training data
The release models are trained primarily from the synthetic HF pipeline, with a small real-audio fine-tune mixed in for the v0.5+ line:
- Text: callsigns, Q-codes, QSO templates, numerics, English words,
random characters, multilingual prose, and French prose with
É,À, and apostrophe preserved. - Waveform renderer: parametric operator model, Morse keying, and HF
channel simulation in
morse_synth/. Usage is documented in morse_synth/README.md. - Channel: AWGN, QSB, QRN, carrier jitter, carrier drift, receiver bandpass, and QRM.
- Operator timing: widened element/gap jitter, dash:dot ratio variation, gap inflation, and long inter-word silence inflation.
- Real audio: forced-alignment-aware fine-tune (Phase 11b, v0.6.4)
on hand-keyed ragchew chunks. Per-token timestamps from
torchaudio.functional.forced_aligndrive a word-gap augmentation that inserts silence in the true inter-word gap and trims the label when the inflated audio overflows the target window — closing a silent-truncation bug that had blocked Phases 8 / 8a / 9 / 10 / 11. Broader real-audio coverage (multi-operator, W1AW transcripts) remains the main data gap.
Project history
The phase-by-phase training history is intentionally kept in CHANGELOG.md, which is the single source for release notes, model promotions/demotions, benchmark tables, and known regressions. The current model-card summary lives in MODEL_CARD.md, and the current debt snapshot lives in reports/technical_debt_2026-05-18.html.
License
Apache 2.0 — see LICENSE. The released model weights are distributed under the same license.
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 Phase 3.3 multilingual prose corpus
- The amateur-radio community — decades of publicly available CW recordings and transcripts
73 de morseformer
Project details
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 morseformer-0.6.4.tar.gz.
File metadata
- Download URL: morseformer-0.6.4.tar.gz
- Upload date:
- Size: 409.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ede230d803d4821d41e930ee1ccc8a382873c6d1d8264809d230f23527413f1
|
|
| MD5 |
ba6bcce03594616cc46454db93b3b170
|
|
| BLAKE2b-256 |
0efe1c9a2954224d573785260bc14b334f50bbae938e95d9b1b81c005ab35c30
|
File details
Details for the file morseformer-0.6.4-py3-none-any.whl.
File metadata
- Download URL: morseformer-0.6.4-py3-none-any.whl
- Upload date:
- Size: 261.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdb74d6ec3d906cedff2af801ab6bcbdca08a3410dc4736573e2cd0cc9d66bbf
|
|
| MD5 |
31ee08b375f7e1c93d3ca6e94b449aab
|
|
| BLAKE2b-256 |
d2faf10f147a523bb2d828f51fb2a94efeb9f5e0f6505b0757a8beaad5a3f0c7
|