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.
';