style: replace emoji glyphs in the UI with plain text labels
Buttons read Play/Hide, Cut, Delete, Download, Highlights, Prev/Next, Open in file, Refresh, Clear; the clip-bar close button uses a typographic multiplication sign. Day disclosure arrows switch to the text-presentation triangles (U+25B8/U+25BE) so they can never render as colored emoji. The red REC dot stays: U+25CF is text-presentation and takes the badge's CSS color. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -163,15 +163,15 @@ The browser UI (HTML/CSS/JS) lives in `webui.html`, which `web.py` loads at star
|
||||
Shows recordings grouped by day with collapsible sections. Features:
|
||||
|
||||
- **Day groups** — recordings are grouped under a collapsible day heading showing date, file count, total duration, and total size. The most recent day is expanded by default; older days start collapsed. Expanded state is preserved across filter changes.
|
||||
- **Day highlights** — click **★ Highlights** on any day heading to run loudness analysis across all WAV/FLAC files in that day and display a combined activity timeline SVG. Orange segments show when loud sections occurred relative to the day's time span; blue shows the file extents. Labels show the start, midpoint, and end times. When a day has more sections than fit as chips, the chips show the top 50 by score (loudest-above-background first) so the most promising events are reviewed first; J/K still steps through all sections in time order.
|
||||
- **Inline playback** — collapsible `▶ Play` button per row; audio loads lazily via a seekable `/stream/` endpoint with HTTP Range support. Metadata is fetched immediately so the duration is visible without pressing play.
|
||||
- **Day highlights** — click **Highlights** on any day heading to run loudness analysis across all WAV/FLAC files in that day and display a combined activity timeline SVG. Orange segments show when loud sections occurred relative to the day's time span; blue shows the file extents. Labels show the start, midpoint, and end times. When a day has more sections than fit as chips, the chips show the top 50 by score (loudest-above-background first) so the most promising events are reviewed first; J/K still steps through all sections in time order.
|
||||
- **Inline playback** — collapsible `Play` button per row; audio loads lazily via a seekable `/stream/` endpoint with HTTP Range support. Metadata is fetched immediately so the duration is visible without pressing play.
|
||||
- **Waveform analysis** — on demand per file; computes RMS per 100 ms window and marks sections that stand out above the background. Detection is **adaptive**: a rolling noise floor (20th percentile per 30 s block) is estimated across the file, and a section is flagged when the level rises at least *margin* dB (default 12) above that floor. Slow ambience changes — rain setting in, day/night traffic hum — move the floor instead of producing false positives. Each section gets a **score** (its peak dB above the floor) used to rank sections by how much they stand out. Supported for WAV and FLAC (FLAC requires `numpy` + `soundfile`). Pure-Python fallback for WAV when numpy is absent. Results are cached in `recordings/analyses/<filename>.analysis.json`; subsequent requests at the same margin, min-gap, and min-duration settings return instantly without re-reading the audio. The cache file is deleted automatically when the audio file is deleted. Orphaned cache files (audio deleted outside the UI) are pruned on startup.
|
||||
- **Grace period** — configurable in the controls bar (default 2 s). Loud sections separated by less than this gap are merged into one. Raise this (e.g. to 15–30 s) when a single event generates many timestamps due to brief quiet gaps within it.
|
||||
- **Min duration** — configurable in the controls bar (default 0.5 s). Loud sections shorter than this (after grace-period merging) are discarded, so isolated sub-second pops — a click, a single raindrop — don't flood a day with thousands of near-zero-length sections. Set to 0 to disable.
|
||||
- **Clip playback** — clicking a loud-section chip plays a short server-rendered WAV clip (`/api/clip`, pre-roll included) in a player bar at the bottom of the page. Playback starts instantly even for sections deep inside multi-hundred-MB FLACs, because the browser never has to seek the full file. **J** / **K** (or ⏮ / ⏭) step through the queued sections — one file's, or a whole day's after ★ Highlights — and **Auto-advance** plays the next section when one ends, turning a day's detections into a continuous review reel. **⤴ Open in file** switches to the full recording at the same position for context; each chip click also pre-fills the cut panel.
|
||||
- **Cut & download** — `✂ Cut` button opens the player row and reveals a cut panel. Enter start and end times in `m:ss` or `h:mm:ss` format and click **↓ Download cut** to receive an ffmpeg-trimmed copy without re-encoding. Requires ffmpeg (included in the Docker image).
|
||||
- **Clip playback** — clicking a loud-section chip plays a short server-rendered WAV clip (`/api/clip`, pre-roll included) in a player bar at the bottom of the page. Playback starts instantly even for sections deep inside multi-hundred-MB FLACs, because the browser never has to seek the full file. **J** / **K** (or the **Prev** / **Next** buttons) step through the queued sections — one file's, or a whole day's after **Highlights** — and **Auto-advance** plays the next section when one ends, turning a day's detections into a continuous review reel. **Open in file** switches to the full recording at the same position for context; each chip click also pre-fills the cut panel.
|
||||
- **Cut & download** — `Cut` button opens the player row and reveals a cut panel. Enter start and end times in `m:ss` or `h:mm:ss` format and click **Download cut** to receive an ffmpeg-trimmed copy without re-encoding. Requires ffmpeg (included in the Docker image).
|
||||
- **Filters** — live filename search and from/to date pickers above the table; applied client-side with no additional requests. Shows `N of M shown` when a filter is active.
|
||||
- **Delete** — `✕ Delete` button per row with confirmation prompt; disabled for files currently being recorded; sends `DELETE /api/files/<name>` and re-renders the table.
|
||||
- **Delete** — `Delete` button per row with confirmation prompt; disabled for files currently being recorded; sends `DELETE /api/files/<name>` and re-renders the table.
|
||||
- **Live REC badge** — files currently being written by `isr.py` show an animated REC indicator, polled every 5 seconds via `/api/status`. Duration for in-progress files shows `—` in the table (header is unfinalized until recording stops). The file list refreshes automatically when a recording starts, stops, or rolls over to a new split file (unless audio is playing).
|
||||
- **Listen while recording** — in-progress files are playable and seekable. For WAV and FLAC the server patches the (still unfinalized) header on the fly so the browser sees the real duration-so-far — for FLAC the exact sample count is parsed from the last frame header in the file tail. Reopening the player reloads the source to pick up newly recorded audio. Live responses are sent with `Cache-Control: no-store`.
|
||||
- **Fast loading** — analysis results are cached server-side on disk and client-side per session; cached waveforms load only for expanded day groups, and collapsed days fetch nothing until opened.
|
||||
|
||||
+21
-21
@@ -140,7 +140,7 @@ body.clip-open{padding-bottom:70px}
|
||||
<h1>ISR Archive</h1>
|
||||
<span id="subtitle" aria-live="polite" aria-atomic="true">Loading…</span>
|
||||
<span id="storage-info" aria-live="polite"></span>
|
||||
<button id="refresh-btn" aria-label="Refresh file list">↻ Refresh</button>
|
||||
<button id="refresh-btn" aria-label="Refresh file list">Refresh</button>
|
||||
</header>
|
||||
<div class="controls-bar">
|
||||
<label for="margin-input">Loudness margin:</label>
|
||||
@@ -167,20 +167,20 @@ body.clip-open{padding-bottom:70px}
|
||||
<input type="date" id="filter-from" aria-label="From date">
|
||||
<label for="filter-to">To:</label>
|
||||
<input type="date" id="filter-to" aria-label="To date">
|
||||
<button id="filter-clear" aria-label="Clear all filters">✕ Clear</button>
|
||||
<button id="filter-clear" aria-label="Clear all filters">Clear</button>
|
||||
</div>
|
||||
<div class="wrap" id="main">
|
||||
<div id="tbody" role="region" aria-label="Recordings archive"></div>
|
||||
<div id="empty" class="empty" style="display:none" role="status">No recordings found.</div>
|
||||
</div>
|
||||
<div id="clip-bar" hidden role="region" aria-label="Section clip player">
|
||||
<button id="clip-prev" aria-label="Previous section (J)">⏮</button>
|
||||
<button id="clip-next" aria-label="Next section (K)">⏭</button>
|
||||
<button id="clip-prev" aria-label="Previous section (J)">Prev</button>
|
||||
<button id="clip-next" aria-label="Next section (K)">Next</button>
|
||||
<span id="clip-label"></span>
|
||||
<audio id="clip-audio" controls preload="auto" aria-label="Section clip playback"></audio>
|
||||
<label id="clip-auto-label"><input type="checkbox" id="clip-auto" checked> Auto-advance</label>
|
||||
<button id="clip-context" title="Open the full recording at this position">⤴ Open in file</button>
|
||||
<button id="clip-close" aria-label="Close clip player">✕</button>
|
||||
<button id="clip-context" title="Open the full recording at this position">Open in file</button>
|
||||
<button id="clip-close" aria-label="Close clip player">×</button>
|
||||
</div>
|
||||
<script>
|
||||
const esc = s => String(s)
|
||||
@@ -226,7 +226,7 @@ let activePlayerIdx = null;
|
||||
let allFiles = [];
|
||||
// dayId -> boolean, persists expanded state across re-renders
|
||||
const dayExpanded = new Map();
|
||||
// cross-file day section list (populated by ★ Highlights)
|
||||
// cross-file day section list (populated by the day Highlights button)
|
||||
let dayActiveSections = [];
|
||||
let dayActiveId = null;
|
||||
|
||||
@@ -248,7 +248,7 @@ function closePlayer(idx) {
|
||||
const btn = document.getElementById('pbtn-'+idx);
|
||||
if (btn) {
|
||||
btn.setAttribute('aria-expanded','false');
|
||||
btn.textContent = '▶ Play';
|
||||
btn.textContent = 'Play';
|
||||
btn.setAttribute('aria-label','Play '+(recMap.get(idx) || ''));
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,7 @@ function togglePlayer(idx, filename) {
|
||||
activePlayerIdx = idx;
|
||||
prow.hidden = false;
|
||||
btn.setAttribute('aria-expanded','true');
|
||||
btn.textContent = '⏹ Hide';
|
||||
btn.textContent = 'Hide';
|
||||
btn.setAttribute('aria-label','Hide player for '+filename);
|
||||
audio.focus();
|
||||
}
|
||||
@@ -347,7 +347,7 @@ function seekToSection(idx, filename, startSec, endSec, sectionIdx) {
|
||||
// Sections play as small server-rendered WAV clips (/api/clip) in the bottom
|
||||
// bar instead of seeking the full recording, which is slow for big FLACs.
|
||||
// clipQueue holds the active review list (one file's sections, or a whole
|
||||
// day's); J/K and ⏮/⏭ step through it.
|
||||
// day's); J/K and the Prev/Next buttons step through it.
|
||||
let clipQueue = [];
|
||||
let clipCursor = -1;
|
||||
|
||||
@@ -557,11 +557,11 @@ async function deleteFile(idx, filename) {
|
||||
} else {
|
||||
const d = await r.json().catch(()=>({}));
|
||||
alert('Delete failed: '+(d.error||r.statusText));
|
||||
if (btn) { btn.disabled = false; btn.textContent = '✕ Delete'; }
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Delete'; }
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Delete failed: '+e.message);
|
||||
if (btn) { btn.disabled = false; btn.textContent = '✕ Delete'; }
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Delete'; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,13 +701,13 @@ function renderFiles(files) {
|
||||
<button class="day-toggle" id="daytgl-${dayId}"
|
||||
aria-expanded="${expanded}"
|
||||
aria-controls="daytbl-${dayId}">
|
||||
<span class="day-arrow" aria-hidden="true">${expanded ? '▼' : '▶'}</span>
|
||||
<span class="day-arrow" aria-hidden="true">${expanded ? '▾' : '▸'}</span>
|
||||
${esc(day)}
|
||||
<span class="day-meta">${fileStr}${durStr}${sizeStr}</span>
|
||||
</button>
|
||||
</h2>
|
||||
${canHl ? `<button class="day-hl" id="dayhln-${dayId}"
|
||||
aria-label="Show day highlights for ${esc(day)}">★ Highlights</button>` : ''}`;
|
||||
aria-label="Show day highlights for ${esc(day)}">Highlights</button>` : ''}`;
|
||||
section.appendChild(headBar);
|
||||
|
||||
// Highlights panel (hidden until button clicked)
|
||||
@@ -766,15 +766,15 @@ function renderFiles(files) {
|
||||
<button id="pbtn-${i}"
|
||||
aria-expanded="false"
|
||||
aria-controls="prow-${i}"
|
||||
aria-label="Play ${esc(f.name)}">▶ Play</button>
|
||||
aria-label="Play ${esc(f.name)}">Play</button>
|
||||
<a class="dl" href="/download/${encodeURIComponent(f.name)}"
|
||||
aria-label="Download ${esc(f.name)}">↓ Download</a>
|
||||
aria-label="Download ${esc(f.name)}">Download</a>
|
||||
<button id="cutbtn-${i}" class="cut"
|
||||
aria-label="Cut ${esc(f.name)}"
|
||||
${isRec ? 'disabled title="Cannot cut while recording"' : ''}>✂ Cut</button>
|
||||
${isRec ? 'disabled title="Cannot cut while recording"' : ''}>Cut</button>
|
||||
<button id="delbtn-${i}" class="del"
|
||||
aria-label="Delete ${esc(f.name)}"
|
||||
${isRec ? 'disabled title="Cannot delete while recording"' : ''}>✕ Delete</button>
|
||||
${isRec ? 'disabled title="Cannot delete while recording"' : ''}>Delete</button>
|
||||
</div>
|
||||
</td>`;
|
||||
dayTbody.appendChild(tr);
|
||||
@@ -790,7 +790,7 @@ function renderFiles(files) {
|
||||
<audio id="aud-${i}" controls preload="none"
|
||||
aria-label="Playback: ${esc(f.name)}"></audio>${durLabel}
|
||||
<div class="cut-panel">
|
||||
<span class="cut-label">✂ Cut:</span>
|
||||
<span class="cut-label">Cut:</span>
|
||||
<label class="cut-field">Start
|
||||
<input type="text" id="cut-start-${i}" class="cut-time" placeholder="m:ss or h:mm:ss">
|
||||
</label>
|
||||
@@ -799,7 +799,7 @@ function renderFiles(files) {
|
||||
</label>
|
||||
<button id="cut-dl-${i}" class="cut"
|
||||
${isRec ? 'disabled title="Cannot cut while recording"' : ''}
|
||||
aria-label="Download cut of ${esc(f.name)}">↓ Download cut</button>
|
||||
aria-label="Download cut of ${esc(f.name)}">Download cut</button>
|
||||
</div>
|
||||
</td>`;
|
||||
dayTbody.appendChild(prow);
|
||||
@@ -813,7 +813,7 @@ function renderFiles(files) {
|
||||
dayExpanded.set(dayId, nowExp);
|
||||
const tgl = document.getElementById('daytgl-' + dayId);
|
||||
tgl.setAttribute('aria-expanded', nowExp);
|
||||
tgl.querySelector('.day-arrow').textContent = nowExp ? '▼' : '▶';
|
||||
tgl.querySelector('.day-arrow').textContent = nowExp ? '▾' : '▸';
|
||||
headBar.classList.toggle('open', nowExp);
|
||||
document.getElementById('daytbl-' + dayId).hidden = !nowExp;
|
||||
if (nowExp) autoloadDayAnalyses(dayId);
|
||||
|
||||
Reference in New Issue
Block a user