Skip to content

DeepCW: keep word spaces in the panels; sliding-window decode with time-anchored commit - #1

Open
skerker wants to merge 4 commits into
K5PTB:feat/deepcw-decoder-prototypefrom
skerker:deepcw/streaming-commit
Open

skerker wants to merge 4 commits into
K5PTB:feat/deepcw-decoder-prototypefrom
skerker:deepcw/streaming-commit

Conversation

@skerker

@skerker skerker commented Sep 25, 2026 •

Copy link
Copy Markdown

@K5PTB Follow-up to the streaming limitation noted on aethersdr#4817: decodeLoopDeep() dropped divergent re-decodes and reset every 15 s.

On-air test. Both bulletins below were received over the air from W1AW on 17 m on 2026-09-24 with a FlexRadio FLEX-8400 (firmware 4.2.20.41), receive only, recorded, and replayed offline on an Intel i9-14900HX laptop running Ubuntu 24.04.5 LTS (DeepCW on the CPU).

Trade-off: decoded text now appears about 6 s after the audio. The new loop shows a character only once it is 5 s behind the live edge, so on average the text trails the signal by about 6 s (measured 5.9–6.1 s in replay). The previous loop printed each guess as soon as it was decoded. The wait is what lets the model hear what follows a character before showing it, and it's where the accuracy comes from. It's adjustable with AETHER_DEEPCW_HOLD_S (1–14 s): a 3 s hold gives about 4 s delay and scored 9.2 % CER on ARLD038, against 8.8 % at 5 s (full recording). If you'd rather keep text immediate, the panel fix (51c79dbe) stands on its own.

Changes

Commit Change Why
51c79dbe CW Neural applet and CW panel insert plain text with a colour format instead of insertHtml QTextEdit::insertHtml drops a fragment's leading whitespace (Qt 6.8.3, measured), so a suffix starting with its word space merged onto the previous word
b1fd3a83 DeepCwEngine::inferLogProbs() + greedyEmissions() (character, frame, posterior) per-character timing for the next commit; decode() unchanged
d59c117d DeepCwCommitter replaces the grow / prefix-extend / reset loop re-decode every 2 s; show a character only once it is ≥ 5 s behind the live edge (cutoff snapped into a blank run); keep hold + 3 s when trimming — no reset, no dropped revisions. AETHER_DEEPCW_HOLD_S (1–14 s) overrides the hold
f0306533 tools/deepcw_replay (EXCLUDE_FROM_ALL) offline regression harness: replays a WAV through whole 15 s windows, the previous loop, and DeepCwCommitter

Evidence — character error rate against ground truth text (the W1AW bulletins as ARRL issued them)

CER = character errors (substituted, missing or extra) ÷ ground-truth characters; lower is better.

Two 18 WPM W1AW bulletins recorded on 17 m, replayed offline (same audio per row). All values are CER:

Bulletin Previous loop, as displayed 15 s windows, whole This branch ggmorse, app defaults → narrowed to the signal
ARLD038 DX bulletin, 2026-09-24 (2562 chars) 51.4 % 17.7 % 9.5 % 49.7 % → 41.7 %
ARLP038 propagation bulletin, 2026-09-24 (2487 chars) 45.1 % 13.6 % 7.3 % 36.1 % → 33.5 %

ARLP038's ground truth is the ARRL archive text (issued 2026-09-17, the bulletin W1AW sent that evening); two words differ from what was sent and are scored as sent.

The same sentences side by side (ARLD038; character errors per sentence, a count):

text character errors
ground truth ACTIVITY IS ON 40 TO 10 METERS USING SSB.
DeepCW, this branch ACTIVITY IS ON 40 TO 10 ME DRS USI G SSBK 4
ggmorse ANITIVITC IS TUN E UE 4T? I SIV 24
ground truth JACEK, SP5EAQ, IS QRV AS 5W0AF FROM SAMOA THROUGH OCTOBER 12.
DeepCW, this branch JACEK, SP5EAQ, IS QRV AS 5W0AF F NOM SAMOA THROUGH OCTOBER 12 SK 5
ggmorse T CEK, SP5EAQTTB 5W0SF F TM SAMOA THR WH NH OF ET?M EV 30
ground truth OLAFUR, TF1OL, WILL BE QRV AS 5B/TF1OL FROM LARNACA FROM SEPTEMBER 24 TO SEPTEMBER 27.
DeepCW, this branch OLAS4R, TF1OLN3 EILL BE QA U AS 5B/TF1OL FROM LARNACA FROM SEPOBER 24 TO SEPTE TB R 27. 14
ggmorse ?LAI UEA ES ITETT F?T?I SI HEBE QS ?S 5B?AF1OL U SEE FLTT M LURNACA KEA ITE TH ??EE S EEPS TTEEE . 61
  • ggmorse is its own source replayed through the app's feed path; the replay matches the on-air CW panel to within a character. "Narrowed" = pitch 550–650 Hz, 15–22 WPM, chosen after measuring the recording (its best case). ggmorse held the pitch (601.6 Hz) but its speed estimate reached 33 WPM at the 90th percentile.
  • Both bulletins reproduce from the bundle below, which starts each recording shortly before its bulletin. ARLD038 gives 9.5 % there; decoding from the start of the recording gives 8.8 %.

Weaker stretches. To estimate how each decoder copes when the signal weakens (as in fading), we measured the tone-to-noise ratio at the CW pitch every 2 s and sorted each bulletin's characters into quarters by the ratio when each was sent (about 610–665 characters per quarter, bundle audio). Over both bulletins the ratio ranged from about 12 to 30 dB. It's a relative measure on post-AGC audio, not a calibrated SNR.

Decoder Weakest quarter (CER) 2nd (CER) 3rd (CER) Strongest quarter (CER)
DeepCW, this branch — ARLD038 20.4 % 9.0 % 6.9 % 2.0 %
DeepCW, this branch — ARLP038 21.4 % 4.5 % 1.8 % 1.6 %
ggmorse, narrowed — ARLD038 67.7 % 47.8 % 39.0 % 16.2 %
ggmorse, narrowed — ARLP038 68.2 % 43.2 % 21.8 % 6.0 %

Both decoders make most of their errors in the weakest stretches; there DeepCW's CER was about 20 %, ggmorse's about 68 %.

Takes without ground truth text — share of output characters that form dictionary words, CW abbreviations, callsigns or numbers (a proxy, not CER; higher is better):

Take Previous loop, as displayed 15 s windows, whole This branch
08-14 7.019 MHz weak (the aethersdr#4817 regression take) 0 % 30 % 30 %
08-15 14.037 MHz POTA, strong 14 % 64 % 65 %
09-24 W1AW practice, 20 WPM 24 % 71 % 81 %
09-24 W1AW fast code opening, 35 WPM 8 % 67 % 85 %

Qt 6.8.3, ONNX Runtime 1.27.0; app built at f0306533, About verified; the branch's replay decodes the same text as the build used on air. A Windows (MSVC) build check will follow; the change is platform-neutral C++ and Qt — your call whether to wait for it before merging.

Proof

Full reproduction of both bulletins at f0306533 — audio, ground truth text, both replay tools, scoring scripts, the weaker-stretches analysis, all outputs; run.sh reproduces every number above except the full-recording figures (measured: all outputs identical). Unzip all five into one folder:

Not included

  • Voting across overlapping windows (hop 1–2 s): changed 0.2–1.5 % of characters, fixes and new errors alike; mean score unchanged. The model's errors are consistent across windows.
  • The model omits some word spaces (after one-letter words; 7–10 % of words on weak or Farnsworth-spaced copy).

— authored by agent (Claude Code) on behalf of @skerker

skerker and others added 4 commits September 24, 2026 18:39
QTextEdit::insertHtml() drops a fragment's leading whitespace, so a decode
suffix that began with its word space glued onto the previous word. Insert
plain text with a colour format instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
inferLogProbs() returns the model output for a window; greedyEmissions()
applies the same greedy-CTC rule as ctcDecode() and keeps each character's
frame and posterior. decode() is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaces the grow / prefix-extend / 15 s reset loop in decodeLoopDeep().
Re-decode the window every 2 s; show a character only once it was emitted
at least holdSec (default 5 s) behind the live edge, cutoff snapped into a
blank run; drop the oldest audio but keep hold + 3 s, so there is no reset
and no revision is lost. AETHER_DEEPCW_HOLD_S (1-14 s) overrides the hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replays a recorded WAV through 15 s whole-window decodes, the previous
grow/reset loop as the panel showed it, and DeepCwCommitter as shipped.
EXCLUDE_FROM_ALL; built only on request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@skerker
skerker marked this pull request as ready for review September 25, 2026 03:52
@skerker
skerker marked this pull request as draft September 25, 2026 04:56
@skerker
skerker marked this pull request as ready for review September 25, 2026 05:26
@skerker

skerker commented Sep 25, 2026

Copy link
Copy Markdown
Author

Windows (MSVC) check at f0306533: builds clean, and the replay gives the same text as on Linux.

Setup: Windows 11 Pro, Intel Core i9-14900HX, MSVC 2022 Build Tools + Ninja, Qt 6.8.3, fresh worktree, every
scripts/setup/*.ps1 run in it except the Vulkan SDK one. Configured with -DREQUIRE_ASR_ONNX=ON, so a missing ONNX
Runtime would have stopped the configure. It found ONNX Runtime (sherpa-onnx's bundled 1.27).

Check Result
App build exit 0, 2843/2843 targets
deepcw_replay build exit 0
MSVC warnings in the files this PR changes none (one C4244 in DeepCwEngine.cpp:131, the Hann window line, predates this PR)
Help → About v26.8.3 (f0306533)
Model downloaded by the app, sha256 ef120799…fe02 (matches the pin)
Demo radio, CW, Neural decoder decodes the demo's Ls, as does the DSP decoder
Replay, ARLD038 + ARLP038, deepcw and deepcw commit 5 decoded text identical to the Linux outputs in the bundle, all six files

One small thing in my own commit: tools/deepcw_replay.cpp:103 opens its output files with fopen(path, "w"). On
Windows that is text mode, which turns the two newlines into \r\n, so a byte compare against the Linux outputs reports
a difference even though the decoded text is identical (it is, once the two \r bytes are removed). Changing "w" to
"wb" would fix it. I have left the branch as it is so the head the evidence was taken at doesn't move; I can push
that one-character change if you'd like it.

— authored by agent (Claude Code) on behalf of @skerker

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant