feat: persist analyses on reload, add re-analyse button and metadata, trim highlights tally
- _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 <noreply@anthropic.com>
This commit is contained in:
@@ -324,8 +324,24 @@ class _Handler(BaseHTTPRequestHandler):
|
|||||||
self._send(200, _HTML.encode('utf-8'), 'text/html; charset=utf-8')
|
self._send(200, _HTML.encode('utf-8'), 'text/html; charset=utf-8')
|
||||||
|
|
||||||
def _api_files(self):
|
def _api_files(self):
|
||||||
data = json.dumps(list_files(self.recordings_dir)).encode('utf-8')
|
files = list_files(self.recordings_dir)
|
||||||
self._send(200, data, 'application/json')
|
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):
|
def _api_analyze(self, qs):
|
||||||
filename = qs.get('file', [None])[0]
|
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-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-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-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}
|
.analysis-meta{font-size:10px;color:var(--muted);margin-top:5px;font-style:italic}
|
||||||
.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}
|
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)}
|
.empty{text-align:center;padding:60px;color:var(--muted)}
|
||||||
/* player row */
|
/* player row */
|
||||||
.player-row td{padding:0 10px 10px;background:var(--bg);border-bottom:1px solid var(--brd)}
|
.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}
|
|||||||
<label for="threshold-input">Analysis threshold:</label>
|
<label for="threshold-input">Analysis threshold:</label>
|
||||||
<input type="number" id="threshold-input" min="0" max="1" step="0.005" value="0.05"
|
<input type="number" id="threshold-input" min="0" max="1" step="0.005" value="0.05"
|
||||||
aria-describedby="threshold-hint">
|
aria-describedby="threshold-hint">
|
||||||
<span id="threshold-hint" class="controls-hint">RMS 0–1 · sections above this value are marked loud</span>
|
<span id="threshold-hint" class="controls-hint">RMS amplitude 0–1 (linear; 0.05 ≈ −26 dBFS) · sections above this are marked loud</span>
|
||||||
<label for="preroll-input" style="margin-left:16px">Pre-roll:</label>
|
<label for="preroll-input" style="margin-left:16px">Pre-roll:</label>
|
||||||
<input type="number" id="preroll-input" min="0" max="30" step="0.5" value="3"
|
<input type="number" id="preroll-input" min="0" max="30" step="0.5" value="3"
|
||||||
aria-describedby="preroll-hint">
|
aria-describedby="preroll-hint">
|
||||||
@@ -954,6 +971,11 @@ async function analyse(idx, filename, cell, btn) {
|
|||||||
cell.innerHTML = '<div class="spin" aria-live="polite" aria-busy="true">Analysing…</div>';
|
cell.innerHTML = '<div class="spin" aria-live="polite" aria-busy="true">Analysing…</div>';
|
||||||
const threshold = document.getElementById('threshold-input').value || '0.05';
|
const threshold = document.getElementById('threshold-input').value || '0.05';
|
||||||
const minGap = document.getElementById('min-gap-input').value || '2';
|
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 {
|
try {
|
||||||
const r = await fetch('/api/analyze?file='+encodeURIComponent(filename)
|
const r = await fetch('/api/analyze?file='+encodeURIComponent(filename)
|
||||||
+'&threshold='+encodeURIComponent(threshold)
|
+'&threshold='+encodeURIComponent(threshold)
|
||||||
@@ -961,17 +983,14 @@ async function analyse(idx, filename, cell, btn) {
|
|||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (d.error) {
|
if (d.error) {
|
||||||
cell.innerHTML = `<div class="spin" role="alert">Error: ${esc(d.error)}</div>`;
|
cell.innerHTML = `<div class="spin" role="alert">Error: ${esc(d.error)}</div>`;
|
||||||
btn.disabled = false; btn.textContent = 'Analyse';
|
restoreBtn(); return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const box = document.createElement('div'); box.className='wbox';
|
const box = document.createElement('div'); box.className='wbox';
|
||||||
box.appendChild(drawWave(d.rms_display||[], d.sections||[], d.duration||0, filename));
|
box.appendChild(drawWave(d.rms_display||[], d.sections||[], d.duration||0, filename));
|
||||||
if (d.cached) {
|
|
||||||
const badge = document.createElement('span');
|
const meta = document.createElement('div'); meta.className='analysis-meta';
|
||||||
badge.className = 'cached-badge'; badge.textContent = 'cached';
|
meta.textContent = `threshold: ${threshold} · gap: ${minGap}s${d.cached ? ' · cached' : ''}`;
|
||||||
badge.title = 'Result loaded from cache — change threshold/gap and re-analyse to recompute';
|
box.appendChild(meta);
|
||||||
box.firstChild.after(badge);
|
|
||||||
}
|
|
||||||
|
|
||||||
const chips = document.createElement('div');
|
const chips = document.createElement('div');
|
||||||
chips.className='chips';
|
chips.className='chips';
|
||||||
@@ -995,10 +1014,16 @@ async function analyse(idx, filename, cell, btn) {
|
|||||||
chips.appendChild(q);
|
chips.appendChild(q);
|
||||||
}
|
}
|
||||||
box.appendChild(chips);
|
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);
|
cell.innerHTML=''; cell.appendChild(box);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
cell.innerHTML = `<div class="spin" role="alert">Error: ${esc(e.message)}</div>`;
|
cell.innerHTML = `<div class="spin" role="alert">Error: ${esc(e.message)}</div>`;
|
||||||
btn.disabled = false; btn.textContent = 'Analyse';
|
restoreBtn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1102,15 +1127,21 @@ function _attachFileRowHandlers(f, isRec, expanded, dayId) {
|
|||||||
if (canAnalyse) {
|
if (canAnalyse) {
|
||||||
const cell = document.getElementById('wave-'+i);
|
const cell = document.getElementById('wave-'+i);
|
||||||
const abtn = document.createElement('button');
|
const abtn = document.createElement('button');
|
||||||
abtn.textContent = 'Analyse';
|
|
||||||
abtn.setAttribute('aria-label', `Analyse loudness of ${f.name}`);
|
abtn.setAttribute('aria-label', `Analyse loudness of ${f.name}`);
|
||||||
if (isRec) {
|
if (isRec) {
|
||||||
|
abtn.textContent = 'Analyse';
|
||||||
abtn.disabled = true;
|
abtn.disabled = true;
|
||||||
abtn.title = 'Recording in progress — analyse after recording stops';
|
abtn.title = 'Recording in progress — analyse after recording stops';
|
||||||
} else {
|
|
||||||
abtn.addEventListener('click', () => analyse(i, f.name, cell, abtn));
|
|
||||||
}
|
|
||||||
cell.appendChild(abtn);
|
cell.appendChild(abtn);
|
||||||
|
} else if (f.cached_analysis) {
|
||||||
|
abtn.textContent = 'Re-analyse';
|
||||||
|
cell.innerHTML = '<div class="spin">Loading…</div>';
|
||||||
|
analyse(i, f.name, cell, abtn);
|
||||||
|
} else {
|
||||||
|
abtn.textContent = 'Analyse';
|
||||||
|
abtn.onclick = () => analyse(i, f.name, cell, abtn);
|
||||||
|
cell.appendChild(abtn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('pbtn-'+i)
|
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%';
|
const progFill = document.createElement('div'); progFill.className = 'prog-fill'; progFill.style.width = '0%';
|
||||||
progBar.appendChild(progFill);
|
progBar.appendChild(progFill);
|
||||||
const progFile = document.createElement('div'); progFile.className = 'prog-file';
|
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(progBar); progWrap.appendChild(progFile); progWrap.appendChild(progTally);
|
|
||||||
contentEl.innerHTML = ''; contentEl.appendChild(progWrap);
|
contentEl.innerHTML = ''; contentEl.appendChild(progWrap);
|
||||||
|
|
||||||
const threshold = document.getElementById('threshold-input').value || '0.05';
|
const threshold = document.getElementById('threshold-input').value || '0.05';
|
||||||
@@ -1362,10 +1392,6 @@ async function dayHighlights(dayId, analyzableFiles) {
|
|||||||
const f = analyzableFiles[i];
|
const f = analyzableFiles[i];
|
||||||
progFile.textContent = `${i + 1} / ${n} — ${f.name}`;
|
progFile.textContent = `${i + 1} / ${n} — ${f.name}`;
|
||||||
progFill.style.width = `${(i / n) * 100}%`;
|
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 {
|
try {
|
||||||
const r = await fetch('/api/analyze?file=' + encodeURIComponent(f.name)
|
const r = await fetch('/api/analyze?file=' + encodeURIComponent(f.name)
|
||||||
+ '&threshold=' + encodeURIComponent(threshold)
|
+ '&threshold=' + encodeURIComponent(threshold)
|
||||||
@@ -1375,8 +1401,8 @@ async function dayHighlights(dayId, analyzableFiles) {
|
|||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
progFill.style.width = '100%';
|
progFill.style.width = '100%';
|
||||||
progFile.textContent = `Done — ${n} file${n!==1?'s':''} (${nCached} cached, ${nLive} analysed)`;
|
const doneExtra = nCached ? ` (${nCached} from cache)` : '';
|
||||||
progTally.textContent = '';
|
progFile.textContent = `Done — ${n} file${n!==1?'s':''}${doneExtra}`;
|
||||||
|
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
contentEl.innerHTML = '<div class="quiet">No analysable results.</div>';
|
contentEl.innerHTML = '<div class="quiet">No analysable results.</div>';
|
||||||
|
|||||||
Reference in New Issue
Block a user