Skip to main content

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 / 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 KarpathynanoGPT, 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

Release files for morseformer 0.6.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for morseformer 0.6.4
File Size Uploaded
morseformer-0.6.4.tar.gz 409.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for morseformer 0.6.4
File Interpreter ABI Platform
morseformer-0.6.4-py3-none-any.whl Python 3 none any Details

Total release size:671.2 kB

Release files / morseformer-0.6.4.tar.gz

Download URL morseformer-0.6.4.tar.gz
Size 409.3 kB
Tags Source
SHA-256 checksum
How to use checksums
5ede230d803d4821d41e930ee1ccc8a382873c6d1d8264809d230f23527413f1
BLAKE2b-256 checksum
How to use checksums
0efe1c9a2954224d573785260bc14b334f50bbae938e95d9b1b81c005ab35c30
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release files / morseformer-0.6.4-py3-none-any.whl

Download URL morseformer-0.6.4-py3-none-any.whl
Size 261.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cdb74d6ec3d906cedff2af801ab6bcbdca08a3410dc4736573e2cd0cc9d66bbf
BLAKE2b-256 checksum
How to use checksums
d2faf10f147a523bb2d828f51fb2a94efeb9f5e0f6505b0757a8beaad5a3f0c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

0.6.4 This release

2 release files

0.6.3

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.4

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page