Bläddra i källkod

feat: add debug overlay toggle and score bars

master
Jan Svabenik 3 dagar sedan
förälder
incheckning
8e4e7f9d28
3 ändrade filer med 45 tillägg och 2 borttagningar
  1. +38
    -2
      web/app.js
  2. +1
    -0
      web/index.html
  3. +6
    -0
      web/style.css

+ 38
- 2
web/app.js Visa fil

@@ -95,6 +95,7 @@ const recordingAudioLink = qs('recordingAudioLink');
const followBtn = qs('followBtn');
const fitBtn = qs('fitBtn');
const resetMaxBtn = qs('resetMaxBtn');
const debugOverlayToggle = qs('debugOverlayToggle');
const timelineFollowBtn = qs('timelineFollowBtn');
const timelineFreezeBtn = qs('timelineFreezeBtn');

@@ -150,6 +151,8 @@ let timelineRects = [];
let liveSignalRects = [];
let recordings = [];
let recordingsFetchInFlight = false;
let showDebugOverlay = true;
let showDebugOverlay = true;

const GAIN_MAX = 60;
const timelineWindowMs = 5 * 60 * 1000;
@@ -181,6 +184,27 @@ function fmtMs(ms) {
return `${(ms / 1000).toFixed(2)} s`;
}

function renderScoreBars(scores) {
if (!classifierScoreBarsEl) return;
if (!scores || typeof scores !== 'object') {
classifierScoreBarsEl.innerHTML = '';
return;
}
const entries = Object.entries(scores)
.filter(([, v]) => Number.isFinite(Number(v)))
.sort((a, b) => Number(b[1]) - Number(a[1]))
.slice(0, 6);
if (!entries.length) {
classifierScoreBarsEl.innerHTML = '';
return;
}
const maxVal = Math.max(...entries.map(([, v]) => Number(v)), 1e-6);
classifierScoreBarsEl.innerHTML = entries.map(([label, value]) => {
const width = Math.max(4, (Number(value) / maxVal) * 100);
return `<div class="score-bar"><span class="score-bar-label">${label}</span><span class="score-bar-track"><span class="score-bar-fill" style="width:${width}%"></span></span><span class="score-bar-value">${Number(value).toFixed(2)}</span></div>`;
}).join('');
}

function colorMap(v) {
const x = Math.max(0, Math.min(1, v));
const r = Math.floor(255 * Math.pow(x, 0.55));
@@ -225,14 +249,16 @@ function sampleOverlayAtX(overlay, x, width, centerHz, sampleRate) {
}

function drawThresholdOverlay(ctx, w, h, minDb, maxDb) {
if (!latest?.thresholds?.length) return;
if (!showDebugOverlay) return;
const thresholds = latest?.debug?.thresholds;
if (!Array.isArray(thresholds) || thresholds.length === 0) return;
ctx.save();
ctx.strokeStyle = 'rgba(255, 196, 92, 0.9)';
ctx.lineWidth = 1.25;
if (ctx.setLineDash) ctx.setLineDash([6, 4]);
ctx.beginPath();
for (let x = 0; x < w; x++) {
const v = sampleOverlayAtX(latest.thresholds, x, w, latest.center_hz, latest.sample_rate);
const v = sampleOverlayAtX(thresholds, x, w, latest.center_hz, latest.sample_rate);
if (v == null || Number.isNaN(v)) continue;
const y = h - ((v - minDb) / (maxDb - minDb)) * (h - 18) - 6;
if (x === 0) ctx.moveTo(x, y);
@@ -1063,6 +1089,7 @@ function openDrawer(ev) {
.map(([k, v]) => `${k}:${v.toFixed(2)}`)
.join(' · ');
classifierScoresEl.textContent = rows ? `Classifier scores: ${rows}` : 'Classifier scores: -';
renderScoreBars(scores);
} else {
const liveScores = (latest?.debug?.scores || []).find((s) => Math.abs((s.center_hz || 0) - (ev.center_hz || 0)) < Math.max(500, (ev.bandwidth_hz || 0)));
if (liveScores?.scores) {
@@ -1072,8 +1099,10 @@ function openDrawer(ev) {
.map(([k, v]) => `${k}:${Number(v).toFixed(2)}`)
.join(' · ');
classifierScoresEl.textContent = rows ? `Classifier scores: ${rows}` : 'Classifier scores: -';
renderScoreBars(liveScores.scores);
} else {
classifierScoresEl.textContent = 'Classifier scores: -';
renderScoreBars(null);
}
}
}
@@ -1363,6 +1392,11 @@ maxHoldToggle.addEventListener('change', () => {
maxSpectrum = null;
markSpectrumDirty();
});
if (debugOverlayToggle) debugOverlayToggle.addEventListener('change', () => {
showDebugOverlay = debugOverlayToggle.checked;
markSpectrumDirty();
updateHeroMetrics();
});
resetMaxBtn.addEventListener('click', () => {
maxSpectrum = null;
markSpectrumDirty();
@@ -1521,6 +1555,8 @@ if (recordingList) {
});
}

if (debugOverlayToggle) debugOverlayToggle.checked = showDebugOverlay;

window.addEventListener('keydown', (ev) => {
if (ev.target && ['INPUT', 'SELECT', 'TEXTAREA'].includes(ev.target.tagName)) return;
if (ev.key === ' ') {


+ 1
- 0
web/index.html Visa fil

@@ -58,6 +58,7 @@
<button class="act-btn" id="followBtn" type="button">Follow</button>
<button class="act-btn" id="fitBtn" type="button">Fit</button>
<button class="act-btn" id="resetMaxBtn" type="button">Reset Max</button>
<label class="pill-toggle pill-toggle--compact"><input id="debugOverlayToggle" type="checkbox" checked /><span class="pt"><span class="pk"></span></span><span class="pl">CFAR/Debug</span></label>
</div>
</div>



+ 6
- 0
web/style.css Visa fil

@@ -413,6 +413,12 @@ input[type="range"]::-moz-range-thumb {

.insp-viz { height: 200px; }
.insp-note { font-size: 0.72rem; color: var(--text-mute); }
.score-bars { display: grid; gap: 6px; }
.score-bar { display: grid; grid-template-columns: 52px 1fr 44px; gap: 8px; align-items: center; font-family: var(--mono); font-size: 0.68rem; }
.score-bar-label { color: var(--text-dim); }
.score-bar-track { position: relative; height: 8px; border-radius: 999px; background: rgba(148, 163, 184, 0.14); overflow: hidden; }
.score-bar-fill { position: absolute; inset: 0 auto 0 0; background: linear-gradient(90deg, rgba(0,255,200,0.72), rgba(0,144,255,0.8)); border-radius: 999px; }
.score-bar-value { color: var(--text-mute); text-align: right; }

/* ═══════════ KEYBOARD OVERLAY ═══════════ */
.kb-overlay {


Laddar…
Avbryt
Spara