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 = `Error: ${esc(d.error)}
`;
- 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 = `Error: ${esc(e.message)}
`;
- 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.
';