Skip to content

RFC: DeepCW neural CW decoder as an optional second CW backend #4817

Description

@K5PTB

Summary

Add DeepCW — a neural (ONNX CTC) Morse decoder — as an optional second CW
decode backend alongside the existing ggmorse DSP decoder
, downloaded on
demand. This is an RFC to gauge interest and settle the open questions before
proposing it for merge; a working prototype exists (see below).

Motivation

The client-side CW decoder (ggmorse) is a hand-tuned DSP/Goertzel decoder. It's
excellent on clean, steady signals but degrades on the things hams actually hit:
QSB, drift/chirp, adjacent-signal QRM, and weak/near-noise signals. Neural CW
decoders (e.g. e04/deepcw-engine) claim large weak-signal gains — roughly
error-free down to ~-4 dB SNR and usable to ~-10 dB in the author's figures.
If that holds on real air, a neural backend would meaningfully extend copy in
exactly the conditions where operators want help most.

Proposal

  • Add a neural CW backend using e04/deepcw-engine's trained ONNX model
    (a small CTC CNN over a log-magnitude spectrogram; ~15 MB).
  • Download-on-demand + SHA-256-pinned, mirroring the ASR model manager — no
    weights are shipped, so release binaries stay pure GPLv3 and the model only
    attaches when a user opts in.
  • Reuse the in-tree ONNX Runtime (already present for the ASR features) —
    no new heavyweight dependency.
  • Selectable per operator: keep ggmorse (DSP) as the default; DeepCW is opt-in.

Design (as prototyped)

  • DeepCwEngine — Qt-free: resample to 3200 Hz → spectrogram (256-pt FFT,
    hop 48, Hann, 400-1200 Hz → 65 bins, log1p) → Ort::Session → greedy CTC.
    A faithful C++ port of the reference decode_morse.py.
  • CwDecoder gains a backend switch (ggmorse/DeepCW); the DSP path is unchanged.
  • Header toggle in the CW panel; download orchestrated at the GUI layer via the
    existing AsrModelManager (source failover + hash verify + atomic rename).
  • The model also yields a per-decode confidence (drives the sensitivity
    filter) and a dominant-tone pitch (feeds zero-beat + a pitch readout).

What we learned from the prototype (honest, so far)

  • Functional parity on clean/moderate CW — DeepCW and ggmorse both decode
    clean signals essentially perfectly across 15-25 WPM.
  • Synthetic weak-signal tests did NOT reproduce the headline advantage — they
    actually favored ggmorse.
    With broadband white noise, ggmorse's narrow
    Goertzel filter rejects almost all of it, while the model analyzes an 800 Hz
    band; for a steady tone in white noise the DSP is near-optimal. A pure
    sine + Gaussian noise is also out-of-distribution for the net. So the model's
    real edge — if any — is on what synthetic tests omit: QSB, drift, QRM, and
    imperfect human sending. The honest arbiter is real off-air CW judged by a
    CW operator
    , which is in progress.
  • The model's confidence is well-calibrated — where the two decoders
    disagree, it's on the model's own low-confidence output, which the sensitivity
    filter can suppress. That's a useful property in its own right.
  • Fixed 400-1200 Hz analysis band. DeepCW only "hears" CW whose pitch sits
    in that band. In practice that's fine — it pairs with the existing VFO-flag
    zero-beat (centers the note ~600 Hz); ggmorse's own range is only 300-1200 Hz.

Licensing

e04/deepcw-engine's model is AGPL-3.0; AetherSDR is GPL-3.0. GPLv3 §13
explicitly permits combining GPLv3 + AGPLv3; the only added obligation is AGPL's
network clause, which affects someone hosting the combined work as a service —
desktop users are unaffected, and AetherSDR source is already public.
Keeping the model download-on-demand means shipped binaries carry no AGPL
code until a user opts in.

Open questions (what this RFC is really asking)

  1. Is the weak-signal advantage real enough on-air to justify a second backend?
    (Gathering real-signal comparison now — happy to bring numbers back here.)
  2. Is a ~15 MB downloaded AGPL model acceptable as an optional component?
  3. UI: a decoder toggle in the CW panel, a separate compare applet, or both?
  4. Concurrency cost on low-end hardware (e.g. RPi 5) if an operator runs both
    decoders at once for comparison — acceptable, or gate it?

Prototype status

A working prototype (bottom-panel DSP/Neural toggle + a temporary side-by-side
"CW Neural" compare applet + download-on-demand) is built and running on macOS,
Windows, and Raspberry Pi 5. Not proposing merge yet — this RFC is to align on
whether and how before that.

Activity

  1. added
    claude-activeAetherClaude is actively working on this issue
    CWCW keying, decode, and operation
    GUIUser interface
    maintainer-reviewRequires maintainer review before any action is taken
    on Aug 7, 2026
  2. aethersdr-agent commented on Aug 7, 2026

    @aethersdr-agent
    Contributor

    Triage: RFC, well-scoped → maintainer-review

    This is a design proposal with a working prototype, not a defect, so there's no root cause to find. What I can do is check the RFC against the tree it would land in and flag the integration points that will decide whether this is a clean drop-in or a refactor. Findings below, then a recommendation on each of your four questions.

    What the tree already gives you (the RFC's premises check out)

    • ONNX Runtime is genuinely free. CMakeLists.txt:2613-2660 discovers ORT from third_party/onnxruntime (or brew/apt/pkg-config) and defines HAVE_ONNX. All three release workflows already run the setup script — appimage.yml:234, macos-dmg.yml:252, windows-installer.yml:104 — so a neural CW backend ships enabled on every published binary with no new dependency. Correct as written.
    • The model-manager reuse is real. AsrModelManager (src/asr/AsrModelManager.h) already does ordered source failover, pinned SHA-256, background hashing off the UI thread, and .part → atomic rename. That is exactly the download-on-demand contract you want.
    • r8brain is vendored (third_party/r8brain) — relevant below.

    Integration points that need attention before a merge PR

    1. HAVE_ONNX is optional, and ci.yml doesn't install ORT. The main CI matrix builds with ORT absent, so HAVE_ONNX is undefined there. DeepCwEngine must compile and link as an inert stub in that configuration — follow src/asr/SileroVad.cpp, which keeps every Ort:: member and call behind #ifdef HAVE_ONNX and returns false from load() otherwise. If the header declares Ort::Session unconditionally, the whole default CI build breaks, not just the CW feature. The UI toggle needs the matching compile-time gate so it doesn't offer a backend that can't exist.

    2. Confidence must be converted to a cost, or the sensitivity filter inverts. CwDecoder::textDecoded(text, cost) is a ggmorse cost function — lower is better. CwDecoder.cpp:221 gates on stats.costFunction < 1.0f, MainWindow.cpp:2988 suppresses with cost >= m_cwDecoderApplet->cwCostThreshold(), and CopyAssistPanel.h:18 explicitly documents the CW panel's coloring as the inverse of a confidence scale. A model confidence (higher = better) handed straight to that signal makes the sensitivity slider filter out the good decodes and colour the bad ones green. Emit 1.0f - confidence (or add a distinct signal), and say which in the PR description.

    3. 24 kHz → 3200 Hz is a 7.5× non-integer decimation. feedAudio() (CwDecoder.cpp:145-166) receives 24 kHz stereo float32 (note the header comment at CwDecoder.h:54 says int16 — it's stale) and downmixes to mono int16 for ggmorse. Your 3200 Hz path needs a properly filtered resample, not sample dropping/linear interpolation: anything above 1600 Hz folds straight back into the 400-1200 Hz analysis band, which is where the whole model lives. Use the vendored r8brain rather than rolling one. Also worth stating whether the neural path should keep the int16 round-trip at all — it wants float, so tapping the float32 input directly avoids a needless quantization.

    4. The TX-side decoder should stay ggmorse-only. There are two instances, m_cwDecoder and m_cwDecoderTx (MainWindow.cpp:1640-1651, 7686-7770). The TX one is driven by setKnownParameters(pitchHz, speedWpm) (CwDecoder.cpp:98-127), where pitch and WPM come from PhoneCwApplet, both locked — a self-generated sidetone at exactly known parameters is the best case for the Goertzel path and has no neural upside. A backend switch that applies to both instances would regress #2417's TX decode for no gain. Scope the switch to the RX instance.

    5. Catalog/manager naming is ASR-shaped. AsrModelTier / AsrModelCatalog / AsrModelFamily (src/asr/AsrModelCatalog.h) are documented as the Whisper/sherpa registry for RFC #4333, and AsrModelFamily enumerates inference engines for ASR backends. Threading a CW model through them either abuses the type or forces a rename. Cleanest split: keep the download/verify machinery (rename the manager to a neutral ModelManager, or add a thin CW-facing wrapper) and give DeepCW its own small catalog entry rather than a fake ASR tier. This is the one piece of the prototype I'd expect to change most before merge.

    6. Config goes in the existing blob. Constitution Principle V, and CwDecodeSettings (src/gui/CwDecodeSettings.h) is already the nested AppSettings["CwDecoder"] object holding rx/tx/fontPx/panelHeight. The backend selection is a new key in that object ("backend": "dsp"|"neural"), defaulting to dsp — not a new flat key, and not a second blob.

    Licensing — one correction

    GPLv3 §13 does permit the combination, and download-on-demand does keep the weights out of the binaries. But the RFC's stronger claim — "no weights are shipped, so release binaries stay pure GPLv3" — doesn't hold for the code. DeepCwEngine is described as "a faithful C++ port of the reference decode_morse.py," and a faithful port of AGPL-3.0 source is a derivative of it. That AGPL-derived code is compiled into every shipped binary regardless of whether a user ever downloads weights. This is still fine for a GPLv3 project via §13, but it changes what has to be true at merge:

    • THIRD_PARTY_LICENSES needs an AGPL-3.0 entry for e04/deepcw-engine covering the ported code, not just the model (Constitution "Technology Constraints" requires the update for vendored third-party material).
    • The port's provenance should be stated plainly in the file header (ported from <repo>@<commit>, AGPL-3.0) — same discipline as the clean-room rule in Principle I, applied to attribution.
    • If the fallback download source is an AetherSDR release asset (as the ASR catalog does at AsrModelCatalog.cpp:24), we become a distributor of the AGPL weights too. Not a blocker, just be deliberate about it rather than discovering it later.

    If you'd rather not carry AGPL-derived code in the binary at all, the alternative is a from-scratch implementation written against the model's published input/output contract (spectrogram shape, CTC alphabet) instead of its source — the spec is small enough that this is plausible, and it would keep the shipped tree GPLv3-only with AGPL confined to the downloaded weights.

    On your four questions

    1. Weak-signal advantage — your own negative synthetic result is the most credible thing in the RFC and the right reason not to merge yet. Agreed that a steady tone in white noise is near-optimal for a narrow Goertzel and out-of-distribution for the net; that test can't settle it either way. Real off-air A/B judged by a CW op is the arbiter. Suggest recording raw off-air audio and replaying it into both backends offline, so the comparison is reproducible by a reviewer instead of a live listening impression.
    2. ~15 MB AGPL optional download — maintainer call, but the mechanism is already precedent (ASR weights, DeepFilterNet, NVIDIA AFX). The AGPL question is about the ported code, per above; that's the part worth deciding explicitly.
    3. UI — the toggle in the CW panel is the shippable surface. A permanent side-by-side compare applet doubles the DSP cost and the MQTT publish path (publishCwDecodeMqtt, MainWindow.cpp:2983) for a feature most operators use once. Keep the compare mode as the temporary evaluation harness it is, and don't carry it into the merge PR unless the maintainer wants it.
    4. Concurrency on low-end hardware — falls out of (3): if compare mode doesn't ship, running both at once isn't a supported state and needs no gate. If it does ship, gate it, since a Pi 5 running two decoders plus the panadapter is exactly where the audio thread starts missing deadlines.

    Handoff

    Nothing to fix in the current tree — this is a go/no-go on a proposal. What's known: ORT is present in all release builds so the dependency claim is sound; the model-manager reuse is sound; the prototype's own weak-signal data does not yet support the motivation, and the author says so. What a merge PR would have to change from the prototype: stub out cleanly under !HAVE_ONNX, invert confidence into the existing cost signal, filtered 24k→3.2k resample via r8brain, restrict the switch to the RX decoder, split the CW model catalog out of the ASR-named types, store the backend key in AppSettings["CwDecoder"], and add the AGPL-3.0 attribution for the ported code. My read: the engineering is tractable and the licensing is workable; the open item is whether (1) produces evidence, and that should gate aetherclaude-eligible.


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


    📍 Citation health check (Foundry Constitution Principle I): 1 resolved, 12 unresolved against /Users/aetherclaude/workspace/AetherSDR at origin/main.

    Unresolved citations (12)
    Citation Reason
    AsrModelCatalog.cpp:24 file not found in workspace
    CopyAssistPanel.h:18 file not found in workspace
    CwDecoder.cpp:145-166 file not found in workspace
    CwDecoder.cpp:221 file not found in workspace
    CwDecoder.cpp:98-127 file not found in workspace
    CwDecoder.h:54 file not found in workspace
    MainWindow.cpp:1640-1651 file not found in workspace
    MainWindow.cpp:2983 file not found in workspace
    MainWindow.cpp:2988 file not found in workspace
    appimage.yml:234 file not found in workspace
    macos-dmg.yml:252 file not found in workspace
    windows-installer.yml:104 file not found in workspace
  3. K5PTB commented on Aug 7, 2026

    @K5PTB
    ContributorAuthor

    Correcting the licensing claim in the RFC — thanks for catching this.

    The RFC's stronger claim — "no weights are shipped, so release binaries stay pure GPLv3" — is wrong and I'm retracting it. You're right: DeepCwEngine is a C++ port of the reference decode_morse.py, and a faithful port of AGPL-3.0 source is a derivative of it. So AGPL-derived code is compiled into every binary regardless of whether a user ever downloads the weights. The download-on-demand design keeps the weights out of the binary; it does not make the tree "pure GPLv3."

    This is still fine for a GPL-3.0 project via GPLv3 §13 — the only added obligation is AGPL's network clause, which affects hosting the combined work as a network service (desktop users unaffected, and our source is already public). What it requires is honest attribution, which I've taken as the resolution rather than a clean-room rewrite:

    • THIRD_PARTY_LICENSES now carries an AGPL-3.0 entry for e04/deepcw-engine covering the ported code (not just the model).
    • DeepCwEngine.{h,cpp} carry a provenance header (ported from decode_morse.py, AGPL-3.0).

    The clean-room alternative (reimplement from the published I/O contract — spectrogram shape + CTC alphabet — rather than the source, keeping the shipped tree GPLv3-only) stays on the table if a maintainer would prefer AGPL confined strictly to the downloaded weights; I went with attribution as the lower-risk, faithful-to-the-reference option for the prototype.

  4. self-assigned this
    on Aug 7, 2026
  5. added theissue type on Aug 11, 2026
  6. moved this from Inbox to In Progress in AetherSDR Feature Deliveryon Aug 11, 2026
  7. 2 remaining items

  8. skerker commented on Aug 14, 2026

    @skerker
    Contributor

    @K5PTB A follow-up observation from looking at where e04's own decoder stack has gone since the engine the prototype ports: the web app (github.com/e04/web-deep-cw-decoder) has evolved past the single-model pipeline, and several of the additions land exactly on the harness losses measured in the 08-13 A/B.

    Model family. The app now ships four models: wide EN (the one the prototype's pipeline matches), a narrow EN variant (~50 Hz effective slice, 5 spectrogram bins), a dedicated CW-detection model, and a Japanese model with kana vocabulary (src/const.ts). Both front-ends use 12.5 Hz bins, so the prototype's spectrogram (3200 Hz / FFT 256) and the web app's (9600 Hz / FFT 768) are the same resolution over different spans.

    Pileup ("skimmer") mode. Multi-signal decoding is a three-stage design, not one model scanning the band: the detection model finds active carriers in 100–2000 Hz; a tracker locks up to 5 simultaneous signals (lock/release at −10 dB SNR); each locked track gets its own narrow-model decoder over an 8 s window with a 1.5 s text hold (usePileupDetection.ts, usePileupDecode.ts, constants in const.ts).

    User-adjustable parameters (all persisted): mode (normal / pileup / benchmark), filter width (100/150/250/500/800 Hz), filter center (set by clicking the scope), decode window (6/12/18/30 s), language (EN, EN+JA), inference backend, audio in/out devices. The prototype exposes rx/tx toggles, ggmorse↔deepcw backend, font and panel size (src/gui/CwDecodeSettings.h); the decode-relevant knobs — 5 s floor, 2 s hop, 15 s cap, 400–1200 Hz band — are compile-time constants in CwDecoder.cpp / DeepCwEngine.h.

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

  9. rfoust commented on Sep 14, 2026

    @rfoust
    Collaborator

    I propose expanding this RFC to support multiple selectable CW decoder backends, with DeepFist joining DeepCW as an optional neural backend alongside the default ggmorse decoder.

    Each neural model would download on first use, with pinned size/SHA-256 verification and local caching. Only the selected decoder would run. Users would not need Python, additional installers, or manual model setup.

    We have a working local DeepFist prototype using a pinned ~13 MB ONNX model from the public Lyra 0.24.0 release. Its metadata and dated source identify an MIT-licensed revision, and Lyra’s pinned documentation identifies the model as MIT. This applies to that specific artifact, not necessarily newer DeepFist releases. We propose hosting the verified files as versioned AetherSDR GitHub release assets, outside the application bundle.

    We also compared all three pipelines using identical audio and reference text across 30 cases. Lower character error rate is better.

    Test ggmorse DeepFist DeepCW prototype
    Clean synthetic 1.96% 2.94% 37.25%
    White noise, +10 dB 14.22% 2.94% 37.25%
    White noise, 0 dB 16.18% 2.94% 38.24%
    Fading plus noise 69.12% 5.39% 41.67%
    ARRL 10 WPM, Farnsworth 60.96% 6.80% 30.48%
    ARRL 15 WPM 4.03% 6.04% 34.98%
    ARRL 20 WPM 0.99% 3.43% 43.87%

    The DeepCW results measure the prototype’s streaming output, not standalone model accuracy. We used the unmodified engine from f6205355, its exact pinned model, and an offline adapter reproducing its streaming emission logic. Dropped revisions substantially affect the scores: in one clean case, the final inference decoded the entire phrase correctly, while accumulated output retained errors because corrections weren’t displayed. This matches the behavior already documented in this discussion.

    These are preliminary results. The synthetic set is small, DeepFist’s adaptive timing was tuned against it, and the ARRL recordings are clean practice files rather than off-air captures. Training overlap is unknown. Correcting DeepCW’s streaming output and evaluating all three against transcribed off-air recordings remain necessary before drawing conclusions about relative model quality.

    The proposed expansion would keep decoder selection, configuration, and model acquisition consistent while allowing each backend its own processing and supported controls. DeepFist has local macOS ARM64 build, automated-test, and native UI evidence; Windows/Linux qualification and public-download verification remain outstanding. No implementation PR or model release has been published.

  10. ten9876 commented on Sep 14, 2026

    @ten9876
    Collaborator

    Approved — as a backend framework, not a one-off switch

    The motivation holds up, the prototype has already absorbed the triage feedback, and @rfoust's expansion is the right shape. Approving the broader scope: a pluggable CW decode backend interface with ggmorse as the permanent default, not a single ggmorse↔DeepCW toggle. DeepCW lands first — it is the one with a rebased prototype, on-air data, and builds on four platforms. DeepFist joins through the same interface without a new RFC, once it has a published model release and Windows/Linux qualification; the framework is approved now precisely so that lands as an implementation, not another design round.

    I re-verified the triage's citations against main — all twelve flagged unresolved do resolve; the checker was pointed at a workspace that didn't have the tree. CwDecoder.{h,cpp} is at src/core/, not src/gui/, which is what the path check missed. The cost gate is CwDecoder.cpp:240 (stats.costFunction < 1.0f), the MQTT suppression MainWindow.cpp:3343, the CopyAssist inverse-coloring note CopyAssistPanel.h:18, and the ORT setup step windows-installer.yml:175 (not :104). Everything the triage concluded from them stands.

    The four open questions

    1. Is the weak-signal advantage real enough on-air? — Yes.

    @skerker's 40 m work is the evidence the RFC asked for and it is the only evidence in this thread taken off air. The 497 s recorded take at 7.019 MHz is the one that decides it: ggmorse produced hash while DeepCW emitted coherent QSO content, and the lexical-coverage proxy (24 % vs 36 %) understates the difference in kind — DeepCW recovered BRAND NEW, ALINCO, 339, QRQ; ggmorse recovered accidental fragments. That both engines agree where both partially copied (the RST region, OK, the rig sentence) is a real correctness signal on each side.

    The behavior after the signal faded matters as much as the copy: ggmorse's speed tracker ran to 59 WPM on an empty band, while DeepCW emitted six characters flagged red by its own confidence. A calibrated confidence channel that marks its own hallucinations is a feature in its own right, independent of the CER question, and ggmorse has nothing equivalent.

    @rfoust's table does not contradict this. It scores the prototype's streaming emission, and you say so plainly — the clean-synthetic case where the final inference decoded the whole phrase correctly while the accumulated output kept the errors is the whole story of that 37 %. Noted and separated below.

    2. Is a ~15 MB downloaded AGPL model acceptable? — Yes, with one constraint.

    The mechanism is settled precedent (ASR weights, DeepFilterNet, NVIDIA AFX). The constraint is on hosting: upstream sources only, no AetherSDR release-asset mirror — for either model. The ASR catalog's mirror-as-failover pattern is not the precedent to copy here. We take a redistribution obligation with it, and I don't want to carry one for AGPL weights we didn't train. Keep the SHA-256 pinning and the ordered-failover machinery; the ordering just won't include us. If upstream disappears, the backend degrades to unavailable and ggmorse carries on — acceptable for an opt-in feature.

    3. UI — toggle only.

    The CW-panel backend selector merges. CwNeuralApplet.{h,cpp}, the AppletPanel/PanadapterApplet compare wiring, and the second m_cwDecoderNeural instance stay on the prototype branch as the evaluation harness they are. It is the right tool for the A/B work in this thread and the wrong thing to carry in the tree — it doubles the DSP cost and the publishCwDecodeMqtt() path for something most operators use once.

    4. Concurrency on low-end hardware — moot, by (3).

    Only the selected backend runs. Running two at once stops being a supported state, so there is nothing to gate. @skerker's measurement (under 5 % of one core, full-window re-decodes well inside the 2 s hop) says a single neural backend is not the problem anyway.

    Licensing — attribution, not a clean-room rewrite

    @K5PTB's retraction is correct and the resolution he already shipped is the one I want: the THIRD_PARTY_LICENSES entry covering the ported code as well as the weights, and the provenance header on DeepCwEngine.{h,cpp}. GPLv3 §13 permits the combination; the network clause does not bind desktop users and our source is public. I am not requiring the clean-room reimplementation. It would keep the shipped tree GPLv3-only, but it trades a faithful port for a from-scratch one against a contract we'd be inferring — more risk of silent accuracy drift than the licensing question is worth, and the attribution obligation is one we can meet honestly.

    The existing entry (item 26) already states both obligations separately and correctly. Drop the download source line's implication of any AetherSDR-hosted copy, per (2).

    Merge-PR conditions

    The prototype already satisfies most of the triage's list — verified on f6205355:

    • ✅ !HAVE_ONNX stub. Every Ort:: member is behind the guard and decode() returns empty. ci.yml installs no ORT, so this is what keeps the default CI build alive; it holds.
    • ✅ Cost, not confidence. cost = 1.0f - conf before emit textDecoded(). The sensitivity slider filters the right direction.
    • ✅ Anti-aliased 24k→3.2k. r8brain via the in-tree Resampler, continuous across segment resets, float path throughout — no int16 round-trip on the neural side.
    • ✅ RX-only. reconcileCwBackend() touches m_cwDecoder only; m_cwDecoderTx never leaves ggmorse. Right call — a self-generated sidetone at a known pitch and WPM is the Goertzel path's best case and has no neural upside. Don't let this regress when the switch becomes a general interface.

    Three things still to do:

    1. Rename the download machinery to a neutral ModelManager; give CW its own catalog. deepCwModelTier() returning an AsrModelTier is exactly the type abuse the triage flagged, and with DeepFist coming it gets worse, not better. Mechanical rename of the ASR call sites, no behavior change.

    2. Decouple model download from AETHER_ASR_ENABLED — this is the blocker in the list. Today the whole download path in reconcileCwBackend() sits inside #ifdef AETHER_ASR_ENABLED, and the #else silently forces the operator back to ggmorse. Our Windows CI check builds ASR-OFF, and any ASR-OFF distribution build makes the neural backend unreachable with no diagnostic. CW decode has nothing to do with speech recognition; the flag coupling is an artifact of borrowing the ASR manager, and it disappears with (1).

    3. Backend key in the existing blob. Already correct — "backend" inside AppSettings["CwDecoder"], defaulting to ggmorse, per Principle V. Keep it that way when the key has to hold three values instead of two; prefer a string the framework can extend ("ggmorse"/"deepcw"/"deepfist") over a bool.

    The streaming harness — not a merge blocker

    decodeLoopDeep() adopts a divergent re-decode as its new baseline and emits nothing (emitted = text on the else branch), so corrections never reach the panel. That is the mechanism behind @skerker's visible holes and behind the clean-synthetic CER in @rfoust's table. The cadence — 5 s floor, 2 s hop, 15 s hard reset — is the other half of why it reads as chronically behind.

    I'm not gating merge on fixing it. The feature is opt-in, ggmorse stays the default, and the weak-signal case where DeepCW wins is already strong enough to be worth putting in operators' hands. Blocking it until the harness is right means the people who could test it best can't reach it.

    Conditions on landing it that way: the emission behavior is documented in decodeLoopDeep() as a known prototype limitation (it partly is), and it gets a follow-up issue for overlap-merge plus revision-aware emission — filed with the merge PR, not "later". And please carry @skerker's recorded take forward as the regression case; that zip is the closest thing we have to a fixture, and any harness change should be re-run against it.

    One caveat for anyone quoting numbers from this thread in the meantime: until the harness emits corrections, no CER figure here measures a model. They measure the emission logic. That applies to the DeepCW column of the comparison table and to any future DeepFist-vs-DeepCW claim made through the same path.

    On the wider e04 stack

    @skerker's survey of the web app is the right map for where this goes after the harness work — the narrow EN model, the detection model, and the three-stage pileup design all land on losses measured here, and the user-adjustable decode window (6/12/18/30 s) is the direct answer to the cadence complaint. None of it is in scope for the first merge. It is a good argument for getting the backend interface right now, since a second model from the same family with a different band and window is exactly the thing the interface has to absorb without another refactor.

  11. skerker commented on Sep 28, 2026

    @skerker
    Contributor

    I've been testing DeepFist on Linux and Windows and working on some improvements to it. Now that the model has a public release, two things look pending before release builds can enable it: the license, and whether the qualification so far is sufficient, between the testing in @rfoust's PR #5716 (macOS) and mine in #5950 and #5953 (Linux and Windows). I'd like feedback from the maintainers on both.

    N9BC has published the DeepFist model as a release: exp27_bt-champion (2026-09-23). Its deepfist.onnx and deepfist.onnx.json match the sizes and SHA-256 hashes pinned in src/core/deepfist/DeepFistModelAssets.cpp.

    One open question before release is the license. The release has no LICENSE file, and N9BC's repository moved from MIT to GPL-3.0-or-later on 2026-07-20, after this checkpoint was created (2026-07-15). The pinned LICENSE is MIT. Which license covers this checkpoint, and does N9BC need to confirm it before release builds can enable DeepFist?

    Testing so far (including on-air), with the model loaded from a local directory: Linux (#5950, off-air W1AW replay) and Windows (#5953: MSVC build, the 9 DeepFist/CW tests, live decoding on a FLEX-8400).

    I'm also working on two improvements: the activity threshold (#5953) and per-letter confidence coloring. These could be follow-up PRs, but I think a decision or feedback about release based on the licensing and qualification is the next step.

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

  12. skerker commented on Oct 8, 2026

    @skerker
    Contributor

    @ten9876 Draft PR #6300 adds DeepCW as the third CW receive decoder and makes both neural decoders standard features. These are the decisions it likely needs before it can merge:

    1. Standard features. The PR retires ENABLE_DEEPFIST_EXPERIMENT. DeepFist and DeepCW build wherever ONNX Runtime is found (Apple Silicon, Linux, Windows; Intel Macs keep ggmorse only). Neither model ships with the app: each downloads on first use from its upstream source at a pinned size and SHA-256. Are both decoders ready to ship that way?
    2. DeepFist license. N9BC's exp27_bt-champion release has no LICENSE file, and the repository moved from MIT to GPL-3.0-or-later on 2026-07-20, after the checkpoint was created (2026-07-15). The PR downloads the MIT LICENSE that main already pins, from the repository's first commit (061fc1d7). Is that the right license for this checkpoint, or does N9BC need to confirm or attach one?
    3. DeepFist qualification. The 09-14 approval asked for a published model release and Windows/Linux qualification. The release is published, Linux and Windows testing is in DeepFist drops most of the text on off-air CW: activity gate closed on 66–72 % of ticks (two W1AW bulletins) #5950, fix(cw): lower DeepFist's activity-gate threshold to 3 so weak off-air CW is not gated out (#5950). Principle XI. #5953 and feat(cw): colour DeepFist letters by the model's per-letter confidence (#6099). Principle VIII. #6103, and the PR will add runs on both. Is that sufficient?
    4. Download code. The approval asked for the ASR model manager to be renamed to a neutral one. DeepFist has since added its own downloader, which has no ASR dependency; the PR reuses it for DeepCW and lets an asset name its own source. Keep that, or rename?

    The rest follows the approval: the CW panel's selector offers ggmorse, DeepFist and DeepCW, with ggmorse the default and the TX decoder; only the selected decoder runs; the compare applet stays on the prototype branch; DeepCW downloads from e04/deepcw-engine at a pinned commit; the attribution covers the ported code. On the demo radio, both models download and verify, the choice survives a restart, and text appears about 0.5 s (ggmorse), 1.8 s (DeepFist) and 7.6 s (DeepCW) after the signal, median of five runs each. The follow-up issue for DeepCW's emission work will be filed with the PR.

    — 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

Metadata

Metadata

Assignees

Labels

CWCW keying, decode, and operationGUIUser interfaceNew FeatureNew feature requestmaintainer-reviewRequires maintainer review before any action is taken

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions