Procházet zdrojové kódy

Add CFAR legend, classifier scores, and reorder settings

master
Jan Svabenik před 3 dny
rodič
revize
1e2d6a4712
5 změnil soubory, kde provedl 81 přidání a 33 odebrání
  1. +1
    -0
      internal/classifier/rules.go
  2. +6
    -5
      internal/classifier/types.go
  3. +27
    -0
      web/app.js
  4. +45
    -28
      web/index.html
  5. +2
    -0
      web/style.css

+ 1
- 0
internal/classifier/rules.go Zobrazit soubor

@@ -106,6 +106,7 @@ func RuleClassify(feat Features) Classification {
BW3dB: bw, BW3dB: bw,
Features: feat, Features: feat,
SecondBest: second, SecondBest: second,
Scores: scores,
} }
} }




+ 6
- 5
internal/classifier/types.go Zobrazit soubor

@@ -39,11 +39,12 @@ type Features struct {


// Classification is the classifier output attached to signals/events. // Classification is the classifier output attached to signals/events.
type Classification struct { type Classification struct {
ModType SignalClass `json:"mod_type"`
Confidence float64 `json:"confidence"`
BW3dB float64 `json:"bw_3db_hz"`
Features Features `json:"features,omitempty"`
SecondBest SignalClass `json:"second_best,omitempty"`
ModType SignalClass `json:"mod_type"`
Confidence float64 `json:"confidence"`
BW3dB float64 `json:"bw_3db_hz"`
Features Features `json:"features,omitempty"`
SecondBest SignalClass `json:"second_best,omitempty"`
Scores map[SignalClass]float64 `json:"scores,omitempty"`
} }


// SignalInput is the minimal input needed for classification. // SignalInput is the minimal input needed for classification.


+ 27
- 0
web/app.js Zobrazit soubor

@@ -86,6 +86,7 @@ const decodeEventBtn = qs('decodeEventBtn');
const decodeModeSelect = qs('decodeMode'); const decodeModeSelect = qs('decodeMode');
const recordingMetaEl = qs('recordingMeta'); const recordingMetaEl = qs('recordingMeta');
const decodeResultEl = qs('decodeResult'); const decodeResultEl = qs('decodeResult');
const classifierScoresEl = qs('classifierScores');
const recordingMetaLink = qs('recordingMetaLink'); const recordingMetaLink = qs('recordingMetaLink');
const recordingIQLink = qs('recordingIQLink'); const recordingIQLink = qs('recordingIQLink');
const recordingAudioLink = qs('recordingAudioLink'); const recordingAudioLink = qs('recordingAudioLink');
@@ -1007,6 +1008,19 @@ function openDrawer(ev) {
detailSnrEl.textContent = `${(ev.snr_db || 0).toFixed(1)} dB`; detailSnrEl.textContent = `${(ev.snr_db || 0).toFixed(1)} dB`;
detailDurEl.textContent = fmtMs(ev.duration_ms || 0); detailDurEl.textContent = fmtMs(ev.duration_ms || 0);
detailClassEl.textContent = ev.class?.mod_type || '-'; detailClassEl.textContent = ev.class?.mod_type || '-';
if (classifierScoresEl) {
const scores = ev.class?.scores;
if (scores && typeof scores === 'object') {
const rows = Object.entries(scores)
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
.map(([k, v]) => `${k}:${v.toFixed(2)}`)
.join(' · ');
classifierScoresEl.textContent = rows ? `Classifier scores: ${rows}` : 'Classifier scores: -';
} else {
classifierScoresEl.textContent = 'Classifier scores: -';
}
}
if (recordingMetaEl) { if (recordingMetaEl) {
recordingMetaEl.textContent = 'Recording: -'; recordingMetaEl.textContent = 'Recording: -';
} }
@@ -1427,6 +1441,19 @@ if (recordingList) {
const rds = meta.rds_ps ? `RDS: ${meta.rds_ps}` : ''; const rds = meta.rds_ps ? `RDS: ${meta.rds_ps}` : '';
decodeResultEl.textContent = `Decode: ${rds}`; decodeResultEl.textContent = `Decode: ${rds}`;
} }
if (classifierScoresEl) {
const scores = meta.classification?.scores;
if (scores && typeof scores === 'object') {
const rows = Object.entries(scores)
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
.map(([k, v]) => `${k}:${v.toFixed(2)}`)
.join(' · ');
classifierScoresEl.textContent = rows ? `Classifier scores: ${rows}` : 'Classifier scores: -';
} else {
classifierScoresEl.textContent = 'Classifier scores: -';
}
}
} catch {} } catch {}
}); });
} }


+ 45
- 28
web/index.html Zobrazit soubor

@@ -80,7 +80,14 @@
<!-- Spectrum + Waterfall Stack --> <!-- Spectrum + Waterfall Stack -->
<div class="viz-stack"> <div class="viz-stack">
<div class="viz-card"> <div class="viz-card">
<div class="viz-head"><span class="viz-label">Spectrum</span><span class="viz-hint" id="spectrumMeta">Wheel = zoom · Drag = pan · Dbl-click = reset</span></div>
<div class="viz-head">
<span class="viz-label">Spectrum</span>
<span class="viz-hint" id="spectrumMeta">Wheel = zoom · Drag = pan · Dbl-click = reset</span>
<span class="viz-legend" id="cfarLegend">
<span class="legend-swatch"></span>
CFAR edge (static threshold)
</span>
</div>
<canvas id="spectrum"></canvas> <canvas id="spectrum"></canvas>
</div> </div>
<div class="viz-card"> <div class="viz-card">
@@ -142,33 +149,38 @@
</div> </div>


<div class="form-group"> <div class="form-group">
<div class="grp-title">Recorder</div>
<div class="toggle-grid">
<label class="pill-toggle"><input id="recEnableToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Enabled</span></label>
<label class="pill-toggle"><input id="recIQToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Record IQ</span></label>
<label class="pill-toggle"><input id="recAudioToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Record Audio</span></label>
<label class="pill-toggle"><input id="recDemodToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Auto Demod</span></label>
<label class="pill-toggle"><input id="recDecodeToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Auto Decode</span></label>
</div>
<label class="field"><span>Min SNR (dB)</span><input id="recMinSNR" type="number" step="1" min="0" /></label>
<label class="field"><span>Max Disk (MB)</span><input id="recMaxDisk" type="number" step="256" min="0" /></label>
<label class="field"><span>Class Filter (CSV)</span><input id="recClassFilter" type="text" placeholder="e.g. NFM,USB" /></label>
</div>

<div class="form-group">
<div class="grp-title">DSP</div>
<div class="grp-title">DSP & Display</div>
<label class="field"><span>FFT Size</span> <label class="field"><span>FFT Size</span>
<select id="fftSelect"> <select id="fftSelect">
<option value="512">512</option><option value="1024">1024</option> <option value="512">512</option><option value="1024">1024</option>
<option value="2048">2048</option><option value="4096">4096</option> <option value="2048">2048</option><option value="4096">4096</option>
<option value="8192">8192</option><option value="16384">16384</option> <option value="8192">8192</option><option value="16384">16384</option>
<option value="32768">32768</option><option value="65536">65536</option>
<option value="32768">32768</option>
<option value="65536">65536</option>
</select> </select>
</label> </label>
<div class="slider-field"> <div class="slider-field">
<span>Gain</span> <span>Gain</span>
<div class="slider-row"><input id="gainRange" type="range" min="0" max="60" step="1" /><input id="gainInput" type="number" min="0" max="60" step="1" class="slider-num" /><em>dB</em></div> <div class="slider-row"><input id="gainRange" type="range" min="0" max="60" step="1" /><input id="gainInput" type="number" min="0" max="60" step="1" class="slider-num" /><em>dB</em></div>
</div> </div>
<label class="field"><span>Averaging</span>
<select id="avgSelect">
<option value="0">Off</option><option value="0.4">Fast</option>
<option value="0.2">Medium</option>
<option value="0.1">Slow</option>
</select>
</label>
<div class="toggle-grid">
<label class="pill-toggle"><input id="agcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">AGC</span></label>
<label class="pill-toggle"><input id="dcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">DC Block</span></label>
<label class="pill-toggle"><input id="iqToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">IQ Bal</span></label>
<label class="pill-toggle"><input id="maxHoldToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Max Hold</span></label>
<label class="pill-toggle"><input id="gpuToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">GPU FFT</span></label>
</div>
</div>

<div class="form-group">
<div class="grp-title">Detector</div>
<div class="slider-field"> <div class="slider-field">
<span>Threshold</span> <span>Threshold</span>
<div class="slider-row"><input id="thresholdRange" type="range" min="-120" max="0" step="1" class="range--warn" /><input id="thresholdInput" type="number" min="-120" max="0" step="1" class="slider-num" /><em>dB</em></div> <div class="slider-row"><input id="thresholdRange" type="range" min="-120" max="0" step="1" class="range--warn" /><input id="thresholdInput" type="number" min="-120" max="0" step="1" class="slider-num" /><em>dB</em></div>
@@ -179,25 +191,28 @@
<label class="field"><span>CFAR Scale (dB)</span><input id="cfarScaleInput" type="number" step="0.5" min="0" /></label> <label class="field"><span>CFAR Scale (dB)</span><input id="cfarScaleInput" type="number" step="0.5" min="0" /></label>
<label class="field"><span>Min Duration (ms)</span><input id="minDurationInput" type="number" step="50" min="50" /></label> <label class="field"><span>Min Duration (ms)</span><input id="minDurationInput" type="number" step="50" min="50" /></label>
<label class="field"><span>Hold (ms)</span><input id="holdInput" type="number" step="50" min="50" /></label> <label class="field"><span>Hold (ms)</span><input id="holdInput" type="number" step="50" min="50" /></label>
<label class="field"><span>Averaging</span>
<select id="avgSelect">
<option value="0">Off</option><option value="0.4">Fast</option>
<option value="0.2">Medium</option><option value="0.1">Slow</option>
</select>
</label>
<label class="field"><span>EMA Alpha</span><input id="emaAlphaInput" type="number" step="0.05" min="0" max="1" /></label> <label class="field"><span>EMA Alpha</span><input id="emaAlphaInput" type="number" step="0.05" min="0" max="1" /></label>
<label class="field"><span>Hysteresis (dB)</span><input id="hysteresisInput" type="number" step="1" min="0" /></label> <label class="field"><span>Hysteresis (dB)</span><input id="hysteresisInput" type="number" step="1" min="0" /></label>
<label class="field"><span>Min Stable Frames</span><input id="stableFramesInput" type="number" step="1" min="1" /></label> <label class="field"><span>Min Stable Frames</span><input id="stableFramesInput" type="number" step="1" min="1" /></label>
<label class="field"><span>Gap Tolerance (ms)</span><input id="gapToleranceInput" type="number" step="50" min="0" /></label> <label class="field"><span>Gap Tolerance (ms)</span><input id="gapToleranceInput" type="number" step="50" min="0" /></label>
<div class="toggle-grid"> <div class="toggle-grid">
<label class="pill-toggle"><input id="cfarToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">CFAR</span></label> <label class="pill-toggle"><input id="cfarToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">CFAR</span></label>
<label class="pill-toggle"><input id="agcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">AGC</span></label>
<label class="pill-toggle"><input id="dcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">DC Block</span></label>
<label class="pill-toggle"><input id="iqToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">IQ Bal</span></label>
<label class="pill-toggle"><input id="maxHoldToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Max Hold</span></label>
<label class="pill-toggle"><input id="gpuToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">GPU FFT</span></label>
</div> </div>
</div> </div>

<div class="form-group">
<div class="grp-title">Recorder</div>
<div class="toggle-grid">
<label class="pill-toggle"><input id="recEnableToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Enabled</span></label>
<label class="pill-toggle"><input id="recIQToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Record IQ</span></label>
<label class="pill-toggle"><input id="recAudioToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Record Audio</span></label>
<label class="pill-toggle"><input id="recDemodToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Auto Demod</span></label>
<label class="pill-toggle"><input id="recDecodeToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">Auto Decode</span></label>
</div>
<label class="field"><span>Min SNR (dB)</span><input id="recMinSNR" type="number" step="1" min="0" /></label>
<label class="field"><span>Max Disk (MB)</span><input id="recMaxDisk" type="number" step="256" min="0" /></label>
<label class="field"><span>Class Filter (CSV)</span><input id="recClassFilter" type="text" placeholder="e.g. NFM,USB" /></label>
</div>
</section> </section>


<!-- ── Signals Tab ── --> <!-- ── Signals Tab ── -->
@@ -312,6 +327,7 @@
<div class="insp-note">Ready for: IQ snippets, clip playback, bookmarks, annotations.</div> <div class="insp-note">Ready for: IQ snippets, clip playback, bookmarks, annotations.</div>
<div class="insp-note" id="recordingMeta">Recording: -</div> <div class="insp-note" id="recordingMeta">Recording: -</div>
<div class="insp-note" id="decodeResult">Decode: -</div> <div class="insp-note" id="decodeResult">Decode: -</div>
<div class="insp-note" id="classifierScores">Classifier scores: -</div>
<div class="insp-note"> <div class="insp-note">
<a id="recordingMetaLink" href="#" target="_blank">meta.json</a> · <a id="recordingMetaLink" href="#" target="_blank">meta.json</a> ·
<a id="recordingIQLink" href="#" target="_blank">IQ</a> · <a id="recordingIQLink" href="#" target="_blank">IQ</a> ·
@@ -355,3 +371,4 @@
</script> </script>
</body> </body>
</html> </html>


+ 2
- 0
web/style.css Zobrazit soubor

@@ -207,6 +207,8 @@ button, input, select { font: inherit; }
} }
.viz-card--compact { } .viz-card--compact { }
.viz-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .viz-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.viz-legend { display: inline-flex; align-items: center; gap: 6px; font-size: 0.68rem; color: var(--text-dim); }
.legend-swatch { width: 10px; height: 10px; border-radius: 2px; background: rgba(255, 204, 102, 0.35); border: 1px solid rgba(255, 204, 102, 0.35); }
.viz-label { font-family: var(--mono); font-size: 0.6rem; font-weight: 600; letter-spacing: 0.1em; color: var(--text-dim); text-transform: uppercase; } .viz-label { font-family: var(--mono); font-size: 0.6rem; font-weight: 600; letter-spacing: 0.1em; color: var(--text-dim); text-transform: uppercase; }
.viz-hint { font-family: var(--mono); font-size: 0.58rem; color: var(--text-mute); } .viz-hint { font-family: var(--mono); font-size: 0.58rem; color: var(--text-mute); }




Načítá se…
Zrušit
Uložit