From 4539ff78fa806aca9dd33dd8fabcf02d7e5f2819 Mon Sep 17 00:00:00 2001 From: jonathan Date: Wed, 3 Jun 2026 09:32:04 +0200 Subject: [PATCH] feat: persist analyses on reload, add re-analyse button and metadata, trim highlights tally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _api_files: read each file's analysis cache and return cached_analysis {threshold, min_gap} so the client knows upfront which files are cached - On page load: files with a cached analysis auto-fetch and render their waveform immediately instead of showing an idle Analyse button - After any analysis: show threshold/gap/cached metadata line below the waveform and a Re-analyse button inside the waveform box; old button no longer disappears on success - Error path now always re-adds an actionable Analyse button to the cell - dayHighlights progress: remove the redundant intermediate "N analysed" tally (16/22 counter already conveys progress); done message now says "N files (N from cache)" when relevant - Threshold hint updated to show dBFS equivalent (0.05 ≈ −26 dBFS) - CSS: remove unused .prog-tally and .cached-badge; add .analysis-meta and .reanalyse-btn Co-Authored-By: Claude Sonnet 4.6 --- web.py | 76 +++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/web.py b/web.py index 4624c3b..6207d61 100644 --- a/web.py +++ b/web.py @@ -324,8 +324,24 @@ class _Handler(BaseHTTPRequestHandler): self._send(200, _HTML.encode('utf-8'), 'text/html; charset=utf-8') def _api_files(self): - data = json.dumps(list_files(self.recordings_dir)).encode('utf-8') - self._send(200, data, 'application/json') + files = list_files(self.recordings_dir) + recordings_base = Path(self.recordings_dir).resolve() + analyses_base = Path(self.analyses_dir).resolve() + for f in files: + if f.get('ext') in ('wav', 'flac') and not f.get('recording'): + cache_path = _analysis_cache_path( + analyses_base, recordings_base, recordings_base / f['name']) + try: + cached = json.loads(cache_path.read_text('utf-8')) + f['cached_analysis'] = { + 'threshold': cached['threshold'], + 'min_gap': cached['min_gap'], + } + except Exception: + f['cached_analysis'] = None + else: + f['cached_analysis'] = None + self._send(200, json.dumps(files).encode('utf-8'), 'application/json') def _api_analyze(self, qs): filename = qs.get('file', [None])[0] @@ -712,8 +728,9 @@ button.chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px} .prog-bar{height:3px;background:var(--brd);border-radius:2px;margin:6px 0 4px;overflow:hidden} .prog-fill{height:100%;background:var(--accent);border-radius:2px;transition:width 0.15s ease} .prog-file{font-size:12px;color:var(--muted);font-style:italic;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%} -.prog-tally{font-size:11px;color:var(--muted);margin-top:3px} -.cached-badge{font-size:10px;color:var(--muted);background:var(--surf);border:1px solid var(--brd);border-radius:3px;padding:1px 5px;margin-left:6px;vertical-align:middle;font-style:normal} +.analysis-meta{font-size:10px;color:var(--muted);margin-top:5px;font-style:italic} +button.reanalyse-btn{font-size:10px;padding:2px 7px;margin-top:6px;color:var(--muted);border-color:var(--brd);background:transparent} +button.reanalyse-btn:hover:not(:disabled){background:var(--surf);color:var(--txt)} .empty{text-align:center;padding:60px;color:var(--muted)} /* player row */ .player-row td{padding:0 10px 10px;background:var(--bg);border-bottom:1px solid var(--brd)} @@ -772,7 +789,7 @@ svg.day-timeline{display:block;width:100%;height:22px} - RMS 0–1 · sections above this value are marked loud + RMS amplitude 0–1 (linear; 0.05 ≈ −26 dBFS) · sections above this are marked loud @@ -954,6 +971,11 @@ async function analyse(idx, filename, cell, btn) { cell.innerHTML = '
Analysing…
'; const threshold = document.getElementById('threshold-input').value || '0.05'; const minGap = document.getElementById('min-gap-input').value || '2'; + const restoreBtn = () => { + btn.textContent = 'Analyse'; btn.disabled = false; + btn.onclick = () => analyse(idx, filename, cell, btn); + if (!cell.contains(btn)) cell.appendChild(btn); + }; try { const r = await fetch('/api/analyze?file='+encodeURIComponent(filename) +'&threshold='+encodeURIComponent(threshold) @@ -961,17 +983,14 @@ async function analyse(idx, filename, cell, btn) { const d = await r.json(); if (d.error) { cell.innerHTML = ``; - btn.disabled = false; btn.textContent = 'Analyse'; - return; + restoreBtn(); return; } const box = document.createElement('div'); box.className='wbox'; box.appendChild(drawWave(d.rms_display||[], d.sections||[], d.duration||0, filename)); - if (d.cached) { - const badge = document.createElement('span'); - badge.className = 'cached-badge'; badge.textContent = 'cached'; - badge.title = 'Result loaded from cache — change threshold/gap and re-analyse to recompute'; - box.firstChild.after(badge); - } + + const meta = document.createElement('div'); meta.className='analysis-meta'; + meta.textContent = `threshold: ${threshold} · gap: ${minGap}s${d.cached ? ' · cached' : ''}`; + box.appendChild(meta); const chips = document.createElement('div'); chips.className='chips'; @@ -995,10 +1014,16 @@ async function analyse(idx, filename, cell, btn) { chips.appendChild(q); } box.appendChild(chips); + + const rebtn = document.createElement('button'); + rebtn.className='reanalyse-btn'; rebtn.textContent='Re-analyse'; + rebtn.onclick = () => analyse(idx, filename, cell, rebtn); + box.appendChild(rebtn); + cell.innerHTML=''; cell.appendChild(box); } catch(e) { cell.innerHTML = ``; - btn.disabled = false; btn.textContent = 'Analyse'; + restoreBtn(); } } @@ -1102,15 +1127,21 @@ function _attachFileRowHandlers(f, isRec, expanded, dayId) { if (canAnalyse) { const cell = document.getElementById('wave-'+i); const abtn = document.createElement('button'); - abtn.textContent = 'Analyse'; abtn.setAttribute('aria-label', `Analyse loudness of ${f.name}`); if (isRec) { + abtn.textContent = 'Analyse'; abtn.disabled = true; abtn.title = 'Recording in progress — analyse after recording stops'; + cell.appendChild(abtn); + } else if (f.cached_analysis) { + abtn.textContent = 'Re-analyse'; + cell.innerHTML = '
Loading…
'; + analyse(i, f.name, cell, abtn); } else { - abtn.addEventListener('click', () => analyse(i, f.name, cell, abtn)); + abtn.textContent = 'Analyse'; + abtn.onclick = () => analyse(i, f.name, cell, abtn); + cell.appendChild(abtn); } - cell.appendChild(abtn); } document.getElementById('pbtn-'+i) @@ -1349,8 +1380,7 @@ async function dayHighlights(dayId, analyzableFiles) { const progFill = document.createElement('div'); progFill.className = 'prog-fill'; progFill.style.width = '0%'; progBar.appendChild(progFill); const progFile = document.createElement('div'); progFile.className = 'prog-file'; - const progTally = document.createElement('div'); progTally.className = 'prog-tally'; - progWrap.appendChild(progBar); progWrap.appendChild(progFile); progWrap.appendChild(progTally); + progWrap.appendChild(progBar); progWrap.appendChild(progFile); contentEl.innerHTML = ''; contentEl.appendChild(progWrap); const threshold = document.getElementById('threshold-input').value || '0.05'; @@ -1362,10 +1392,6 @@ async function dayHighlights(dayId, analyzableFiles) { const f = analyzableFiles[i]; progFile.textContent = `${i + 1} / ${n} — ${f.name}`; progFill.style.width = `${(i / n) * 100}%`; - const tallyParts = []; - if (nCached) tallyParts.push(`${nCached} cached`); - if (nLive) tallyParts.push(`${nLive} analysed`); - progTally.textContent = tallyParts.join(' · '); try { const r = await fetch('/api/analyze?file=' + encodeURIComponent(f.name) + '&threshold=' + encodeURIComponent(threshold) @@ -1375,8 +1401,8 @@ async function dayHighlights(dayId, analyzableFiles) { } catch(e) {} } progFill.style.width = '100%'; - progFile.textContent = `Done — ${n} file${n!==1?'s':''} (${nCached} cached, ${nLive} analysed)`; - progTally.textContent = ''; + const doneExtra = nCached ? ` (${nCached} from cache)` : ''; + progFile.textContent = `Done — ${n} file${n!==1?'s':''}${doneExtra}`; if (!results.length) { contentEl.innerHTML = '
No analysable results.
';