Skip to content

feat(cw): colour DeepFist letters by the model's per-letter confidence (#6099). Principle VIII. - #6103

Merged
jensenpat merged 9 commits into
aethersdr:mainfrom
skerker:feat/6099-deepfist-letter-confidence
Oct 7, 2026
Merged

jensenpat merged 9 commits into
aethersdr:mainfrom
skerker:feat/6099-deepfist-letter-confidence

Conversation

@skerker

@skerker skerker commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6099. Builds on #5953 (merged 2026-10-06 as abc5b26e). This PR's commits: 41a889ef, 4c9a73e5, and the review round 34bea416 (Blocker 1) and e1eb9a9e (Blocker 2). ee5c816b merges main (25de97c3): the two theme files keep main's new token blocks beside this PR's cw block, and #5953's change has dropped out of the diff.

Summary

Each DeepFist letter in the CW decode panel is now coloured by the model's own confidence, in the four colour bands the panel already uses for ggmorse, drawn from four theme tokens: Dark keeps the ggmorse colours, Light gets readable values. The score is the mean posterior over the letter's frames, averaged over the overlapping decode windows that saw it before it settled; cost = 1 − score; the existing bands (< 0.15 green, < 0.35 yellow, < 0.60 orange, else red) apply. Nothing is hidden: the sensitivity slider stays disabled for DeepFist, as before. ggmorse display, callsign spotting and MQTT output are unchanged. DeepFist remains off by default (ENABLE_DEEPFIST_EXPERIMENT), so a default build is unchanged.

Issue criterion Where
DeepFist letters in the four existing colours; ggmorse unchanged src/core/deepfist/DeepFistStream.{h,cpp} (spanConfidence()), DeepFistCommitter.h (Piece, sighting average), src/models/DeepFistCwModel.{h,cpp} (scoredTextDecoded), src/models/CwRxModel.{h,cpp} (coloredTextDecoded), src/gui/PanadapterApplet.{h,cpp} (cwCostColor(), appendColoredCwText()), src/gui/MainWindow_DigitalModes.cpp, resources/themes/default-{dark,light}.json (color.cw.confidence.*)
Default build unchanged the GUI and backend code is inside HAVE_DEEPFIST / the DeepFist sources; a default build also gets the unconnected CwRxModel::coloredTextDecoded signal (beside unscoredTextDecoded) and cwCostColor(), the ggmorse band table moved into a helper with no behaviour change
Socket-free test pins the per-letter score tests/deepfist_committer_test.cpp (existing registered target, extended)
Colour table reproduced on a recording not used to choose the method Proof below: two held-out W1AW takes

Kept on purpose: the unscored path (CwRxModel::unscoredTextDecoded, PanadapterApplet::appendUnscoredCwText) has no producer after this change — DeepFist was its only one — and stays as #5716's contract for a backend that reports no per-letter score; its comment now says so.

Left out: no sensitivity filtering for DeepFist (hiding helps mainly on weak signals; marking does not lose text); no claim for hand-sent code; no change to DeepFist's release status (#4817).

Design choices

  • Score statistic: mean posterior over the letter's frames while it stays the argmax (DeepFistStream::spanConfidence()), then the mean over its sightings in successive windows (DeepFistCommitter, same token within 0.12 s). Same letters chained within 0.12 s in one window pair with an earlier window's sightings by order when the counts match, otherwise each takes the closest sighting; closest alone gives a fast EE swapped scores once the windows' times shift by half the spacing (test case 25). Chosen by measurement on two W1AW takes (the sighting mean separated correct from wrong copy better than a single window's value).
  • Cost, not confidence, over the wire: cost = 1 − score so the existing panel contract (lower is better, PanadapterApplet.cpp band table) applies unchanged — the convention RFC: DeepCW neural CW decoder as an optional second CW backend #4817's review set for a neural backend.
  • Separate signal: coloredTextDecoded(text, cost) beside textDecoded / unscoredTextDecoded on CwRxModel, because this cost is on the backend's own scale and must never meet the Sens threshold that appendCwText() applies to ggmorse's cost.
  • Colours: four tokens, color.cw.confidence.{high,medium,fair,low}, added to both themes (docs/style/theme-style-guide.md §4), seed regenerated, recorded in docs/theming/canonical-tokens.md. Dark = the ggmorse colours (#00ff88 / #e0e040 / #ff9020 / #ff4040). Light = #1a8040 (the theme's green) / #737313 / #ae5700 / #c02020 (the theme's red), 4.59 / 4.61 / 4.64 / 5.56 : 1 on color.background.0 #f5f5f8, the CW panel's background. In Light the yellow and orange are darker than their Dark counterparts (≥ 4.5 : 1 needs that); green and red read clearly. ggmorse keeps its literal table (hardcoded-colour ratchet +0); moving it to the same tokens is a separate follow-up.
  • Identifiers spelled Color, matching the codebase.
  • Doc: docs/deepfist-cw-backend.md now says letters are coloured by the model's posterior and never filtered by the sensitivity slider (it previously said output is displayed "without inventing confidence"). CHANGELOG.md untouched.

Governance note: colours are on the RFC list; this change reuses the existing bands on an off-by-default experiment and adds four theme tokens (Dark = the existing ggmorse colours, two new Light values) and no dependency, thread, setting or default. DeepFist itself lives under RFC #4817, where this is the decode-quality work #6099 cites. Stated here for the maintainers' call.

Relation to the triage on #6099

The triage's sketch and this branch agree on: the score computed in DeepFistStream.cpp, not the vendored files; a separate per-piece signal carrying (text, cost) pairs rather than a widened textDecoded; a separate DeepFist append with no Sens filter and no cwRxTextDisplayed; the band table factored into a helper. Two differences: (1) the cross-window average does not go through m_pending — the committer keeps a short sighting history (same token id within 0.12 s, pruned after 8 s) that the commit-tick path reads, so the letter that commits on a tick does get its earlier windows; the extended test pins that case. (2) spanConfidence() normalises each frame row (max-subtracted softmax), so it yields the same value whether the model export hands back log-probabilities or raw logits; no row-sum test is needed for that.

Constitution principle honored

Principle VIII — the colour is a measured quantity from the model's own output, scored against published bulletin text before and after the change, with the evidence attached.

Test

  • deepfist_committer_test (socket-free, Qt Core only, registered in tests/tests.cmake, builds with the experiment off): a published letter carries the mean over its sightings; the separator is shown as certain; a carried letter averages the sightings it had. Fixtures are CONSTRUCTED values exercising the arithmetic only.
  • Repeated letters (CONSTRUCTED): two E 80 ms apart at 0.9 / 0.1 keep their own scores (case 24, the review's case verbatim); the same pair with the later window shifted 50 ms (case 25); an earlier window that saw one of the two takes the closest-sighting fallback (case 26).
  • Mutations at e1eb9a9e (logs/mutation-check.log in the review-round bundle): head 4c9a73e5's matching restored → exit 24; closest-only → 25; sighting history ignored → 22; restored → 0.
  • Full registered suite at e1eb9a9e: 609 tests, 606 pass, 2 pre-existing skips, 1 fail — hl2_receiver_count_restart_paced_test, a timing check that ran while eight replays loaded the machine; it passes alone (logs/ctest-full.log, logs/hl2-paced-rerun.log). Repeated at ee5c816b after the main merge: 668 tests, 665 pass, 2 pre-existing skips, 1 fail — pgxl_panel_test (:553/:554, the PGXL panel layout check from main's fix(pgxl): fit the docked panel's controls and readings. Principle XI. #6036); this PR touches no PGXL file.
  • CI: the extended test runs on full-suite.yml at merge and weekly on sanitizers.yml; it is not on the frozen per-PR gate (ci: freeze the per-PR test gate; the full suite runs weekly on sanitizers.yml. Principle VIII. #5405). ENABLE_DEEPFIST_EXPERIMENT is not set in any workflow, so hosted builds compile the experiment off. CI at ee5c816b: all five checks pass.
  • Static gates on the head: engine boundary (findings identical to main), test registration, touchpoint manifest, CI test gate, capability records, command plane — all pass.

Proof

Replay evidence from W1AW over-the-air recordings: real band conditions (noise, fading, nearby signals) captured earlier on a FLEX-8400, receive only, and replayed offline. A local replay tool compiled against this head's DeepFistStream / DeepFistCommitter feeds each recording through the same path the app's worker uses and writes every letter with the confidence the panel colours it by; letters are scored right/wrong against the ARRL bulletin text with the scoring helpers used in #5950/#5953. Percent of decoded letters correct per colour (letters in brackets). The table reproduces exactly at e1eb9a9e from the attached audio (the four bulletins, cut as the README states); 4c9a73e5 and e1eb9a9e give identical text and 0 confidence changes in 9,105 letters:

W1AW take green yellow orange red CER
ARLD038 resend, 2026-09-25, 17 m, weak — used to choose the method 95.9 % (679) 78.8 % (236) 55.9 % (238) 32.1 % (137) 52.9 %
ARLP039, 2026-09-26, 40 m, strong — used to choose the method 94.7 % (1729) 64.5 % (256) 42.0 % (100) 14.8 % (27) 18.6 %
ARLP038, 2026-09-24 — held out 98.5 % (1464) 86.6 % (209) 46.4 % (110) 26.1 % (23) 19.7 %
ARLD038, 2026-09-24, 17 m — held out 97.1 % (1736) 76.3 % (169) 36.1 % (83) 20.0 % (20) 14.6 %

The ARLD038 weak row differs from the figures in #6099 (96.7 / 75.6 / 54.3 / 32.6 %, CER 49.8 %): those came from the Phase 0 trace on 2026-09-26; this row is re-scored at this head from the shipped committer's own output.

Three minutes of the held-out ARLP038 take at three signal levels (bar = CW tone over noise; grey = the bulletin text; underlined = wrong):

aethersdr-deepfist-colours-arlp038-three-levels-4c9a73e5-2026-10-02

At 25.7 / 20.0 / 16.7 dB: 83 / 78 / 32 letters decoded, 1 / 7 / 5 wrong, 2 % / 21 % / 46 % not green. A letter the decoder never produced has no colour; on the weak minute the missing words show only as gaps against the bulletin line.

Live smoke at e1eb9a9e (About grab in the bundle): FLEX-8400, live 40 m CW, receive only, DeepFist selected. Pixel colours read from the CW panel grabs: Dark = #00ff88 / #e0e040 / #ff9020 / #ff4040 on #0f0f1a; Light = #1a8040 / #737313 / #ae5700 / #c02020 on #f5f5f8; all four bands appear in each. The bundle's pages/ draw the four recordings in both themes.

Evidence bundle (pages, per-letter CSVs, tools, truth texts, logs):

  • Bundle index:
    • aethersdr-issue6099-deepfist-letter-colours-2026-10-02.zip — head 4c9a73e5 · four replay pages + CSVs, scorer, mutation and suite logs, demo grabs.
    • aethersdr-pr6103-review-round-2026-10-06.zip — head e1eb9a9e · old/new-head replays + comparison, scoring, Dark/Light pages, mutation and suite logs, live-smoke grabs and app log, audio manifest.
    • Audio: eight …-audio-…wav.xz.part0/1.zip files = the four bulletins analysed (two parts each; reassembly and sha256 in the review-round README).
      aethersdr-issue6099-deepfist-letter-colours-2026-10-02.zip

aethersdr-pr6103-review-round-2026-10-06.zip

aethersdr-pr6103-review-round-2026-10-06-audio-arlp038-2026-09-24-15m-bulletin-mono-24k.wav.xz.part1.zip

aethersdr-pr6103-review-round-2026-10-06-audio-arlp038-2026-09-24-15m-bulletin-mono-24k.wav.xz.part0.zip

aethersdr-pr6103-review-round-2026-10-06-audio-arlp039-2026-09-26-40m-bulletin-mono-24k.wav.xz.part1.zip

aethersdr-pr6103-review-round-2026-10-06-audio-arlp039-2026-09-26-40m-bulletin-mono-24k.wav.xz.part0.zip

aethersdr-pr6103-review-round-2026-10-06-audio-arld038-2026-09-25-17m-weak-bulletin-mono-24k.wav.xz.part1.zip

aethersdr-pr6103-review-round-2026-10-06-audio-arld038-2026-09-25-17m-weak-bulletin-mono-24k.wav.xz.part0.zip

aethersdr-pr6103-review-round-2026-10-06-audio-arld038-2026-09-24-17m-bulletin-mono-24k.wav.xz.part1.zip

aethersdr-pr6103-review-round-2026-10-06-audio-arld038-2026-09-24-17m-bulletin-mono-24k.wav.xz.part0.zip

Other backends / other platforms

  • ggmorse: unchanged path (appendCwText), the band table only moved. TX-side decode (appendCwTextTx) unchanged.
  • Built and tested on Linux (Qt 6.12.0), live smoke on a FLEX-8400. Windows and macOS not built here; the change has no platform-specific code.
  • Hand-sent code: not claimed; the measurements are machine-sent W1AW bulletins.

What I tried to break

Test plan

  • Local build passes — clean build of ee5c816b, Linux, -DENABLE_DEEPFIST_EXPERIMENT=ON
  • Behavior verified on a real radio if applicable — FLEX-8400, live 40 m CW, receive only, Dark and Light (Proof); replays of FLEX-8400 recordings
  • Existing tests pass (CI) — locally at ee5c816b (Test); CI passes at ee5c816b
  • Reproduction steps documented if user-reported bug — n/a, feature

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls — use nested-JSON-under-one-key (Principle V) — no settings touched
  • Code is clean-room — not decompiled, disassembled, or reverse-engineered from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (AGENTS.md convention) — n/a, no meter
  • Documentation updated if user-visible behavior changed — docs/deepfist-cw-backend.md; CHANGELOG.md untouched
  • Security-sensitive changes reference a GHSA if applicable — n/a

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

skerker and others added 4 commits September 25, 2026 17:32
…r CW is not gated out (aethersdr#5950). Principle XI.

The stream default (12) comes from n9bc/DeepFist tools/squelch.py, calibrated between
dead air and a tuned-in signal; weak off-air CW scores in that gap and the gate
dropped most of it. The app's parameters now set 3; carriers stay held off by the
completed-mark guard (the carrier regression now also runs at the app's parameters).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aethersdr#6099). Principle VIII.

Each letter carries the mean posterior over its frames, averaged over the
windows that saw it before it settles; the panel colours it with the four
existing cost bands and never applies the Sens threshold. ggmorse is unchanged.

Refs aethersdr#6099

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…). Principle VIII.

Refs aethersdr#6099

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@skerker
skerker marked this pull request as ready for review October 2, 2026 23:15
@skerker
skerker requested a review from a team as a code owner October 2, 2026 23:15

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

Yes, for #6099's three code criteria. Each DeepFist letter now reaches the panel as its own (text, cost) piece and is coloured by the existing four bands; the Sens threshold is not applied to it; ggmorse's path is untouched apart from the band table moving into cwCostColor() (I compared the boundaries — 0.15 / 0.35 / 0.60 and the four literals are identical, so the ggmorse refactor is behaviour-preserving). The fourth criterion ("colour table reproduced on a recording not used to choose the method") is evidence I can read but not reproduce: the two held-out W1AW rows are in the body and the bundle, and I take them as stated.

One criterion deserves a note rather than a tick: #6099 asks for "a socket-free test [that] pins the per-letter score". The committer's cross-window average is pinned (and pinned well — see "what I tried to break"); spanConfidence(), the function that actually computes the score, has no test at all. Details in the nits.

Scope

File / group What it changes Claimed by Verdict
DeepFistCommitter.h Token::confidence, Piece, sighting history + mean #6099 In scope
DeepFistStream.{h,cpp} spanConfidence(), optional pieces out-param #6099 In scope
CwRxModel.{h,cpp} coloredTextDecoded, DeepFist backend rewired to it #6099 In scope
DeepFistCwModel.h/.cpp — scoredTextDecoded, pieces in run() per-piece emit #6099 In scope
DeepFistCwModel.h/.cpp — appParameters(), activityThreshold = 3 the #5950 threshold #5953 (bf9e0338) Stacked base — not this PR's; review under #5953
MainWindow.h, MainWindow_DigitalModes.cpp new slot + UniqueConnection #6099 In scope
PanadapterApplet.{h,cpp} cwCostColor() extraction, appendColoredCwText() #6099 In scope
tests/deepfist_committer_test.cpp confidence cases #6099 In scope
tests/deepfist_cw_model_test.cpp, tests/tests.cmake weakSignal(), carrier test extension, deepfist_weak_signal_test #5953 Stacked base
docs/deepfist-cw-backend.md threshold sentence (#5953) + confidence/colour sentence (#6099) both Mixed, both disclosed

Nothing in the diff is unexplained, and CHANGELOG.md is correctly untouched. The stacked rows are disclosed in the first line of the body with the base SHA, which is the right way to do it — I did not review them on their merits. The merge-order consequence is worth stating plainly for the maintainer: #5953 is still open and assigned, so merging #6103 first lands the threshold change through this PR rather than through its own review. And every colour-band figure in #6099 and in this body was measured at threshold 3, so if #5953 lands at a different value the band calibration needs re-measuring.

No new settings keys, no capability fields, no protocol surface, no new thread, no new colour literal. New public surface is three signals and two static helpers, all C++-internal.

Blockers

None. I could not find a case where this produces wrong text, loses text, crashes, or changes ggmorse behaviour.

Nits (non-blocking)

  • Light-theme contrast: DeepFist text goes from 13.4:1 to 1.23:1 (inline on PanadapterApplet.cpp:931). The old unscored path used color.text.primary; the new path uses the ggmorse literals. Resolving the tokens in resources/themes/default-light.json (color.background.0 → gray.900 → #f5f5f8) against the four literals gives 1.23 : 1 green, 1.29 yellow, 2.08 orange, 3.18 red — and by this PR's own tables ~85 % of letters are green. On the dark theme all four are fine (14.2 / 13.5 / 8.4 / 5.5). ggmorse already has this, so the change is consistency rather than a new class of defect, and docs/a11y.md explicitly permits concrete hex as "a valid intermediate state — file a follow-up issue to add the token". The body says mapping the bands to tokens is "left for a follow-up" with no issue cited. Please file it and reference it here; and consider saying in the body that DeepFist text loses its theme token, since right now that reads as a neutral detail.
  • Two different tolerances for "the same letter in an earlier window" (inline on DeepFistCommitter.h:33): the sighting lookup uses 0.12 s, the pending matcher just below uses 0.16 s. A letter whose time estimate drifts 0.12–0.16 s between windows is still carried as the same pending character but silently loses its earlier sightings, falling back to the single-window value — i.e. to the behaviour the mutation test is designed to catch, for exactly the jittery weak copy the feature exists for. Also, unlike the pending matcher ("one-to-one matching preserves repeated characters as separate events"), the sighting scan is not one-to-one, so a fast same-letter pair can take the other letter's sighting. Same-id-within-0.12 s is the one case where the two matchers diverge and the test does not cover it.
  • spanConfidence() has no direct test (inline on DeepFistStream.h:55). It is static, pure and deterministic — the ideal socket-free CTest subject — but DeepFistStream.cpp only compiles under ENABLE_DEEPFIST_EXPERIMENT, which no workflow sets, so today it is unreachable from any CI-built target. Making it header-only (it touches no model type) would put it on deepfist_committer_test's lane, which is the existing precedent for "model-free algorithms: no sockets, weights or ORT".
  • Token::confidence defaults to 1.f, so a producer that forgets to set it gets green rather than red. Only DeepFistStream and the tests construct Tokens today, so nothing is wrong now; 0.f would fail safe.

Socket tests

No socket test added, modified or removed; no QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost and no Fake* peer in either changed test file. deepfist_committer_test links Qt6::Core only; deepfist_weak_signal_test (stacked, #5953) runs the vendored model over constructed audio and skips with 77 without a model.

What I tried to break

  • The committer arithmetic, by hand, window by window. A publishes at 0.6 = mean(0.9, 0.6, 0.3) — the current sighting via current=true plus two from history, with this window's tokens pushed after the publish loop so there is no double count; T carries through the gated tick at 0.4 = mean(0.2, 0.4, 0.6) with current=false, its own pending confidence covered by the third history entry rather than added twice. The mutation the body claims is real: current-window-only gives 0.3 and exits 22. The test is not asserting the implementation back to itself.
  • Does spanConfidence() measure what its comment says? This was my best shot at a real finding and it held. greedyCtcFrames() (third_party/deepfist/DeepFistCtc.cpp:43-57) pushes t on the first frame of each collapsed run, so walking forward from frames[j] really is the letter's run, not a single frame. And DeepFistStream calls it with the default blankPenalty = 0.0f, so the decoder's argmax and spanConfidence()'s plain max_element are the same argmax — had the penalty been non-zero the span would truncate on exactly the weak letters this feature is for. The logits-vs-log-probs invariance claim also checks out algebraically (softmax(log p) = p when p is normalised).
  • Text/colour divergence. publish() and separate() are the only writers to both output and pieces, so the concatenated pieces equal the returned string; pieces is scoped per queue item alongside output; on stream.failed() the worker returns before emitting, so a mid-item inference failure drops both together. spanConfidence() cannot divide by zero (the break is guarded by t > frame, so count >= 1), and out-of-range input returns 0.f → red rather than a bad read. NaN logits would give NaN cost, which falls through every band to red — fail-safe.
  • Duplicate append. DeepFistCwModel still emits the whole-batch textDecoded(output) and the per-piece signal, which looked like double text in the panel — but after this PR DeepFistCwModel::textDecoded has no consumer outside tests/deepfist_cw_model_test.cpp; the backend connects scoredTextDecoded only. No double insert. unscoredTextDecoded is likewise producerless now, which the body discloses.
  • Lifecycle. The new connection uses the same generation == m_generation && isRunning() guard in CwRxModel::bind() as its two siblings, Qt::UniqueConnection in refreshCwRxBackend() makes repeated backend switches idempotent, and m_history dies with the DeepFistStream that is rebuilt on every generation change in run(). History is bounded: 8 s of pruning at tickSamples = 1280 (0.4 s/tick) is ~20 windows, and the per-letter scan is O(history × ticks) on a handful of letters per second.
  • Governance. GOVERNANCE.md:97 makes "any change to visual design — colors, fonts, spacing, theme, icons" RFC-gated, and :221 says an agent "may not autonomously change" it, with :226 prescribing exactly what this PR did: implement and note that a design decision needs maintainer review. #6099 already carries maintainer-review. I am not calling this a violation — the required disclosure is present — but it is the maintainer's call, not mine.
  • Could not check: no build and no GUI here, so every finding above is reasoned from the code rather than reproduced at runtime, and the replay/CER tables are taken as stated. Also worth being blunt about CI: the five green checks prove the default build still compiles with the unconditional coloredTextDecoded signal and the cwCostColor() refactor — no workflow sets ENABLE_DEEPFIST_EXPERIMENT, so DeepFistStream.cpp, DeepFistCwModel.cpp and appendColoredCwText() were not compiled or run by any check on this head. deepfist_committer_test is registered unconditionally (tests/tests.cmake:7640-7644), so the new confidence cases do build and run in the full suite, though not on the frozen per-PR gate — the body says both of these accurately.

Recommendation

Needs maintainer decision. The engineering is clean and unusually well evidenced, I found no blocker, and the implementation seams match the triage sketch. Two things are above my pay grade: the PR is stacked on an undecided #5953, so the merge order decides where the threshold change gets reviewed; and GOVERNANCE.md:97 puts colour assignment with the maintainer, which the author correctly flagged rather than assumed. Concrete next step for @skerker, both small and independent of those calls: file the theme-token follow-up issue docs/a11y.md asks for and cite it here (noting the light-theme ratios), and either align the 0.12 s sighting window with the matcher's 0.16 s or add a sentence saying why they differ.

Thanks for the held-out-recording table and the mutation log — being able to check which rows chose the method made this much faster to review.


🤖 aethersdr-agent · cost: $7.0666 · model: claude-opus-5

Comment thread src/gui/PanadapterApplet.cpp Outdated
Comment thread src/core/deepfist/DeepFistCommitter.h Outdated
Comment thread src/core/deepfist/DeepFistStream.h
@ten9876 ten9876 self-assigned this Oct 4, 2026
ten9876
ten9876 previously requested changes Oct 4, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

Partially. Head 4c9a73e57374f8344234cacf1d6281a6c20474a2 implements #6099's per-letter confidence path, keeps the sensitivity filter out of DeepFist, and leaves the ggmorse publication/spotting route separate. However, the cross-window matcher can assign one repeated letter another letter's confidence. The new foreground mapping also needs the light-theme regression below addressed.

Scope

File/group Change Claimed? Verdict
src/core/deepfist/DeepFistStream.{h,cpp} Per-span posterior, optional scored pieces Yes In scope
src/core/deepfist/DeepFistCommitter.h Sighting history and per-letter averaged confidence Yes In scope
src/models/DeepFistCwModel.{h,cpp}, CwRxModel.{h,cpp} Scored publication signals and generation guard Yes In scope
src/gui/MainWindow.h, MainWindow_DigitalModes.cpp, PanadapterApplet.{h,cpp} Route scored pieces and select existing bands Yes In scope
tests/deepfist_committer_test.cpp Cross-window/carried confidence arithmetic Yes In scope
DeepFistCwModel::appParameters(), tests/deepfist_cw_model_test.cpp, tests/tests.cmake Threshold 3 and weak/carrier coverage inherited from #5953 Explicitly disclosed stack Review/resolve #5953 before landing its behavior through this PR
docs/deepfist-cw-backend.md Threshold and scored-output contract Yes Both parts disclosed

The older authored threshold commit is the disclosed stack, not hidden bundling. The two feature commits and merge commit are explained. No new dependency, settings document, protocol verb, thread, or CHANGELOG edit. The new signals are internal C++ surface. Scope/checklist claims hold, subject to the stacked dependency and the visual design decision explicitly noted in the body.

Blockers

  1. Match repeated letters to their own earlier sightings. src/core/deepfist/DeepFistCommitter.h:33-38 takes the first reverse-ordered same-ID sighting within 0.12 seconds from each earlier window. That can be the following letter, even when an exact-time match exists. A focused executable using this exact header, with both windows containing E at 2.80 s/confidence 0.9 and another E at 2.88 s/confidence 0.1, returned:
E confidence 0.500
E confidence 0.100

Both sightings of the first letter are 0.9, so its mean should be 0.9 (green); it is incorrectly orange because the later E's 0.1 was borrowed. Closely spaced repeated letters are representable by CTC and occur in fast machine-sent CW; the timestamp tolerance is larger than their separation. This is an executed committer-level reproduction, not a claim that the demo produced that sequence. Select the closest matching token per earlier window and preserve one-to-one matching for repeated letters; add a socket-free regression with two nearby equal IDs having distinct scores. The existing test only spaces its equal-ID text cases farther apart and does not catch this.

  1. Keep the newly coloured DeepFist text readable in Light themes. src/gui/PanadapterApplet.cpp:931 now applies the literal cwCostColor() table to DeepFist output. Previously appendUnscoredCwText() used ThemeManager::color("color.text.primary"). In an exact-head full build, I selected Default Light, connected only DEMO-0001, selected DeepFist and enabled the simulator's CW tone with noise/carrier disabled. The decoder reached “DeepFist ready” and produced green letters; the actual cwDecodePanel capture shows very faint text on the light background. The new green #00ff88 against Default Light's #f5f5f8 has only 1.23:1 contrast (old primary text #1a2a3a: 13.44:1). Yellow is 1.30:1; orange 2.08:1. This is a regression for DeepFist users of the shipped Light theme even though the table already existed for ggmorse. Resolve the bands through theme tokens with readable Light values and verify both themes; reusing the pre-existing table does not preserve the former DeepFist readability.

Nits / maintainer decisions (non-blocking apart from the defects above)

  • src/gui/PanadapterApplet.cpp:825-835: Jeremy, the move to confidence colouring and its band definitions is the visual-design decision #6099 and the PR body defer to you. Reusing a literal table does not make newly coloured output theme-aware. Keep #5953's activity-threshold decision separate; all supplied confidence evidence is at threshold 3.
  • tests/deepfist_committer_test.cpp:95-110 covers sighting arithmetic, but does not directly exercise DeepFistStream::spanConfidence(). I independently compiled that production function and verified posterior averaging, termination when argmax changes, null input, and invariance under adding 1000 to all logits. Keeping that small deterministic coverage in the registered suite would protect the score extraction as well as the aggregation.
  • tools/score_pieces.py:83-95 in the attached evidence bundle removes spaces and normalizes each piece separately; its norm(text) does not use hyp=True. The older #5950 scorer normalizes the complete hypothesis with hyp=True, including removal of the station-ID phrase. Thus the PR's “same scorer used in #5950/#5953” wording needs qualification: shared alignment/normalization helpers do not make these CER definitions identical. The per-colour percentages do reproduce from the bundled labelled CSVs; this is a provenance/wording issue, not a refutation of their measured correlation.

What I verified and tried to break

  • The exact head's deepfist_committer_test passed. Removing history from the averaging calculation in a separate scratch header made it exit 22, independently reproducing the author's mutation claim. Text carry, expiry, delayed separators, and its existing repeat cases survived.
  • The independent nearby-repeat probe above exposes the missing association case. A separate executable compiled the actual spanConfidence() source; results were approximately 0.7 before and after a +1000-logit shift. No synthetic firmware or socket-owning test was introduced or run.
  • Checked the ordinary and scored model paths, stop/switch generation checks, and GUI routing. ggmorse's four boundaries and colours are byte-for-byte the old table. The scored append has no sensitivity suppression and does not emit the callsign-spotting signal. MQTT's existing route remains unchanged by the diff.
  • Downloaded and inspected the supplied evidence bundle without executing its scripts. Independently counted the four labelled CSVs: all stated per-colour counts and percentages agree. The absent original audio means those replay tables were not re-inferred locally. The above scoring qualification is based on reading the supplied scorer.
  • Engine-boundary strict checks on isolated merge-base and head snapshots both exited 0 with identical complete output; manifest and tag files are unchanged. No new above-seam include or enforcement baseline was added.

The exact head also completed a full Linux Release build with DeepFist enabled and the pinned ONNX runtime/model present. Its isolated offscreen automation session used a separate settings directory/socket and connected exclusively to serial DEMO-0001 (model AetherSDR Demo, transmit false). The actual model reached ready and decoded simulated CW through the production publication/GUI path. I captured and inspected the Light-theme decode panel. This proves the rendering regression; it is not an over-the-air accuracy benchmark, a full-suite run, or a before/after binary comparison. The earlier renderer's contrast comparison above is calculated from its retained production renderer and shipped theme values.

The earliest CI runs for this head were created 2026-10-02T22:46:54Z; current main's tip timestamp is 2026-10-04T18:26:40Z. Green checks therefore cover an older merge result, and non-strict protection does not force a refresh. Hosted workflows do not enable DeepFist; their green build is not proof of the experimental code path or the full-suite claim. No separate code-review skill was available; this was a manual audit with compiled probes.

Recommendation

Request changes. Correct the per-letter association and theme regression, add focused coverage, and resolve the disclosed stacked/default/visual decisions before landing. Refresh the stale merge checks after remediation.

— Jeremy KK7GWY + Codex

ten9876
ten9876 previously requested changes Oct 4, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

Partially. Head 4c9a73e57374f8344234cacf1d6281a6c20474a2 delivers #6099's mechanism:

  • DeepFist publishes (text, cost) pieces.
  • The panel colours them with the four existing bands.
  • The Sens filter never applies to them.
  • ggmorse's path is unchanged (same four boundaries and literals, now in cwCostColor()).

Two defects stop it short of "each letter coloured by its own confidence, readably". Close repeated letters can swap scores (Blocker 1, reproduced). In the Light theme, the colours that replace DeepFist's theme token are below the contrast ratio canon requires (Blocker 2). I re-verified both independently of the previous ten9876 review, which found the same two.

Stacking: this PR carries #5953

This branch is #5953's commit bf9e0338 byte-for-byte, then a merge of main, then this PR's own 41a889ef and 4c9a73e5.

  • #5953 must be decided first. Merging this PR first lands the threshold change through this review instead of #5953's.
  • If 3 is accepted, a later merge of main here is clean, since the content is identical.
  • If 3 is changed or rejected, bf9e0338 has to come off this branch, and the colour-band calibration (all measured at threshold 3) needs re-measuring.
  • The body still says this PR "stays a draft until #5953 is decided". It was marked ready for review on 2026-10-02 23:15Z with #5953 still undecided. Please update the body, or move it back to draft.

Scope

File/group Change Claimed? Verdict
src/core/deepfist/DeepFistStream.{h,cpp} spanConfidence(), optional pieces out-param Yes In scope
src/core/deepfist/DeepFistCommitter.h Sighting history, per-letter mean, Piece Yes In scope; Blocker 1
src/models/DeepFistCwModel.{h,cpp} (signal, per-piece emit) scoredTextDecoded Yes In scope
src/models/CwRxModel.{h,cpp} coloredTextDecoded, generation-guarded like its siblings Yes In scope
src/gui/MainWindow.h, MainWindow_DigitalModes.cpp Slot + UniqueConnection Yes In scope
src/gui/PanadapterApplet.{h,cpp} cwCostColor() extraction, appendColoredCwText() Yes In scope; Blocker 2
tests/deepfist_committer_test.cpp Confidence arithmetic cases Yes In scope
DeepFistCwModel::appParameters(), tests/deepfist_cw_model_test.cpp, tests/tests.cmake, half of docs/deepfist-cw-backend.md #5953's threshold change Disclosed stack Belongs to #5953
  • New surface: three internal C++ signals and two static helpers. There is no settings key, wire verb or capability.
  • Not in the diff: no CHANGELOG entry.
  • Consumers: unscoredTextDecoded now has no producer, which the body discloses. I checked for any other consumer of DeepFist text (MQTT, spotting, logging). The panel is the only one on main and here, so rerouting drops nothing.
  • Step-0 preflight: deepfist_committer_test links Qt6::Core only; no socket or fake peer.

Blockers

1. src/core/deepfist/DeepFistCommitter.h:30-38: a repeated letter takes its neighbour's score

The sighting scan walks m_history newest-first and takes the first same-ID sighting within 0.12 s from each earlier window. A window's tokens are pushed in time order, so the reverse walk meets the following letter first. Two Es 80 ms apart, at 0.9 and 0.1, seen identically in two windows. Compiled against this head's header:

'E' 0.500     ← should be 0.900 (both of its sightings were 0.9)
'E' 0.100

So a confident letter shows orange because it averaged in its neighbour.

  • When it happens: adjacent same letters 80–120 ms apart are fast machine CW (an EE pair is about 120 ms apart at 40 WPM).
  • Jitter makes it reachable at ordinary speeds: the comparison is across windows, so any jitter in a letter's time estimate between windows shrinks the gap.
  • What it costs: this is the exact property #6099 asks for.
  • The test misses it: the existing test spaces its equal-ID cases far apart and passes on this head.

Drop-in replacement for the std::vector<double> ticks; for (…rbegin…) {…} block. It picks the closest sighting per earlier window:

            // Per earlier window, the closest sighting of the same token: a repeated
            // letter inside the tolerance must not take its neighbour's score.
            std::vector<std::pair<double, const Sighting*>> closest;
            for (const Sighting& sighting : m_history) {
                const double distance = std::abs(sighting.seconds - token.seconds);
                if (sighting.id != token.id || distance > 0.12) { continue; }
                const auto window = std::find_if(closest.begin(), closest.end(),
                    [&](const auto& entry) { return entry.second->end == sighting.end; });
                if (window == closest.end()) { closest.push_back({distance, &sighting}); }
                else if (distance < window->first) { *window = {distance, &sighting}; }
            }
            for (const auto& entry : closest) {
                sum += entry.second->confidence;
                ++count;
            }

And a regression case for tests/deepfist_committer_test.cpp, just before the final std::puts:

    // A repeated letter inside the 0.12 s tolerance keeps its own score.
    DeepFistCommitter closePair;
    pieces.clear();
    closePair.process({{6, 2.80, "E", 0.9f}, {6, 2.88, "E", 0.1f}}, false, 4.0, 2.0, false, &pieces);
    closePair.process({{6, 2.80, "E", 0.9f}, {6, 2.88, "E", 0.1f}}, false, 4.4, 3.0, false, &pieces);
    if (pieces.size() != 2 || !near(pieces[0].confidence, 0.9f) || !near(pieces[1].confidence, 0.1f)) {
        return 24;
    }

I ran both:

  • This head plus the case: exit 24.
  • With the fix: exit 0, the probe prints 0.900 / 0.100, and every existing case passes.
  • With the fix and the sighting history removed: still exit 22, so your mutation check keeps working.

2. src/gui/PanadapterApplet.cpp:931: DeepFist text drops below the contrast canon in Light themes

The old path drew DeepFist text with color.text.primary (#1a2a3a, 13.44 : 1 on Default Light's #f5f5f8). The new path uses the ggmorse literals. Computed with the WCAG formula against that background:

band hex Default Light
green #00ff88 1.23 : 1
yellow #e0e040 1.29 : 1
orange #ff9020 2.08 : 1
red #ff4040 3.18 : 1

By this PR's own tables about 85 % of letters are green, so most DeepFist output becomes close to invisible in Light. The previous ten9876 review captured the panel on the demo and saw exactly that.

docs/a11y.md:143 sets "4.5 : 1 for normal text". docs/a11y.md:177 allows concrete hex only as "Concrete hex that meets the ratio, file an issue to add the token". None of the four meets it in Light, so the intermediate-state allowance does not apply.

  • Why ggmorse doesn't excuse it: ggmorse already has the same problem, but that is a separate defect. This PR moves DeepFist from a compliant token onto it.
  • Fix: resolve the four bands through theme tokens with Light values that meet 4.5 : 1, and verify both themes. Or, if you want to keep this PR narrow, keep color.text.primary for DeepFist in Light until the tokens exist.
  • Also @ten9876: colour assignment is RFC-scoped visual design (GOVERNANCE.md), which the body correctly flags.

Nits (non-blocking)

  • DeepFistCommitter.h:33 (0.12 s) vs the pending matcher's distance = 0.16 at :66. A letter whose time estimate drifts 0.12–0.16 s between windows is still carried as the same pending character, but loses its earlier sightings and shows its single-window score. Share one constant, or say in the comment why the confidence window is tighter.
  • DeepFistStream.cpp:49-66: spanConfidence() has no test. It is pure and static, but DeepFistStream.cpp compiles only under ENABLE_DEEPFIST_EXPERIMENT, which no workflow sets. Making it header-only would put it on deepfist_committer_test's unconditionally-registered lane.
  • Token::confidence = 1.f default. A producer that forgets to set it gets green. 0.f would fail safe; only DeepFistStream and the tests construct Tokens today.
  • Body: "same scorer used in #5950/#5953". As the previous review noted, the bundle's score_pieces.py normalises per piece without hyp=True, so the CER definitions differ. Please qualify the sentence.

What I verified, what held, what I could not test

  • Ran deepfist_committer_test at this head, compiled standalone from the head's sources with Qt 6 Core, the same link line as its tests.cmake block. It passes.
    • Your mutation: removing the sighting history gives exit 22, as the body claims.
    • Blocker 1: reproduced with a probe against the head's header. The fix and the regression case are validated as stated above.
  • Contrast figures recomputed from the hex values; they match the previous reviews.
  • Merge state.
    • vs main: the head's merge base is 52 commits behind main (85f2a124). git merge-tree is clean, and main has touched none of the DeepFist decoder files since.
    • CI: earliest run on this head was created 2026-10-02 22:46Z, before main's tip. No workflow sets ENABLE_DEEPFIST_EXPERIMENT, so green only proves the default build compiles the unconditional signal and the cwCostColor() refactor.
  • Held.
    • Every publish()/separate() writes both output and pieces, so text and colour cannot diverge.
    • spanConfidence() cannot divide by zero, and returns 0.f (red) for out-of-range input.
    • NaN costs fall through every band to red.
    • The new connection uses the same generation/isRunning() guard as its siblings.
    • m_history is pruned at 8 s and dies with the stream on every generation change.
  • Not run. There is no DeepFist model on this host (DEEPFIST_MODEL_BASE_URL is empty by design), so I did not run inference or drive the DeepFist panel. The Light-theme screenshot evidence is the previous ten9876 review's. I did not reproduce the replay tables, so the per-colour accuracy figures are the author's.

Recommendation

Request changes. Take the closest-match fix and its test, and keep DeepFist text at or above 4.5 : 1 in Light. Then this waits on two maintainer calls: #5953's threshold, which has to land or be refused first, and the colour-assignment decision under GOVERNANCE.md.

— Jeremy KK7GWY + Claude

@ten9876

ten9876 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Maintainer ruling: #5953's threshold 3 is accepted, so this PR's calibration at 3 stands. #5953 lands first. The two blockers in the review above still stand: the identical-letter score swap and Light-theme contrast.

— Jeremy KK7GWY + Claude

skerker and others added 4 commits October 5, 2026 18:35
…noise-only arm (aethersdr#5950). Principle VIII.

A failed decode in the threshold-12 control arm read as "the gate closed" and passed.
The same constructed noise with the keying removed must not print more with the app's
parameters than at 12. The carrier arm's comment names the completed-mark guard, which
holds a carrier at any threshold. The Parameters comment no longer says the app uses
the defaults unchanged.
docs: the threshold is the value every user runs, not a developer setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/deepfist-cw-backend.md
…dr#6099). Principle VIII.

Same letters chained within 0.12 s pair with an earlier window's sightings by
order when the counts match, else each takes the closest sighting. The newest-
first scan gave a fast EE's first letter the second's score; closest alone
swaps again once the windows' times shift by half the spacing (test 25).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Light (aethersdr#6099). Principle VIII.

Four tokens, color.cw.confidence.{high,medium,fair,low}: Dark keeps the four
ggmorse colours; Light uses its own green and red and two darker values for
yellow and orange, each at least 4.5 : 1 on color.background.0. ggmorse keeps
its literals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@skerker

skerker commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

@ten9876 Both blockers addressed in e1eb9a9e: repeated letters pair with earlier sightings by order, with closest as the fallback (your regression case is in as case 24), and DeepFist's colours are four theme tokens with readable Light values. The theme-file conflict with #6198 will be resolved in the main merge after #5953 lands. Evidence and the analysed audio are attached to the body.

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

@NF0T NF0T self-assigned this Oct 6, 2026

@NF0T NF0T left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skerker — #5953 landed today (squash abc5b26e7, 2026-10-06 20:32Z), so this PR is unblocked and ready for your final work. I reviewed e1eb9a9e independently and found no new blocker. I'll come back to approve and merge once it is in a mergeable state.

Both earlier blockers are fixed

  • Repeated letters. The new test exits 24 against the old matcher and 0 on e1eb9a9e. Your mutations reproduce on my side (closest-only → 25, history ignored → 22). On a synthetic stream rich in same-letter pairs, letters landing in the wrong colour band dropped from 13.7 % to 5.2 %.
  • Light-theme contrast. I recomputed the ratios from the tokens as the real ThemeManager resolves them (widget-scoped lookup included): Light is 4.59 / 4.61 / 4.64 / 5.56 : 1 on #f5f5f8, all ≥ 4.5. Dark is unchanged. The theme-seed, test-registration and CI-gate checks pass, and theme_seed_test and theme_manager_test pass from this head.
  • The default build compiles the touched files, and the HAVE_DEEPFIST paths pass -fsyntax-only.

What's needed to get it mergeable

  1. Bring in main. The PR is CONFLICTING in three files: resources/themes/default-dark.json, default-light.json (#6198 added tokens in the same place as yours) and docs/deepfist-cw-backend.md (#5953 landed as a squash, and your rewritten next sentence collides with it). Keep both sides' tokens and keep your rewrite of "displayed without inventing confidence". The #5953 commits then drop out of the diff. Merge or rebase is fine; the repo squash-merges.
  2. Let CI run, and refresh the body. No check has run on this head, because a conflicting PR has no merge commit to test. The body still says the branch is stacked on an open #5953 and that CI has not run; both are now out of date.
  3. Push last, then ping us. Branch protection dismisses approvals on push, so we'll approve against your final head.
  4. @ten9876's two CHANGES_REQUESTED reviews still stand. They were written against 4c9a73e5; the colour assignment is also the visual-design call under GOVERNANCE.md:97. Our approval won't clear his reviews, so a re-review from him will be needed.

Optional, non-blocking

  • Matcher fallback. When the earlier window saw a different number of the same letter, each letter takes the closest sighting and one sighting can serve two letters. Case 26 pins that: the second E (0.1) shows 0.5 by borrowing its neighbour's 0.9. Keeping your order rule and making the fallback one-to-one (each sighting used at most once) lowered the band-mismatch rate from 5.2 / 5.6 / 10.1 % to 2.3 / 3.2 / 8.5 % at ±30 / ±50 / ±80 ms jitter. That is synthetic data, and your 9,105 real letters showed no change between the two heads, so it is rare in practice.
  • Duplicated band thresholds. 0.15 / 0.35 / 0.60 now live in both cwCostColor() and appendColoredCwText(); one shared band helper would keep them from drifting.
  • Still open from earlier rounds:
    • Token::confidence defaults to 1.f, which shows green if a producer forgets to set it; 0.f fails safe.
    • spanConfidence() has no direct test.
    • The sighting window is 0.12 s while the pending matcher uses 0.16 s.
  • History in a comment. The "the review's case" comment in tests/deepfist_committer_test.cpp is review history, which AGENTS.md keeps out of code.

What I did not verify

I couldn't run DeepFist inference here (no model or ONNX Runtime on this machine), so the replay table, the live FLEX smoke test and the panel grabs are yours, not reproduced.

Thanks for the quick turnaround on the review round.

—
73,
Ryan NF0T


👨🏼‍💻 Co-authored by Claude Sonnet 5.5

@NF0T NF0T removed their assignment Oct 6, 2026
# Conflicts:
#	docs/deepfist-cw-backend.md
#	resources/themes/default-dark.json
#	resources/themes/default-light.json
@skerker

skerker commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@NF0T @ten9876 — ready at ee5c816b: main merged (both sides' theme tokens kept, the docs sentence rewrite kept, #5953 out of the diff), CI 5/5 green, body refreshed. No other changes. @ten9876 — your two reviews from 4c9a73e5 still show changes requested; both blockers are fixed at this head (NF0T verified both on 10-06), so a re-review when you have time.

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

@jensenpat jensenpat self-assigned this Oct 7, 2026

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed ee5c816b992c589538caf0afc9afb683497123f4.

Approve. This is the change #6099 asks for: each DeepFist letter is coloured from the model's own posterior, on the four bands the CW panel already uses, with the sensitivity slider still not hiding DeepFist text. ggmorse still goes through appendCwText() and the same four literals. A default build stays off the experiment; CI on this SHA is green (build, check-macos, check-windows, Static checks).

Scope

Group Files Fit
Score DeepFistStream::{h,cpp} spanConfidence(), DeepFistCommitter.h sighting mean #6099's mean posterior, then the mean across windows
Delivery DeepFistCwModel, CwRxModel, MainWindow_DigitalModes.cpp, PanadapterApplet Separate coloredTextDecoded path; Sens filter and cwRxTextDisplayed stay on the ggmorse path
Theme default-{dark,light}.json, ThemeSeedGenerated.cpp, canonical-tokens.md, docs/deepfist-cw-backend.md Four color.cw.confidence.* tokens; doc matches the new behaviour
Test tests/deepfist_committer_test.cpp Existing registered target, extended. No socket

merge-tree against main (43e2633e) is clean. This head is 10 commits behind; the DeepFist files do not collide.

Earlier review items

Both changes requested against 4c9a73e5 are on this head. Repeated letters 80 ms apart keep their own scores (test exits 24 on the old matcher). Light-theme tokens on color.background.0 #f5f5f8 measure 4.59 / 4.61 / 4.64 / 5.56 : 1 (#1a8040, #737313, #ae5700, #c02020). Dark is the existing ggmorse literals #00ff88 / #e0e040 / #ff9020 / #ff4040 (14.19 / 13.51 / 8.39 / 5.49 : 1 on #0f0f1a).

Nits (non-blocking)

  • tests/deepfist_committer_test.cpp:112 says "the review's case". That is review history; the numbers in the assertion are the durable part.
  • Token::confidence defaults to 1.f (DeepFistCommitter.h:14), so a forgotten score is green. 0.f would fall through to red.
  • The 0.15 / 0.35 / 0.60 cuts live in both cwCostColor() and appendColoredCwText().
  • Sighting identity is 0.12 s; the pending-character match is 0.16 s. A letter can stay the same pending character and still lose earlier sightings. The header comment states the 0.12 s rule.

Verification

deepfist_committer_test at this SHA, Qt 6.12, QT_QPA_PLATFORM=offscreen: pass. Forcing closest-sighting for every window (no order match) exits 25. Restored sources pass again. The test binary has no socket.

Not run: DeepFist inference, the held-out W1AW colour table, and the panel itself. Those figures stay the author's. spanConfidence() is compiled only with ENABLE_DEEPFIST_EXPERIMENT, which no workflow sets, so this run does not execute it. Reading it: the row is max-subtracted, the starting frame is always counted, and an empty or out-of-range span returns 0.f.

Follow-up (not a merge condition)

spanConfidence() still has no direct test. A 3×3 float fixture on the committer's always-built target would pin the span walk, the softmax, and the 0.f reject. That needs the function visible without the experiment (header-only, or a small extracted helper). No firmware peer.

@jensenpat
jensenpat dismissed stale reviews from ten9876 and ten9876 October 7, 2026 17:49

Requested fixes are on ee5c816: repeated letters keep their own scores (order match, committer test), and Light confidence tokens meet 4.5:1. This review was against 4c9a73e.

@jensenpat
jensenpat merged commit 3897b3b into aethersdr:main Oct 7, 2026
5 checks passed
ten9876 added a commit that referenced this pull request Oct 11, 2026
…remote operation over CGNAT (#6389)

## Summary

Release prep for **v26.10.2**: the six release files plus the Stable
docs snapshot. Metadata and docs only; no source, test or CI changes.

Cutoff: `origin/main` at `46943eb3c` (#6083, merged 2026-10-11).
Anything that merges after it is folded in before the tag.

**106 merged changes** from **15 human contributors**, AetherClaude (2)
and Dependabot (0). First-time contributor: **@dacacioa**. This was
confirmed by checking each login for a merged PR before the previous
tag, not from `author_association`.

## Changelog

The range `v26.10.1..46943eb` was mapped through the commit-to-PR API
(`repos/…/commits/<sha>/pulls`), not from commit subjects. It holds 106
PRs and no direct commits, and every one is cited in the section.
`v26.10.1` is an ancestor of `main`, so nothing is excluded. #6083
merged before the branch was cut, so it is inside the range and needs no
unmerged-PR banner.

The notes lead with:
- the unified 52 px title bar (#6198);
- docs.aethersdr.com (#6286, #6298, #6294);
- remote operation over CGNAT through the in-radio tailnet shim (#6273,
#6318, #6325, #6319);
- the canon windows (#6227, #6334, #6339, #6340, #6341, #6364, #6372,
#6386).

Then come DVK import, FFT Smooth/De-comb, TCI tune and receiver events,
RTL-SDR multi-receiver delivery, HL2 and ANAN-G2 state memory, the
IC-7300MK2 fixes, the first clean sanitizer runs (stated beside #6350's
TSan red) and Authenticode signing.

Numbers come from the PR bodies. "On upgrading" callouts cover the
behavior changes the bodies flagged:
- HL2 receive audio is 12.04 dB quieter (#6254);
- HL2 host CW is +6.02 dB at a fixed drive (#5975);
- ANAN AGC-T is remapped (#6282);
- sleep prevention is on by default (#6195);
- the Tailscale MTU cap has no opt-out (#6319);
- IC-7300MK2 Auto SQL is withdrawn (#6184).

DeepFist (#5953, #6103) is described as applying only to
`ENABLE_DEEPFIST_EXPERIMENT` builds.

## Docs refresh, not just version strings

- **README**:
- Highlights: FFT Smooth/De-comb on the spectrum line; AetherMap naming;
remote over CGNAT and TCI tune/receiver events on the SmartLink/TCI
line; pre-NR Copy Assist; a new docs.aethersdr.com line.
- Supported Hardware: the KPA1500, marked as not yet validated on
hardware.
- Other radio families: HL2 state restore extended; ANAN-G2 gains AGC-T,
NB2, supply voltage/link and per-radio memory; RTL-SDR now "bundled in
every official package", with WFM stereo, PPM, per-receiver SQL/meters
and DAX/TCI feeds, and multi-receiver explicitly an evaluation.
- Roadmap section: the CTR2 line advanced and a visual-canon line added.
  - Maturity labels are unchanged.
- **ROADMAP**:
- In flight: a v26.10.2 sentence each for aetherd, Icom, ANAN-G2,
RTL-SDR (remaining is now production multi-receiver qualification and
digital FM, RFC #6079), Hermes-Lite 2 and the CTR2 relay (RFC #6332
extensions). A new entry for the visual canon (RFC #6226 / #6239).
  - Queued: RFC #4214 (approved 2026-10-09) added.
- Open RFCs, checked against `label:rfc`: #4214 leaves (approved); #4764
and #4861 leave (shipped by #6198 and #6216); #6346, #6232, #6212 and
#6209 join. #6332 is approved and folds into the CTR2 entry.
- Recently shipped: nine new `(v26.10.2)` entries, still within the
v26.10.x cycle, so no month-rollover trim.
- Backends (six: anan, flex, hl2, icom, rtl, sim) and NR engines (seven)
were checked against the tree; both are unchanged.

## Judgment calls flagged for the maintainer

All of these were decided through AskUserQuestion:
- Version **26.10.2**, date **2026-10-11**, opened ready for review.
- **#6350**, the TSan red in the weekly sanitizer lane: proceed, and
name it below.
- RFCs **#4764** and **#4861** come off Open RFCs as shipped. The issues
themselves are left open for you to close.
- RFC **#4214** goes under Queued (next cycle).
- A **visual canon** In-flight entry, plus a README Roadmap line.
- The **KPA1500** is listed in README Supported Hardware with "not yet
validated on hardware".
- Including **#6083**: you asked for it, and it merged before the
cutoff, so it ships as an ordinary part of the range.

## Known failing on `main`

- **`ctr2_hid_thread_port_test`** fails in the weekly TSan lane (runs of
2026-10-08 and 2026-10-10; #6350). `main`'s CI and Full Suite were green
at `414082197`. The CHANGELOG headline and intro say so: the lanes
achieved their first clean runs this cycle, and the test remains failing
under TSan. This prep does not fix the test, and the release ships with
it red in that lane.

## Scope

| file | change |
|---|---|
| `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.2)` |
| `README.md` | Current version line, plus the prose refresh above |
| `AGENTS.md` | Current version line |
| `CHANGELOG.md` | new `[v26.10.2] — 2026-10-11` section; `[Unreleased]`
stays and stays empty; history below byte-identical |
| `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` |
`<release version="26.10.2" date="2026-10-11"/>` first in `<releases>` |
| `ROADMAP.md` | cycle heading → post-v26.10.2, plus the prose refresh
above |
| `docs/user/versioned_docs/version-stable/**`,
`docs/user/versioned_sidebars/version-stable-sidebars.json`,
`docs/user/stable-version.json`,
`docs/user/src/components/LogAnalyzer/rules.json` | docs snapshot
(`tools/docs/snapshot_stable.py 26.10.2`), in its own commit. This is
the first snapshot since #6286. |

## Validation

Output of `check_release_prep.py --mapping mapping.json` at the branch
head: 30 checks, 1 FAIL (explained below), 1 WARN (the snapshot files,
listed in the scope table).

```
PASS v26.10.1 is an ancestor of origin/main
PASS all six version spots say 26.10.2
PASS CHANGELOG heading date equals metainfo release date
PASS appstreamcli validate
PASS metainfo <release> list is date-descending with unique versions
PASS `## [Unreleased]` stays and stays empty
PASS `## [v26.10.2]` sits directly under `[Unreleased]`
PASS the previous section is `[v26.10.1]`
PASS CHANGELOG diff is exactly the inserted section
PASS new section opens with a `### <headline>`
PASS intro sentence has the house shape
PASS new section has a `### Contributors` block
PASS release_contents.py --check (citations, intro and Contributors counts)
     PRs in range: 106  cited: 106; intro 106/15 matches; Contributors (16 entries) matches; first-time ['dacacioa']
     citations outside the range: #4764, #4861, #5468, #6192, #6226, #6271, #6332 (RFCs), #6238, #6350 (issues)
PASS code fences balance / no duplicated table headers or doubled `---` (README, ROADMAP, new section)
PASS every local link in README.md and ROADMAP.md resolves
PASS no heading removed from README/ROADMAP is still linked elsewhere
PASS Recently shipped `(vX)` markers run newest-first and lead with the new version
PASS Recently shipped intro names the current cycle (v26.10.x)
FAIL only the four current-version lines lose the `26.10.1` string outside CHANGELOG
     historical mentions must stay: "label": "26.10.1"
PASS diff touches no src/, tests/, third_party/ or .github/workflows/ path
PASS all six release files are in the diff
WARN files beyond the six (the docs snapshot)
PASS git diff --check
```

- **The one FAIL is the snapshot's version label, not a historical
mention.** `docs/user/stable-version.json`'s `"label"` is the docs
site's current Stable version, and `snapshot_stable.py` rewrites it by
design. The validator predates #6286 and knows only the four
current-version lines. Run on the six-file commit alone, the same check
passes (29 checks, 0 FAIL, 0 WARN).
- The docs build passes: `npm ci && npm run build` in `docs/user`. `node
tools/docs/test_log_rules.mjs` reports 707 checks passed for 45 rules.
The line moves it reports were already on `main`.
- Four merged PRs (#6340, #6341, #6364, #6372) named screenshots to
recapture after merge (`psk-reporter-sidebar.png`, `menu-tools.png`,
`connect-dialog.png`, `rx-agc-calibration.png`), and #6386 named the
panadapter shots (`panadapter.png` and the main-window captures, which
still show the old title strip). They were not recaptured, so the Stable
snapshot carries the older images.
- No configure, build or test was run for this prep; it is metadata
only, and CI builds it.
- `main` CI at the cutoff `46943eb3c`: Docs and CodeQL - Code Quality
are green; CI, Full Suite, CodeQL and the System Libraries Canary were
still running when this was opened. At `414082197`, every one of them
was green.

After merge: tag the resulting **main** commit, not this branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jensenpat added a commit that referenced this pull request Oct 17, 2026
…dels downloaded on first use (#4817). Principle VIII. (#6300)

## License understanding and immutable pin

The operator accepted this understanding on 2026-10-17 and directed
landing this PR: DeepFist's historical MIT terms and its published
GPL-3.0-or-later terms are both compatible with AetherSDR's GPLv3
licensing. For the current `exp27_bt-champion` download, use the
GPL-3.0-or-later LICENSE and README notice at its resolved upstream
release commit, rather than presenting the repository's first MIT
LICENSE as the license of the released weights. No separate upstream
confirmation of the historical checkpoint is claimed.

- Release revision:
[`930bd3cea7d9bf19fd45413acc9064b54c722871`](https://github.com/n9bc/DeepFist/tree/930bd3cea7d9bf19fd45413acc9064b54c722871).
- [Pinned
LICENSE](https://raw.githubusercontent.com/n9bc/DeepFist/930bd3cea7d9bf19fd45413acc9064b54c722871/LICENSE):
**35,927 bytes**, SHA-256
**`d8dcabd2c59c9e96be89f094a94474685e3fcd5ee8c4304ae7fbbf6c722df6ab`**.
- Model and metadata pins are unchanged. Models remain
download-on-demand from upstream, with no AetherSDR mirror.
- Native helper code keeps its separate historical MIT notices. The
release model attribution is THIRD_PARTY_LICENSES entry 33.
- Compatibility reference: [FSF's GPL-compatible Expat/MIT license
entry](https://www.gnu.org/licenses/license-list.html#Expat).

This note records the accepted release basis for these exact pinned
artifacts; it does not establish the license of future model revisions.

## Summary

Part of #4817. This makes the two neural CW receive decoders standard
features beside ggmorse, through the backend interface #5716 added: the
CW panel's decoder selector offers **ggmorse · DeepFist · DeepCW** in
every build that finds ONNX Runtime (Apple Silicon macOS, Linux,
Windows). ggmorse stays the default and still decodes your own sending.
**Neither model ships with the app**: each downloads on first use from
its upstream source, at a pinned size and SHA-256, with no AetherSDR
copy. `ENABLE_DEEPFIST_EXPERIMENT` is retired. Merging this is the
decision that the two decoders are no longer experimental.

What it delivers against the 2026-09-14 approval on #4817:
- **DeepCW lands first, toggle only.** `DeepCwRxBackend` joins
`CwRxModel`'s catalog as `"deepcw"`. The CW Neural compare applet and
the second decoder instance stay on the prototype branch. Only the
selected decoder runs; the TX decoder stays ggmorse.
- **Download decoupled from the ASR build option** (the blocker in the
approval). Both models go through the existing `CwModelAssets`
downloader, which has no ASR dependency.
- **Upstream sources only.** DeepCW: `e04/deepcw-engine` at commit
`9185d5da` (its only model revision). DeepFist: N9BC's
`exp27_bt-champion` release, whose `deepfist.onnx` and
`deepfist.onnx.json` match main's pins. The release tag resolves to
`930bd3cea7d9bf19fd45413acc9064b54c722871`; its GPL-3.0-or-later LICENSE
is downloaded from that exact commit, pinned at 35,927 bytes and SHA-256
`d8dcabd2c59c9e96be89f094a94474685e3fcd5ee8c4304ae7fbbf6c722df6ab`. The
native helpers retain their historical MIT notices.
- **Backend key** stays a string in `AppSettings["CwDecoder"]`
(`"ggmorse"` / `"deepfist"` / `"deepcw"`).
- **Licensing:** DeepCW follows the approval's attribution route:
THIRD_PARTY_LICENSES entry 32 covers the ported code and the downloaded
weights, and `DeepCwEngine.{h,cpp}` now also say the AGPL-3.0 port was
modified (per-character emissions), per AGPL §5(a).

DeepCW uses the sliding-window committer from K5PTB#1, which
this supersedes. The scope follows #4817's approval; the license
understanding accepted by the operator is recorded below.

## Rebase and review validation (2026-10-17)

Head `8275f36dbd8cacf4049f0b1e57f3cfd194ec3cb2` is rebased onto main
`94e3cd47d`, with original authorship preserved and all commits
verified. Follow-up fixes bound DeepCW trimming, reset after dropped
ring audio, keep ggmorse controls usable for an unknown saved backend
key, validate replay hold values, and correct the Zero Beat wording and
decoder documentation.

Apple Silicon validation now includes an ARM64/Qt 6.12.0/ONNX Runtime
1.29.0 app build, 11 focused CTests passed, targeted mutation checks,
and a TX-disabled `DEMO-0001` bridge check of all three decoders,
stale-key fallback, Zero Beat gating, DeepCW selection across a process
restart, and simulated disconnect. No live-radio or TX test was run.
Fresh CI is running. The operator subsequently accepted the
release-license understanding below. The downloader is now neutrally
named `CwModelAssets`; its HTTP user-agent and diagnostics also use CW
naming.

## Final landing validation

Current head `d22d21080a7e8b9c68d9052b71c2c04a53bc6f97` adds the
release-license pin and `CwModelAssets` rename. The ARM64/Qt 6.12.0/ONNX
Runtime 1.29.0 incremental app build passed, and **11/11 focused CTests
passed** with the new GPL LICENSE in the private model cache. The app
replaced the old 1,068-byte LICENSE with the exact 35,927-byte pin while
preserving both model files' bytes, hashes and modification times. On
the isolated TX-disabled `DEMO-0001` bridge, DeepFist and DeepCW both
became ready and emitted fresh text through the renamed downloader. The
owned app was disconnected and closed. Static
boundary/manifest/registration/reference/log-rule checks passed;
current-head hosted checks must finish before merge. Earlier mutation,
persistence and Windows/Linux evidence below remains identified by its
original commit.

## Before merge

- [x] **DeepFist license understanding and pin.** On 2026-10-17 the
operator accepted following the published release revision's
GPL-3.0-or-later terms for this pinned download. The historical MIT
license is also GPL-compatible; no incompatible license was identified.
This is the project's recorded understanding, not a claim of a separate
N9BC clarification about the older checkpoint. The release-revision
LICENSE source, size and SHA-256 are pinned in code and documented in
THIRD_PARTY_LICENSES entry 33 and `docs/deepfist-cw-backend.md`.
- [x] **DeepFist qualification.** The approval asked for a published
release (done, 2026-09-23) and Windows/Linux qualification: #5950,
#5953, #6103, and the Linux and Windows runs in Proof.
- [x] **DeepCW license:** settled by the 2026-09-14 approval.
- [x] **Linux build and test** (@skerker): done 2026-10-09 at
`a1b32197`, and the review round rebuilt and re-run 2026-10-13 at
`6d318a38`; see Proof.
- [x] **Windows build and test** (@skerker): done 2026-10-09 at
`a1b32197`, and the fix commit `f58a19ab` rebuilt 2026-10-10; see Proof.
- [x] **Apple Silicon:** the reviewer built ARM64 with Qt 6.12.0/ONNX
Runtime 1.29.0 and checked the TX-disabled demo bridge; see the review
validation above.
- [x] **Real-radio RX check** on a FLEX-8400: Linux and Windows,
2026-10-10 (see Proof).

## Design choices

- **A selector whenever a second decoder exists.** The selector, status
and Cancel/Retry from #5716 now compile under `HAVE_CW_RX_BACKENDS`,
defined with ONNX Runtime, instead of `HAVE_DEEPFIST`.
- **DeepCW output is coloured, never hidden by Sens**, like DeepFist:
one colour per committed chunk (1 − mean CTC confidence). With either
neural decoder, Sens, the locks and the search ranges are unavailable,
contact cards and received MQTT text come from ggmorse only, and
AetherSDR's Zero Beat is disabled (it needs ggmorse's pitch). A
FlexRadio's own Autotune is unaffected.
- **DeepCW emission.** The committer shows a character only once it is 5
s behind the live edge, so the model's corrections inside that window
are used: there is no hard 15 s reset, words are not cut at window
seams, and text already shown never changes. This replaces the prototype
loop the 2026-09-14 approval described and stands in for the follow-up
issue it asked for; I'll file one if a reviewer wants it. Against the
prototype loop: ARLD038 51.4 → 9.5 %, ARLP038 45.1 → 7.3 % CER
(K5PTB#1). The model loads on the backend's worker, never on
the GUI thread.
- **`DEEPFIST_MODEL_BASE_URL` is now an override:** empty selects the
published release, so a build directory that cached the old empty value
still downloads.
- **Intel Macs get ggmorse only**: the pinned ONNX Runtime has no
Intel-macOS build, and the code builds without the neural decoders
there.
- `AETHER_DEEPCW_MODEL_DIR` points at a local model for development,
like `AETHER_DEEPFIST_MODEL_DIR`.
- The commits separate cleanly (DeepCW engine · DeepCW backend ·
retiring the option · test fix · docs · review fixes) if you would
rather split DeepFist's enablement into its own PR.

## Constitution principle honored

Principle VIII. Principle V: the backend choice stays one key in the
existing `CwDecoder` document.

## Test plan

Test seams, all socket-free:
- `deepcw_rx_backend_test` (new): missing model → unavailable → Retry →
stop clears; cancel → Retry → stop clears.
- `deepcw_ctc_test` (new): DeepCW's greedy CTC decode (`ctcDecode`,
`greedyEmissions`) on constructed log-prob rows: repeats collapse, a
blank separates a repeated letter, both paths emit the same letters with
the same frames and confidence, a constant row offset changes nothing,
zero frames decode to nothing. No ONNX Runtime, weights or sockets.
- `deepfist_model_assets_test`: new per-asset source case (fetched from
its own source; non-HTTPS refused).
- `deepfist_cw_model_test`: its unavailable case now passes an empty
source, so it never reaches the network now that a real release URL is
the default.

Mutation checks: with `stop()`'s clearing removed, 2 failures (first
"stop clears Retry, detail and status"); with the per-asset URL ignored,
2 failures ("asset with its own source is fetched from that source",
"non-HTTPS asset source is refused"). Both restored and passing. For
`deepcw_ctc_test`: a blank that no longer resets the repeat (in
`ctcDecode`, then in `greedyEmissions`) and a confidence fixed at 1 each
fail it; restored and passing.

CI: no per-PR job installs ONNX Runtime, so neither the neural decoders
nor these tests compile in PR CI or in `full-suite.yml`. They compile in
the release workflows (tag / `workflow_dispatch`) and in
`system-libs-canary.yml` after merge. A maintainer dispatch of the
release workflows on this branch would be the strongest pre-merge
compile check. `deepcw_ctc_test` is the exception: it needs no ONNX
Runtime, so it compiles in PR CI's Linux and macOS builds and runs in
`full-suite.yml` after merge.

Local (Intel Mac, Qt 6.12, ONNX Runtime 1.23.2): full registered suite
at `2d7ce9ef` 681/682; the one failure, `unified_title_bar_test` ("Alt+F
still opens the File menu with the menu bar hidden"), compiles only
sources identical to `main` and fails the same way every run here.

- [x] Local build passes (`cmake --build build`): clean builds at
`d065dd4d` (Intel Mac), `a1b32197` (Linux, Windows), `f58a19ab`
(Windows) and `6d318a38` (Linux)
- [x] Behavior verified on a real radio if applicable — demo radio;
FLEX-8400 RX on Linux and Windows
- [x] Existing tests pass (CI) — green at `6d318a38`; the neural decoder
tests don't compile in PR CI (no ONNX Runtime) and ran locally on Linux,
Windows and Intel Mac
- [x] Reproduction steps documented if user-reported bug — n/a, feature

## Proof

Built and tested at `d065dd4d` (About read): clean build, the CW and
DeepFist tests above pass, and on the demo radio each decoder produces
text on the CW tone, with Zero Beat enabled only for ggmorse. `a1b32197`
is that commit plus a merge of `main` at `65b72a65` to resolve one docs
conflict with #6298; main's changes since the base touch help links,
docs and a CTR2 test, no CW code. The Linux and Windows runs below are
at `a1b32197`; `f58a19ab` adds only the Windows model-path fix from
@K5PTB's review (rebuilt and re-run on Windows). The head, `6d318a38`,
adds @rfoust's review round: a merge of `main` at `47aa4098` (one docs
conflict, `BUILD-OPTIONS.md`), the output-shape and WAV-chunk checks,
and `deepcw_ctc_test`; rebuilt and re-run on Linux, below.

**Intel Mac, ONNX Runtime 1.23.2 staged by hand, About `2d7ce9ef`
(2026-10-08):** demo radio (`DEMO-0001`), isolated settings, TX blocked:
- Selector lists ggmorse, DeepFist and DeepCW; each produced text on the
demo's CW tone.
- **Both downloads from upstream:** DeepCW `model.onnx` 15139839 bytes
`ef120799…`; DeepFist `deepfist.onnx` 13051998 `6d2d4e3d…`,
`deepfist.onnx.json` 1257 `840ceb8d…`, `LICENSE` 1068 `9ad70a9e…` (via
the per-asset source). All match the pins.
- **Persistence:** DeepCW selected → app closed and relaunched → DeepCW
restored, ready from the cache with no download.
- **Display delay**, demo CW tone (Morse L, 18 WPM, noise off), five
on/off cycles per decoder, tone off → last character shown:

| Decoder | Median | Range |
|---|---:|---:|
| ggmorse | 0.52 s | 0.26–0.77 s |
| DeepFist | 1.82 s | 1.55–2.83 s |
| DeepCW | 7.57 s | 5.95–7.81 s |

This is timing on clean synthetic CW, not accuracy. The user manual uses
these figures.

**Linux, head `a1b32197` (2026-10-09/10):** build + tests + demo smoke ·
FLEX-8400 receive run · CW decoder regression set —

[aethersdr-pr6300-linux-2026-10-10.zip](https://github.com/user-attachments/files/33274141/aethersdr-pr6300-linux-2026-10-10.zip)

- Build and tests: clean build with ONNX Runtime 1.27.0, 0 errors;
targeted tests 11/11 offline and 5/5 with the model; full suite 682/686
(2 skipped, 2 failures outside this PR's files). Demo smoke as above;
display delay medians ggmorse 0.51 s, DeepFist 2.04 s, DeepCW 6.36 s.
- FLEX-8400, receive only, live 40 m CW: each decoder selected in turn,
reported ready and decoded live signals; About `a1b32197`.
- Regression set, all three decoders at `a1b32197` on five recordings:
the four W1AW bulletin excerpts attached to #6103 (strong and weak
signal, with the bulletin text) and the 497 s take from #4817 (comment
5289844251). DeepFist's text and colour data are byte-identical to the
#6103 run; `tools/run.sh` in the bundle re-runs and re-scores
everything.

| Recording (UTC) | Signal | ggmorse | DeepFist | DeepCW |
|---|---|---:|---:|---:|
| ARLD038, 2026-09-24, 17 m | strong | 49.7 % | 13.9 % | 9.3 % |
| ARLD038, 2026-09-25, 17 m | very weak | 79.9 % | 50.9 % | 37.8 % |
| ARLP038, 2026-09-24, 15 m | strong | 35.7 % | 20.5 % | 7.3 % |
| ARLP039, 2026-09-26, 40 m | moderate | 61.2 % | 18.4 % | 11.2 % |

CER from `score_cer.py`: normalized text, semi-global alignment, edit
distance ÷ bulletin length. One sentence from each decoder; the number
in parentheses is that sentence's character errors (wrong + missing +
extra characters):

| Recording (UTC) | Bulletin text | ggmorse | DeepFist | DeepCW |
|---|---|---|---|---|
| ARLD038, 2026-09-24, 17 m, strong | `BARRY, KD6XU, IS QRV AS YJ0BP
FROM EFATE UNTIL EARLY OCTOBER.` | `BARRKE ENC MEW EXU, IS QRV AS AS
O06P SG FROM EFATE ANTIL EAE EA5L Y OCTOBEREEE EETIETETA` (39) | `BARRY,
KD6XU, IS QRV AS YJ0BP FROMEFATE ANTIL EARLY OCTOBER.` (2) | `BARRY,
KD6XU, IS QRV AS YJ0BP FROM EFATE ANTIL EARLY OCTOBER.` (1) |
| ARLD038, 2026-09-25, 17 m, very weak | `BARRY, KD6XU, IS QRV AS YJ0BP
FROM EFATE UNTIL EARLY OCTOBER.` | `Y I? ?V IEI TE` (50) | `BURRY,
KD6XUG3 IEQRV AS YJ,0BP FROM EA ANTIL RAY DESTOBER` (17) | `BARRY,
KD6XUW IS QRV AS YJ0BP FROM EFATE UNTIL EARAEY EIXOBER.` (6) |
| ARLP039, 2026-09-26, 40 m, moderate | `A CALM PERIOD IS EXPECTED TO
FOLLOW DURING THE LAST FEW DAYS OF THE MONTH.` | `A CALM INER EOEI IS E
IS EBEPENSEEED TX FGLLOW D?I PINNE EHE LAST FE D EIA3S NTF THE AE?NTH.`
(36) | `A CALG ANERIOD IS EXPECTED TO FOLLOW DURING THE LAST FEW DAYS OF
THE AONTH.` (4) | `A CALM PERIOD IS EXPECTED TO FOLLOW DSRING THE LAST
FEW DAYS OF THE MONTH.` (1) |

**Linux, head `6d318a38` (2026-10-13): @rfoust's review round** — 

[aethersdr-pr6300-review-round-linux-2026-10-13.zip](https://github.com/user-attachments/files/33395119/aethersdr-pr6300-review-round-linux-2026-10-13.zip)

- Both blockers reproduced first, on the unchanged head `f58a19ab`, with
an AddressSanitizer build of `deepcw_replay` (ONNX Runtime 1.27.0): the
`[1,10000,1]` output model overflows in both inference paths (`decode()`
→ `ctcDecode`, and `inferLogProbs`), and the 1-byte `fmt` WAV overflows
in `readWav()`.
- At `6d318a38` those two and seven more malformed inputs (output with
41 classes, rank 1, rank 2, int64 or zero frames; a `data` length past
the end of the file; a file cut inside `fmt`) fail closed with one line
and no AddressSanitizer report. The real model on a recorded bulletin
decodes byte-identically before and after. The inputs are constructed;
the script that makes them is in the bundle.
- Clean build, 0 errors; full suite 799/804 (4 skipped; 1 failure,
`pgxl_panel_test`, outside this PR's files, failing the same way in
earlier runs on this machine). `deepcw_ctc_test` passes.
- Regression set at `6d318a38`: all 19 output files byte-identical to
`a1b32197`, so the tables above hold.

**Recorded take from #4817: DeepCW's old loop vs the new one, at
`6d318a38`.** The 2026-09-14 approval on #4817 asked for the 497 s take
(comment 5289844251) to be re-run after any change to the decoding loop.
This PR replaces the prototype's loop with `DeepCwCommitter`.
`deepcw_replay` plays the same audio through the old loop (`-grow`, as
the panel showed it), the new one (`commit 5`, what the app runs) and
each 15 s window decoded once (`-window`), on the take and on the four
W1AW recordings above. CER as computed above:

| Recording (UTC) | Signal | Old loop, `-grow` | New loop, `commit 5`
(this PR) | Whole 15 s windows, `-window` |
|---|---|---:|---:|---:|
| ARLD038, 2026-09-24, 17 m | strong | 51.6 % | 9.3 % | 16.4 % |
| ARLD038, 2026-09-25, 17 m | very weak | 69.2 % | 37.8 % | 41.5 % |
| ARLP038, 2026-09-24, 15 m | strong | 44.9 % | 7.3 % | 14.5 % |
| ARLP039, 2026-09-26, 40 m | moderate | 48.0 % | 11.2 % | 17.6 % |

The same sentences as the table above, in DeepCW's three views
(character errors in parentheses):

| Recording (UTC) | Bulletin text | Old loop, `-grow` | New loop,
`commit 5` (this PR) | Whole 15 s windows, `-window` |
|---|---|---|---|---|
| ARLD038, 2026-09-24, 17 m, strong | `BARRY, KD6XU, IS QRV AS YJ0BP
FROM EFATE UNTIL EARLY OCTOBER.` | `BAKTHXU, ISQRV ASYWBPFROM
EFAETILEARAOBER.` (23) | `BARRY, KD6XU, IS QRV AS YJ0BP FROM EFATE ANTIL
EARLY OCTOBER.` (1) | `BARRY, KT HXU, IS QRV AS YJ0BP FROM EFATE ANTIL
EARLY OCT OBER.` (5) |
| ARLD038, 2026-09-25, 17 m, very weak | `BARRY, KD6XU, IS QRV AS YJ0BP
FROM EFATE UNTIL EARLY OCTOBER.` | `1, KD6XUR0BP FROM EFATUNTEY DIXODN5`
(33) | `BARRY, KD6XUW IS QRV AS YJ0BP FROM EFATE UNTIL EARAEY EIXOBER.`
(6) | `BARRY , KD6XUBW IS QRV AS YJ 0BP FROM EFAT UNTIL EARAE Y
EITOBER.` (10) |
| ARLP039, 2026-09-26, 40 m, moderate | `A CALM PERIOD IS EXPECTED TO
FOLLOW DURING THE LAST FEW DAYS OF THE MONTH.` | `A CALM WS EXPECTED TO
FOLLEDHREFEW DAYSOFTHEMONTH` (28) | `A CALM PERIOD IS EXPECTED TO FOLLOW
DSRING THE LAST FEW DAYS OF THE MONTH.` (1) | `A CALM PERIOD IS EXPECTED
T O FOLLOW DS RING THE LAST FEW DAYS OF THE MONTH.` (3) |

The take has no reference text, so it cannot be scored. Its full decode
in all three views, including the `QRQ` the approval quoted, is in the
evidence bundle. The stretch below, from near the end of the take, is
consistent with the W1AW results above: the new loop improves DeepCW's
copy over the old one.

| View | Same stretch, 497 s take |
|---|---|
| Old loop, `-grow` | `OKBERTNYBADNNDKTNXFERQSOTCARE NB3
ANDWIALOONMRUAGN` |
| New loop, `commit 5` (this PR) | `OKBERT QSB PRETTYBADNOWHARDCOPY
TNXFERQSO TAKE CARE 73 AND WILL LOOKFORUAGN` |
| Whole 15 s windows, `-window` (`·` = window boundary) | `OKBERT QSB
PRETT · YBADNOWHARDCOPY · TNXFERQSO TAKE CARE N · B3 AND WILL LOOKFT ·
MRUAGN` |

`BRAND NEW`, `ALINCO` and `339`, quoted in the approval, are not in this
recording: they were decoded in the live pane before the recorder
started, and comment 5289844251 labelled that pane "full take". No
decoder prints them when the WAV is replayed. `QRQ` is decoded from the
recording by DeepCW and DeepFist.

**Windows, head `a1b32197` (2026-10-09/10):** build + tests + demo smoke
· FLEX-8400 receive run · CW decoder regression set · non-ASCII model
path (before) —

[aethersdr-pr6300-windows-2026-10-10.zip](https://github.com/user-attachments/files/33276605/aethersdr-pr6300-windows-2026-10-10.zip)

**Windows, head `f58a19ab` (2026-10-10):** the model-path fix — build,
the non-ASCII path after the fix, regression set again —

[aethersdr-pr6300-windows-f58a19ab-2026-10-10.zip](https://github.com/user-attachments/files/33276607/aethersdr-pr6300-windows-f58a19ab-2026-10-10.zip)

- Build and tests at `a1b32197`: clean MSVC build with ONNX Runtime
1.27.0 (the copy sherpa-onnx bundles), 0 errors; targeted tests 11/11
offline and 5/5 with the model; full suite 676: 655 pass, 4 skipped, 17
fail. 10 of the 17 also fail on `main` on this machine; the other 7 are
line-ending or offscreen-layout tests whose sources this PR does not
touch. Demo smoke as on Linux; display delay medians ggmorse 0.51 s,
DeepFist 2.28 s, DeepCW 6.32 s.
- FLEX-8400, receive only, live 40 m CW, About `a1b32197`: each decoder
selected in turn, reported ready and decoded live signals.
- Regression set: all 15 decoded texts are byte-identical to Linux, so
every number in the tables above holds on Windows. DeepFist's per-letter
confidence values differ from Linux in 10 of 9,105, by 0.0001 (same
letters). At `f58a19ab` all 30 output files are byte-identical to the
`a1b32197` Windows run.
- DeepCW model in a folder named `Modèle` (`AETHER_DEEPCW_MODEL_DIR`):
at `a1b32197` "Model load failed" (ONNX Runtime received the path as
`Modᅢᄄle`); at `f58a19ab` "DeepCW ready". The same file in an ASCII
folder loads at both heads.

**Intel Mac, no ONNX Runtime, `a1b32197` (2026-10-09):** configure
prints `ONNX Runtime not found` and builds without the neural decoders.
On the demo radio, with a saved `deepcw` choice from a runtime build: no
decoder selector, ggmorse decodes the tone, Zero Beat is enabled, and
the stale choice causes no error or crash.

## Checklist

- [x] Commits are signed (`docs/COMMIT-SIGNING.md`)
- [x] No new flat-key `AppSettings` calls — the decoder choice is a
value in the existing `CwDecoder` document
- [x] Code is clean-room — not decompiled, disassembled, or
reverse-engineered from a proprietary binary (Principle IV); DeepCW's
engine is an attributed port of AGPL-3.0 source, as approved on #4817
- [x] All meter UI uses `MeterSmoother` — n/a, no meters
- [x] Documentation updated: `docs/user/docs/cw-decoder.md` (choosing a
decoder, delays, downloads; #6298's decoder-pane screenshot predates the
selector), `BUILD-OPTIONS.md`, `docs/deepfist-cw-backend.md`,
THIRD_PARTY_LICENSES. `CHANGELOG.md` untouched.
- [x] Security-sensitive changes reference a GHSA if applicable — n/a

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

Generated with OpenAI Codex (GPT-6 Astra)

---------

Co-authored-by: K5PTB <274291579+K5PTB@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: jensenpat <patjensen@gmail.com>
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.

DeepFist: colour each decoded letter by the model's own confidence

4 participants