DeepFist neural CW decoder: DeepFist + Auto (hybrid) RX CW engines, with confirmed-call learning (ONNX Runtime) - #7
DeepFist neural CW decoder: DeepFist + Auto (hybrid) RX CW engines, with confirmed-call learning (ONNX Runtime)#7n9bc wants to merge 66 commits into
Conversation
Design for adding DeepFist (self-trained CNN+CTC neural CW decoder, MIT) as a second selectable RX CW engine alongside the fldigi port, via ONNX Runtime (C++, CPU). Covers model contract, new src/dsp/deepfist module, engine selection in WdspEngine, dockable pop-out DeepFistPanel, licensing clearance, and a code-only (model out of git) delivery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native C++ port of the DeepFist neural CW decoder (spectrogram -> CNN+CTC
ONNX -> greedy CTC), mirroring the verified Rust reference (diddle
cw_neural.rs). Qt-free DSP under src/dsp/deepfist/:
- DeepFistResampler : 48k->3200 Hz decimator (121-tap, 1440 Hz), exact
match to the reference filter
- DeepFistSpectrogram : STFT 256/hop48, 65 bins 400-1200 Hz, log1p,
per-window unbiased standardize; from-scratch FFT
- DeepFistCtc : greedy CTC decode
- DeepFistModel : ONNX Runtime session + sidecar token table
- NeuralCwDecoder : streaming adapter (rolling 6 s window, re-decode
every ~1.5 s), same shape as the classic CwDecoder
Vendors prebuilt CPU ONNX Runtime 1.20.1 (MIT) under third_party/onnxruntime
(headers + license committed; DLL/model git-ignored, code-only PR). CMake
links ORT, ships onnxruntime.dll + models/ next to the exe.
Verified: scratch/test_neural_cw decodes known clips (3200 Hz direct path +
48 kHz decimator path) with a 0-mismatch exact match to the Python-ONNX
reference on CQ TEST DE K3E / AGN / 5NN 001 K / CQ OK8G / R FB TU 73.
Licensing: DeepFist MIT (c) Brent Crier + ONNX Runtime MIT, both permissive
inside Lyra's GPLv3; no AGPL (DeepCW/HamNoise) code used.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds DeepFist as a second, selectable CW decode engine beside the classic fldigi port: - WdspEngine: NeuralCwDecoder member + cwEngine_ switch (0=Classic,1=Neural), Q_PROPERTY cwDecodeEngine + cwNeuralAvailable, cwNeuralText signal. The CW-mode-gated audio tap routes cwMonoBuf_ to the selected engine only. The model is lazy-loaded on first switch to Neural (from <exeDir>/models or $DEEPFIST_MODEL_DIR); engine flips to Neural only after the ONNX session is fully built, so the audio thread never touches a half-constructed session. - CwDecoderPanel.qml: Classic/DeepFist selector + model-status line. The neural engine's full 6 s-window decode shows in replace mode (cwNeuralText); the fldigi-only knobs (speed/BW/tracking/squelch/signal) are hidden for Neural, which is self-tuning. - Prefs: persist cwDecodeEngine; restored on panel load. - NOTICE.md / CREDITS.md: DeepFist (MIT) + ONNX Runtime (MIT) attribution. Not yet compiled in the full app: the v0.17.0 tree currently fails to configure because the installed Qt 6.11.1 kit is missing the Qt::SerialPort module (required by unrelated serial CW-key code). The DeepFist decode core is independently verified (see prior commit). Full-app build + live UI check are pending the SerialPort module. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Positive confirmation in the app log (%APPDATA%/N8SDR/Lyra-cpp/logs) that the ONNX model loaded, alongside the existing warning on failure. Verified via a launch smoke test: app starts with onnxruntime.dll, panel restores the Neural engine, and the model loads from the deployed models/ dir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…decode) The neural engine emitted the full 6 s window every 1.5 s in replace mode, so the panel only showed ~6 s of text and updated in slow chunks. Replaced that with the reference live-decode approach (deepfist/scripts/tci_decode.py): - Frame-timed commit: each decoded char carries a frame time (greedyCtcFrames); it is emitted exactly once when its audio settles (~1.3 s old), streaming characters into an accumulating transcript instead of replacing a window. - Re-decode every ~0.4 s (was 1.5 s). - Inference moved to a worker thread so the faster cadence never stalls the audio callback; it idles when no new audio arrives (Neural not active). - NeuralCwDecoder.onText now delivers incremental committed text; the panel appends it (both engines now stream into the same transcript). DeepFistModel split into infer() (raw log-probs) + decode3200(); the one-shot harness still matches the reference exactly (7/7). New test_neural_cw_stream harness plays a 48 kHz WAV through the real streaming path: on DeepFist's real 25 s off-air capture it streams "…ARY 5NN … K6ZH TEST K7CO K7CO 5NN 6", matching the reference decode of that clip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DeepFist's shipped model is now exp15, trained on CONDITIONED single-signal
audio, so inference must condition too (train == inference). Ports diddle's
verified live Rust conditioner (cw_neural.rs::Conditioner) to C++:
AGC (unit RMS) -> tone AFC (4096-pt FFT, 400-1200 Hz) -> complex downconvert +
two cascaded 1-pole LPFs (~90 Hz) -> recenter to 600 Hz -> peak-norm. Runs on
the 3200 Hz window inside DeepFistModel::infer, before the spectrogram.
New src/dsp/deepfist/DeepFistConditioner.{h,cpp} (incremental-phasor form
matching the Rust numerics). Verified: C++ conditioner+exp15 matches the Rust
reference (diddle cargo example) EXACTLY on 5 clean clips; streaming the real
audio_001 off-air capture now copies "WP3Z TEST WP3Z ..." (exp9 was patchy).
Marginal fast/weak clips (untitled) sit at the model's decision boundary where
Rust/Python/C++ all differ slightly — inherent model instability, not a port
bug. Model artifact (exp15) refreshed in models/ (git-ignored).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports diddle's dsp/rescore.rs + scp.rs to C++: for each callsign-shaped word in
a decoded window, swap in nearby MASTER.SCP candidates and score each against
the model's CTC log-prob lattice (ctcNll = full-sequence CTC loss); the call
the audio actually supports wins. Fixes garbles text edit-distance can't (WP3B
vs WP3Z). Confident = margin >= 3 nats over the best different candidate.
- src/dsp/deepfist/DeepFistRescore.{h,cpp}: ctcNll (log-space CTC forward),
is_call_shape, run/span finding, rescoreCalls.
- src/dsp/deepfist/DeepFistScp.{h,cpp}: MASTER.SCP loader + edit-distance
candidate generation.
- NeuralCwDecoder: loads MASTER.SCP beside the model; runs the rescorer per
window on the worker thread; emits confident verdicts via onCalls.
- WdspEngine: cwNeuralCall signal. CwDecoderPanel: a "Calls" readout of
confirmed/corrected calls (deduped, ranked by confirmation count, green =
confirmed / amber = corrected), click -> His Call.
- MASTER.SCP bundled next to the exe (git-ignored, like the model).
Verified: test_rescore matches PyTorch F.ctc_loss (1e-4) + flips A1B->A1C;
streaming the real audio_001 capture confirms WP3Z at 5.4-9.1 nats (matches
the DeepFist reference's 5.4-9.3). Runtime log: "SCP rescorer: 50038 calls".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # CMakeLists.txt # src/wdsp_engine.h
LYRA_CW_WAV now writes UNCLAMPED 32-bit float (was clamped 16-bit) so the true decoder-input level/envelope are visible — a clamped capture hid that the live RX audio runs at ~x21 full scale (+19 dB makeup gain), which looked like clipping but is fine (the conditioner AGC normalizes it). test_neural_cw and test_neural_cw_stream now read fmt=3/32-bit float WAVs too. Used to diagnose the "horrible live decode": the audio is clean (decodes to KG4CB when a window is well-aligned); the streaming garble is exp15 being alignment-sensitive, and diddle's reference decoder garbles the same capture identically — i.e. a model limitation, not a Lyra bug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… finding Single resume anchor for the feat/deepfist-cw work: architecture/file map, build+run+verify procedure, environment gotchas, and the resolved "horrible live decode" investigation — the audio is clean and the Lyra port is faithful (diddle's reference garbles the same capture identically); the garble is exp15 alignment-sensitivity, a DeepFist model limitation to fix in that project. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…no chars) Ports DeepFist's live-copy-display fix (HANDOFF §18.23): the neural decoder was hallucinating streams of junk on empty frequencies because Lyra's CW AGC makes a steady ~700 Hz artifact tone whose LEVEL is indistinguishable from a real signal. KEYING separates them. Per-window keying ratio = p90/p10 of the locked tone's baseband envelope (DeepFistConditioner::keyingRatio); below 12 -> skip the decode, emit nothing, just advance the commit boundary. Chose DeepFist's keying_ratio (tools/squelch.py, VALIDATED on real Lyra x21 captures: dead air ~4, keyed CW 12-600+) over diddle's keying_depth, which gives a flat ~0.68 on Lyra audio and doesn't separate. Verified on the KG4CB capture: dead-air windows (score 3.5-5.5) now emit NOTHING; signal windows (12-671) decode. Real-signal copy quality on hand-sent CW is unchanged (still the DeepFist training frontier; exp16 not yet exported to ONNX). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…del) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nfig) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CTC blank penalty was env-var-only (LYRA_CW_BLANKPEN, invisible). Make it a visible, live, persisted control so it can be tuned/A-B'd on the same signal without a relaunch: - NeuralCwDecoder: blank penalty is now a live atomic (setBlankPenalty / blankPenalty), read by the worker each decode; initial value still seeds from LYRA_CW_BLANKPEN. - WdspEngine: cwBlankPenalty property + setCwBlankPenalty (clamped 0..5). - Prefs: persist cwBlankPenalty; restored on panel load. - CwDecoderPanel: a "Blank penalty" slider (0..5, step 0.5) with a live readout, visible when the DeepFist engine is active. Higher = recover more dropped chars on weak audio; more spurious chars on strong signals. Default 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Blank-penalty slider now snaps to 0.5 steps (snapMode SnapAlways). - Space on long pause: the keying gate emits ONE space when a keyed-CW pause starts (idleGap), so copy across gaps doesn't run together (mirrors tci_decode's idle-gap space). - Removed the green "Calls" chips (rescorer verdicts) that overlapped the decode window; also unwired neuralCw_.onCalls so the per-window callsign-DB rescore no longer runs (CPU saved). Double-click a call in the transcript -> His Call still works (built into the decoded-text area). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- WPM: the neural engine now estimates RX speed from the keying envelope
(DeepFistConditioner::estimateWpm — threshold the tone envelope, dot = short
on-elements, WPM = 1200/dot_ms) and emits it on the existing cwRxWpmChanged
surface, so the header "wpm" populates for DeepFist too (was Classic-only).
- Renamed the blank-penalty slider to "Sensitivity" with a Low/Med/High readout
and a plain-language hint ("Lower = clean copy … Higher = pull weak code").
- RBN highlight: decoded call-shaped words that are currently spotted
(SpotStore::isSpotted, exposed to QML as Spots) are coloured green in the
transcript (RichText) — a real, active station stands out as it's copied.
Re-highlights on Spots.changed().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aming) The double-click handler used selectWord()/selectedText, which the RichText transcript switch and the frequent DeepFist text updates (onTextChanged reset cursorPosition) could disturb -> empty/wrong call. Extract the call-shaped word straight from decodedText at the click position instead (wordAt); the right- click menu uses the same. No dependency on selection state or cursorPosition. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Removed the "• neural (rolling 6 s window)" reference next to the engine selector (kept only the "model not found" warning). - Sensitivity indicator now reads Low (<=0.4) or High, no "Med". Note: AR (+) / BT (=) prosigns are misdecoded by exp16 (BT -> X, AR dropped) — a DeepFist training gap, not a Lyra issue; needs more prosign data in training. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- DeepFist neural copy now shows BT/AR prosigns as <BT>/<AR> (angle notation, matching <SK>/<KN> and the classic decoder) instead of the raw = / + aliases. Display-only: a misread prosign still shows what the model decoded. - Sensitivity (blank-penalty) slider is now bipolar (-2..+5, default 0): negative suppresses stray/doubled characters on strong signals, positive pulls weak code out of the noise. Signed numeric readout + numbered ticks (new LyraSlider showTicks/showTickNumbers). Clamps widened in Prefs and WdspEngine to match. - CwWavCapture (LYRA_CW_WAV): re-patch the RIFF/data size fields every ~1 s of audio so a capture stays a valid, playable WAV even when the process is killed before the static-local destructor runs (Qt app close doesn't guarantee it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `SPDX-License-Identifier: MIT` + Brent Crier copyright to each src/dsp/deepfist/ file. DeepFist is MIT (© Brent Crier) integrated into Lyra's GPLv3+ aggregate — permissive-into-copyleft, already documented in NOTICE.md/CREDITS.md. Per-file headers make the provenance unambiguous now that these files carry an explicit license marker even if copied out of tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- build3.ps1: per-machine build helper (like _b*.bat / _run_lyra_vac1.bat) - .claude/scheduled_tasks.lock: transient runtime lock - models/*.bak: local rollback copies of swapped-in models (the exp16 .bak guarding an exp27 swap) — 13 MB binaries that must stay out of git like the live .onnx artifacts already do Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DEEPFIST_HANDOFF.md was session-handoff notes — a dated commit list, a resolved investigation log, a TODO list, and references to private local paths / a sibling repo. Replace with docs/DEEPFIST.md: a focused, public-appropriate integration reference (what it is + license/attribution, architecture & file map, model contract, front-end recipe, build/run, and verification) with the internal cruft and private paths removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pot color - Decode pane: auto-scroll now triggers off the ScrollView viewport's contentHeight instead of the TextArea's. For RichText the TextArea height settled a layout pass late, so the newest (just-wrapped) line only appeared on the following update. Scrolling when the Flickable's contentHeight changes includes the new line immediately. - Sensitivity (blank penalty) slider narrowed to a symmetric -1..+1 (ticks at -1, -0.5, 0, +0.5, +1); Prefs + WdspEngine clamps narrowed to match so a stale persisted value can't sit past the slider ends. - RBN-confirmed callsign highlight brightened (#5fe0a0 -> #5fffa8) so a spotted station stands out more in the transcript. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the author's callsign (N9BC) to the DeepFist copyright line everywhere it appears — the 16 src/dsp/deepfist/ SPDX headers, NOTICE.md, CREDITS.md, docs/DEEPFIST.md, and the design spec — matching the "Brent Crier (N9BC)" form already used elsewhere in the repo. (DeepFist's own MIT LICENSE is updated in the separate DeepFist project.) Also fix a stale figure in docs/DEEPFIST.md: the Sensitivity slider range is -1..+1 (narrowed this session), not the old -2..+5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Heads-up on the model artifacts (not in this PR by design): The neural engine needs three runtime files that are intentionally git-ignored and not part of this diff:
These are large binaries meant to ship via the installer / release asset, not committed to git (same pattern as other runtime binaries). CMake's POST_BUILD copies whatever is in What this means for review: the code builds and links fine without them (only the API headers under |
Record that the DeepFist engine is portable (non-Windows ONNX path already present; DSP is plain C++/STL) and ONNX Runtime ships for Linux/macOS — so it adds no new Windows lock-in. The blocker to a Linux/macOS build is the rest of Lyra (WDSP DLL, WinSock2; os_compat.h POSIX shim not yet wired in), not DeepFist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rvest) Adds Auto as a third RX CW engine: runs Classic + DeepFist unmodified and a thin engine-agnostic CwArbiter hands display ownership to whichever is trustworthy, using DeepFist's keyingRatio as the fade detector. Never goes silent; fallback copy is subtly source-marked. Second layer (CwCaptureHarvester) passively harvests a training corpus from arbiter events with trust-tiered labels, plus an RBN->SCP loop feeding verified calls back to the rescorer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TDD, bite-sized tasks: CwArbiter ownership state machine (pure unit + test_cw_arbiter target), onKeying feed from NeuralCwDecoder, WdspEngine cwEngine_=2 fan-out to both decoders, and the QML Auto selector with subtle source-marked (dimmed) fallback text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s engine switches Two-tier call colouring in the decode pane: any token that is an exact MASTER.SCP member renders amber (known-real call — DeepFistScp::contains, O(log n), surfaced as Q_INVOKABLE WdspEngine::cwCallKnown); an RBN/cluster spot still upgrades to bright green + bold (live, verified). A strict shape-only fallback (1-3 letter suffix) covers sessions where the SCP never loads; SCP arrival re-renders the transcript so earlier copy gains its amber. Future validation sources (Phase-3 RBN-confirmed local list, SDRLogger+ worked-before sync) OR into cwCallKnown without touching the QML. Engine switches no longer clear the transcript — the operator A/Bs engines on the same signal and tunes each; a " • " seam marks the switch point and the Clear chip still empties on demand. Operator-verified on air (NA2DX amber; AA0TT DE run-together no longer false-ambers). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ScpLocal store (capped, aged, atomic rewrite; TDD), DeepFistScp::addCalls merge at model load, WdspEngine ownership + cwNoteConfirmedCall + Prefs.cwLearnCalls (opt-in per spec §6.6), QML word tap + Learn chip. Also folds in the Phase-1 escape bugfix: prefs engine clamp 0..1 → 0..2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…f; fix engine-pref clamp to allow Auto
… list Also: restore a persisted Auto engine at startup (Prefs.cwDecodeEngine === 2 now survives restart, not just DeepFist), and wire ScpLocal.cpp/h into the main lyra target — it was only ever linked into the standalone test_scp_local unit test, leaving WdspEngine::cwNoteConfirmedCall unresolved at link time.
… placement clearDecoded() also clears the Learn tap's pending word (no cross-Clear word concatenation); the setCwDecodeEngine doc block moves back to its function (the two Phase-3 helpers had landed between them); ensureCwScpLocal's comment now states the privacy boundary (the file, not the mkpath'd dir — load() never creates it, note() is QML-gated by Prefs.cwLearnCalls). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CwCaptureHarvester (Qt-free core): 60s 3200Hz ring fed from the CW tap via a parallel decimator, two v1 triggers (arbiter DeepFist->Classic fade -> hard_negative; RBN-confirmed call -> gold_rbn), per-tier debounce, post-roll, 2GiB retention. Segments = int16@3200 peak->20000 WAV + JSON sidecar (tier, keying trace, both texts, wpm, peak) — matching deepfist's curation-tool conventions; windowing/labeling stays offline in the deepfist repo. New CwArbiter::onOwnerChange callback; cwCaptureEnabled pref (opt-in per spec §6.6); Harvest chip. SILVER/agreement/SCP-margin tiers deferred (schema:1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RBN/cluster spots often land 10-60 s after a station starts up, so a cleanly-decoded call could finish its live tapWord() check before its spot existed - the transcript turned green retroactively but the learner never re-looked (W2DON on air, 2026-07-16). Re-scan the recent transcript whenever the spot bank changes; ScpLocal::note() dedupes so repeat scans are free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A 3-char decode garble ("M5N" on air, 2026-07-16) went green because it
exactly matched a call spotted on a different frequency - isSpotted()
answers for the whole spot bank. The decode panel copies the station
under the cursor, so green highlight + Learn now use isSpottedHere():
same lookup, but the spot must sit within 1.5 kHz of the tuned CW
carrier (same carrier convention as activate()).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ention Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
enforceCap() sorted segment paths lexicographically to pick deletion
order. Filenames are "<tier>_<epoch>_<seq>.(wav|json)", and the tier
string is the primary sort key, so every gold_rbn_* segment (highest
trust) sorted before every hard_negative_* segment ('g' < 'h') and was
evicted first regardless of actual age. Parse the age key from the
trailing <epoch>_<seq> fields instead (the tier prefix itself contains
underscores — "hard_negative", "gold_rbn" — so it's parsed from the end
of the stem) and sort ascending by (epoch, seq), tier-independent.
Adds a mixed-tier retention test that writes an older hard_negative
segment and a newer gold_rbn segment, forces the cap to fit exactly one
pair, and asserts the older pair is evicted regardless of tier name.
Verified the new test fails against the pre-fix code (evicts the newer
gold_rbn pair instead) and passes after the fix; full test_cw_harvest
suite passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, cwCaptureEnabled pref Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Critical defect found in review of 3cbdc12: ~WdspEngine() joined the harvest pump thread and called closeRx1(), but never stopped neuralCw_'s internal worker thread. C++ destroys members in reverse declaration order, and in wdsp_engine.h the harvest members (cwHarvestRing_/cwHarvestDecim_/cwHarvester_) are declared AFTER neuralCw_ and cwArbiter_ — so they were destructing BEFORE ~NeuralCwDecoder() ever ran (which is where its worker thread actually stops). That worker fires onText/onWpm/onKeying synchronously off its own thread (wired in setCwDecodeEngine), and those callbacks dereference cwHarvester_ (when cwCaptureOn_ is true) and unconditionally call cwArbiter_.updateKeying — a decode callback in flight during teardown could land on an already-destroyed harvester/arbiter. Fix: explicitly stop+join neuralCw_'s worker (new public NeuralCwDecoder::stop(), a thin wrapper over the existing private stopWorker(), which is already idempotent via running_.exchange(false)) at the top of ~WdspEngine(), before the harvest-pump join and before any harvest/arbiter member is touched. This covers cwArbiter_ for free, since the same worker thread is its only caller. Header member order is left untouched (lower-risk fix than reordering declarations). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Learn re-scan re-confirms every spotted call still in the transcript on every spot-bank change, so a single call minted a near-identical gold_rbn segment every debounce window (observed on air: 5x W1AW/3 in 3 minutes). Per-call dedupe map with a configurable goldRepeatSec window (default 600 s); tier debounce unchanged. Test 10 covers same-call suppression, different-call pass-through, and window expiry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rescanLearn only looked at the last 1500 chars while displayHtml greens everything it renders (6000-char cap) - on air, KT4K sat visibly green but permanently unlearnable once it drifted past the window. Scan the full transcript; the per-call gold dedupe and ScpLocal note() throttle make repeat scans free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three on-air failures shared one root cause: the learn decision was re-derived at a different moment than the render that showed the operator a green call, and each variant lost calls to timing: - tap at word completion missed spots arriving seconds later (W2DON) - a 1500-char rescan window missed calls still visibly green (KT4K) - rescan-on-spot-change missed spots evicted from the 200-cap bank before the next bank event (KA1ULN) displayHtml''s green pass is the only place green-ness is guaranteed observed, so the learn note now fires there, memoized per call per 10 min (matching the harvester''s gold dedupe window). The invariant the operator actually expects - "if I saw it green, it''s learned" - now holds by construction. tapWord/pendingWord/rescanLearn machinery removed; spotRev re-render on spot-bank changes covers the late-spot case since the render IS the learner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- scratch/test_neural_raw.cpp - deterministic tiled-window decode of a WAV slice through the real DeepFist pipeline (per-window keyingRatio probe + blank-penalty sweep); root-caused the presence-gate fade limitation and stays the replay tool for LYRA_CW_WAV captures. - scratch/gap_probe.cpp - keying-envelope on/off gap histogram. - scratch/rbn_fetch.py - stdlib-only RBN daily-archive fetcher/filter producing label CSVs for IQ-capture training sessions. - scratch/rbn_20260708_cwt19z.csv - proven sample output (CWT 19z, 19759 spots / 520 calls on 20m). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ev-only (env-gated)
Learn and Harvest were opt-in CW-panel chips. A chip nobody understands
raises support questions ("is Lyra recording me?"), and Harvest is really a
training-data tool, not a user feature. Remove both chips:
- Learn (RBN-confirmed calls -> local scp_local.txt, text only, never audio)
is now on by default for everyone, gated by WdspEngine.cwLearnEnabled;
disable with LYRA_CW_LEARN=0. Reset by deleting scp_local.txt.
- Harvest (trust-tiered CW audio capture for DeepFist training) is developer-
only, gated by WdspEngine.cwHarvestEnabled; off unless LYRA_CW_HARVEST=1.
Both flags are CONSTANT, read once at construction. The Prefs.cwLearnCalls
and Prefs.cwCaptureEnabled properties are removed entirely (no dead settings).
gold_rbn capture still rides the Learn tap but only writes when Harvest is
armed, so no audio is ever written without the developer opting in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # src/qml/CwDecoderPanel.qml
… branch Brings the complete DeepFist feature set up to date with origin/main so PR N8SDR1#7 can carry it all: base neural decoder + Auto arbiter (hybrid) + RBN->SCP Learn + training Harvest (dev-gated) + render-time learn + the Learn/Harvest chip retirement. Build verified; merges cleanly into main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hi Rick — PR #7 is rebased up to date with main (v0.20.0) and shows clean/mergeable now. Heads-up on scope: I folded the rest of the DeepFist work into this PR rather than splitting it, so it can land as one coherent unit. Beyond the original second-engine decoder, it now also includes:
I flagged the privacy-relevant bits (Learn / Harvest) up front in the description since those are the ones worth your eyes. Classic stays the default; DeepFist/Auto are opt-in. Two things I'd value your call on whenever you get to it:
No rush — review at your pace. |
…rf settings) into feat/deepfist-cw
…ference) into feat/deepfist-cw
Summary
Adds DeepFist, a neural (spectrogram → CNN + CTC) CW/Morse receive decoder, as
additional selectable RX CW engines alongside the existing fldigi-port ("Classic")
decoder. Three engines: Classic (default, unchanged), DeepFist (neural), and
Auto (a hybrid arbiter that runs both and shows whichever is more confident). Runs via
ONNX Runtime (CPU). Classic remains the default; nothing changes unless the operator
selects DeepFist or Auto.
Engines
src/dsp/deepfist/: resampler (48k→3200), front-end conditioner,from-scratch STFT spectrogram, greedy CTC, ONNX model wrapper, streaming adapter, and a
CTC-lattice callsign rescorer (Super Check Partial).
src/dsp/CwArbiter.*: runs Classic and DeepFist in parallel anddisplays the more-confident source moment-to-moment, falling back to Classic on fades.
Copy is source-marked so you can see which engine produced it.
Confirmed-call learning (on by default, no UI toggle)
Decoded callsigns that are also confirmed live by an RBN/cluster spot near the tuned
frequency are remembered in a local
scp_local.txt(AppData). This feeds the rescorer'scandidate set and the "known-real" (amber) highlight, so the decoder improves at the calls
you actually work.
aged out after a year.
LYRA_CW_LEARN=0; deletescp_local.txtto reset. Privacy surface ispublic callsigns, local-only.
Training capture — developer tool, OFF by default
A trust-tiered CW audio capture path for collecting real-world DeepFist training data. It
has no UI and is disabled unless armed with
LYRA_CW_HARVEST=1. When armed, it writesint16 WAV segments + JSON sidecars to
Documents/Lyra/cw_harvest, size-capped (2 GiB,oldest-evicted), local-only. Not part of the normal user experience.
New dependency
ONNX Runtime (Microsoft, MIT), CPU build — shipped as a DLL beside the exe (load-time
import). Headers committed under
third_party/onnxruntime/; binaries git-ignored. This iswhy the diff is large — ~22k lines are ORT headers.
Model & data artifacts (NOT in git)
models/deepfist.onnx(+ sidecar) andMASTER.SCPare git-ignored and distributed via theinstaller/release. CMake copies them next to the exe on build.
Licensing
DeepFist is MIT (© Brent Crier, N9BC); ONNX Runtime is MIT — GPL-compatible. As integrated
it's part of Lyra's GPLv3+ aggregate; per-file SPDX headers on
src/dsp/deepfist/*recordthe MIT provenance. See
NOTICE.md/CREDITS.md.Verification
Builds clean (VS2022) and runs; coexists with ZeroBeat and the classic decoder. Rebased/
merged up to date with
main(v0.20.0).test_rescorechecks the CTC forward pass againstPyTorch
F.ctc_loss;test_neural_cw/test_neural_cw_streammatch the Python/ONNXreference; Classic + DeepFist smoke-tested decoding live.
Reviewer notes
text-only + disableable; Harvest is invisible unless env-armed.
src/dsp/deepfist/,src/dsp/CwArbiter.*,src/wdsp_engine.*,src/spotstore.*, andsrc/qml/CwDecoderPanel.qml; the rest of the diff is vendored ORTheaders.
Classic (documented in
docs/DEEPFIST.md); Auto and Classic cover that case.🤖 Generated with Claude Code