morseformer / REPO_README.md
sderhy's picture
docs: README + MODEL_CARD for v0.6.4
9eab86a verified
|
Raw
History Blame Contribute Delete
10.7 kB

morseformer

Open-source transformer-based Morse / CW decoder. Fully local. Apache 2.0.

License: Apache 2.0 Python 3.10-3.13 Release: v0.6.4 Model on HuggingFace

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_splitter in the prose preset.
  • 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_2 for research; dropped from the default prose preset 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_align drive 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 /
    1. 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