feat: minimum section duration filter (--min-duration, default 0.5 s)
A single 100 ms RMS window above the noise floor used to become its own section, so isolated pops (clicks, single raindrops) flooded a day with thousands of sub-second clips like "21:18 to 21:18". Sections shorter than min_duration (measured after min_gap merging, so a cluster of blips spanning longer still flags) are now discarded. Wired through all coupled places: CLI flag, /api/config, controls-bar input, /api/analyze query param, and the analysis-cache head keys (old two-key caches no longer match and are recomputed on next analyse). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+18
-8
@@ -155,6 +155,10 @@ body.clip-open{padding-bottom:70px}
|
||||
<input type="number" id="min-gap-input" min="0" max="300" step="0.5" value="2"
|
||||
aria-describedby="min-gap-hint">
|
||||
<span id="min-gap-hint" class="controls-hint">seconds — merge loud sections closer than this</span>
|
||||
<label for="min-duration-input" style="margin-left:16px">Min duration:</label>
|
||||
<input type="number" id="min-duration-input" min="0" max="60" step="0.1" value="0.5"
|
||||
aria-describedby="min-duration-hint">
|
||||
<span id="min-duration-hint" class="controls-hint">seconds — ignore loud sections shorter than this</span>
|
||||
</div>
|
||||
<div class="filter-bar" role="search" aria-label="Filter recordings">
|
||||
<label for="filter-name">Search:</label>
|
||||
@@ -403,16 +407,17 @@ document.getElementById('clip-context').addEventListener('click', () => {
|
||||
seekToSection(c.fileIdx, c.filename, c.start, c.end, null);
|
||||
});
|
||||
|
||||
// filename|margin|gap -> analysis result, so re-renders (filtering,
|
||||
// filename|margin|gap|minDur -> analysis result, so re-renders (filtering,
|
||||
// refresh) never refetch what this session already has
|
||||
const analysisCache = new Map();
|
||||
|
||||
async function fetchAnalysis(filename, margin, minGap, force = false) {
|
||||
const key = `${filename}|${margin}|${minGap}`;
|
||||
async function fetchAnalysis(filename, margin, minGap, minDur, force = false) {
|
||||
const key = `${filename}|${margin}|${minGap}|${minDur}`;
|
||||
if (!force && analysisCache.has(key)) return analysisCache.get(key);
|
||||
const r = await fetch('/api/analyze?file='+encodeURIComponent(filename)
|
||||
+'&margin='+encodeURIComponent(margin)
|
||||
+'&min_gap='+encodeURIComponent(minGap));
|
||||
+'&min_gap='+encodeURIComponent(minGap)
|
||||
+'&min_duration='+encodeURIComponent(minDur));
|
||||
const d = await r.json();
|
||||
if (!d.error) analysisCache.set(key, d);
|
||||
return d;
|
||||
@@ -424,13 +429,14 @@ async function analyse(idx, filename, cell, btn, force = false) {
|
||||
cell.innerHTML = '<div class="spin" aria-live="polite" aria-busy="true">Analysing…</div>';
|
||||
const margin = document.getElementById('margin-input').value || '12';
|
||||
const minGap = document.getElementById('min-gap-input').value || '2';
|
||||
const minDur = document.getElementById('min-duration-input').value || '0.5';
|
||||
const restoreBtn = () => {
|
||||
btn.textContent = 'Analyse'; btn.disabled = false;
|
||||
btn.onclick = () => analyse(idx, filename, cell, btn);
|
||||
if (!cell.contains(btn)) cell.appendChild(btn);
|
||||
};
|
||||
try {
|
||||
const d = await fetchAnalysis(filename, margin, minGap, force);
|
||||
const d = await fetchAnalysis(filename, margin, minGap, minDur, force);
|
||||
if (d.error) {
|
||||
cell.innerHTML = `<div class="spin" role="alert">Error: ${esc(d.error)}</div>`;
|
||||
restoreBtn(); return;
|
||||
@@ -439,7 +445,7 @@ async function analyse(idx, filename, cell, btn, force = false) {
|
||||
box.appendChild(drawWave(d.rms_display||[], d.sections||[], d.duration||0, filename));
|
||||
|
||||
const meta = document.createElement('div'); meta.className='analysis-meta';
|
||||
meta.textContent = `margin: ${margin} dB · gap: ${minGap}s${d.cached ? ' · cached' : ''}`;
|
||||
meta.textContent = `margin: ${margin} dB · gap: ${minGap}s · min: ${minDur}s${d.cached ? ' · cached' : ''}`;
|
||||
box.appendChild(meta);
|
||||
|
||||
const chips = document.createElement('div');
|
||||
@@ -576,7 +582,8 @@ async function updateStorage() {
|
||||
function cachedParamsMatch(ca) {
|
||||
return ca != null
|
||||
&& Number(ca.margin) === parseFloat(document.getElementById('margin-input').value)
|
||||
&& Number(ca.min_gap) === parseFloat(document.getElementById('min-gap-input').value);
|
||||
&& Number(ca.min_gap) === parseFloat(document.getElementById('min-gap-input').value)
|
||||
&& Number(ca.min_duration) === parseFloat(document.getElementById('min-duration-input').value);
|
||||
}
|
||||
|
||||
// Run the deferred analyses of a freshly expanded day
|
||||
@@ -852,6 +859,7 @@ async function dayHighlights(dayId, analyzableFiles) {
|
||||
|
||||
const margin = document.getElementById('margin-input').value || '12';
|
||||
const minGap = document.getElementById('min-gap-input').value || '2';
|
||||
const minDur = document.getElementById('min-duration-input').value || '0.5';
|
||||
|
||||
const results = [];
|
||||
let nCached = 0, nLive = 0;
|
||||
@@ -860,7 +868,7 @@ async function dayHighlights(dayId, analyzableFiles) {
|
||||
progFile.textContent = `${i + 1} / ${n} — ${f.name}`;
|
||||
progFill.style.width = `${(i / n) * 100}%`;
|
||||
try {
|
||||
const d = await fetchAnalysis(f.name, margin, minGap);
|
||||
const d = await fetchAnalysis(f.name, margin, minGap, minDur);
|
||||
if (!d.error) { results.push({ f, data: d }); d.cached ? nCached++ : nLive++; }
|
||||
} catch(e) {}
|
||||
}
|
||||
@@ -1099,6 +1107,8 @@ fetch('/api/config').then(r => r.json()).then(cfg => {
|
||||
document.getElementById('margin-input').value = cfg.margin;
|
||||
if (cfg.min_gap != null)
|
||||
document.getElementById('min-gap-input').value = cfg.min_gap;
|
||||
if (cfg.min_duration != null)
|
||||
document.getElementById('min-duration-input').value = cfg.min_duration;
|
||||
}).catch(() => {}).finally(() => load().then(() => setInterval(pollStatus, 5000)));
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user