Compare commits

..

2 Commits

Author SHA1 Message Date
admin 9f1a6ff711 feat: O key opens the current clip in the full file
Extracts the "Open in file" button handler into openClipInFile() and
binds O in the shared keydown listener as a keyboard alternative, so
clip review never needs the mouse: J/K/U/I to step, O to drop into the
full recording for context.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:24:30 +02:00
admin 13419244e8 fix: clip bar no longer visible before any clip has played
The author rule #clip-bar{display:flex} overrides the UA stylesheet's
[hidden]{display:none}, so the hidden attribute toggled by playClip()/
hideClipBar() had no visual effect: the bar rendered from page load and
the close button appeared to do nothing. Restate display:none for the
hidden state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:21:41 +02:00
3 changed files with 15 additions and 7 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ Dependencies: `requests` (streams), `numpy` + `soundfile` (FLAC output and FLAC
`webui.html` (one `<script>` block): `webui.html` (one `<script>` block):
- Clip review: `clipQueue`/`clipCursor` globals, `playClip()`, `playFileSection()`, `hideClipBar()`; markup is the `#clip-bar` div. - Clip review: `clipQueue`/`clipCursor` globals, `playClip()`, `playFileSection()`, `hideClipBar()`; markup is the `#clip-bar` div.
- Day review: `dayHighlights()` builds `dayActiveSections` (chronological); `jumpToDaySection()` arms the queue. - Day review: `dayHighlights()` builds `dayActiveSections` (chronological); `jumpToDaySection()` arms the queue.
- J/K/U/I: single document-level `keydown` listener — clip queue takes priority, in-player `currentTime` stepping is the fallback when no queue is armed. U/I (and the `#clip-hl-only` checkbox, which also affects J/K, Prev/Next, and auto-advance) restrict stepping to highlights: the top `#clip-top` (default 50) sections by score, computed on demand by `topScoreSet()`; `stepClip()` is the shared queue-stepping path. - J/K/U/I/O: single document-level `keydown` listener — clip queue takes priority, in-player `currentTime` stepping is the fallback when no queue is armed; O calls `openClipInFile()` (shared with the "Open in file" button). U/I (and the `#clip-hl-only` checkbox, which also affects J/K, Prev/Next, and auto-advance) restrict stepping to highlights: the top `#clip-top` (default 50) sections by score, computed on demand by `topScoreSet()`; `stepClip()` is the shared queue-stepping path.
- Analysis: `fetchAnalysis()` (session `analysisCache`), `analyse()` (per-row render), `cachedParamsMatch()` (autoload guard). - Analysis: `fetchAnalysis()` (session `analysisCache`), `analyse()` (per-row render), `cachedParamsMatch()` (autoload guard).
## Verifying changes ## Verifying changes
+1 -1
View File
@@ -159,7 +159,7 @@ Shows recordings grouped by day with collapsible sections. Features:
- **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** used to rank it: its peak dB above the floor, capped by the sharpest rise within 0.5 s. Abrupt events — voices, impacts, barks — rise fast, so their score is their full prominence; a gradual swell (a gust, a distant approaching car) that drifts up faster than the floor can track still gets flagged, but scores near zero and sinks to the bottom of the highlight ranking. 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. - **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** used to rank it: its peak dB above the floor, capped by the sharpest rise within 0.5 s. Abrupt events — voices, impacts, barks — rise fast, so their score is their full prominence; a gradual swell (a gust, a distant approaching car) that drifts up faster than the floor can track still gets flagged, but scores near zero and sinks to the bottom of the highlight ranking. 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 1530 s) when a single event generates many timestamps due to brief quiet gaps within it. - **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 1530 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. - **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 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. **U** / **I** step through *highlights only*: the top-scored sections of the queue (count set by the **Top** input in the player bar, default 50). Ticking **Highlights only** makes J/K, Prev/Next, and Auto-advance skip non-highlights too, so a day with thousands of detections can be reviewed as a short reel of just the loudest events. The same keys work during full-file playback, seeking the open recording between (highlight) sections. **Open in file** switches to the full recording at the same position for context; each chip click also pre-fills the cut panel. - **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. **U** / **I** step through *highlights only*: the top-scored sections of the queue (count set by the **Top** input in the player bar, default 50). Ticking **Highlights only** makes J/K, Prev/Next, and Auto-advance skip non-highlights too, so a day with thousands of detections can be reviewed as a short reel of just the loudest events. The same keys work during full-file playback, seeking the open recording between (highlight) sections. **Open in file** (or the **O** key) 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). The cut is named with the real wall-clock span it covers — `<YYYYMMDD>_<HH-MM-SS>_<HH-MM-SS>.<ext>`, e.g. a 22:31:30→22:32:30 slice of a recording started at 22:00:00 becomes `20260523_22-31-30_22-32-30.flac`. - **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). The cut is named with the real wall-clock span it covers — `<YYYYMMDD>_<HH-MM-SS>_<HH-MM-SS>.<ext>`, e.g. a 22:31:30→22:32:30 slice of a recording started at 22:00:00 becomes `20260523_22-31-30_22-32-30.flac`.
- **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. - **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.
+13 -5
View File
@@ -125,6 +125,9 @@ svg.day-timeline{display:block;width:100%;height:22px}
#clip-bar{position:fixed;bottom:0;left:0;right:0;z-index:20;background:var(--surf); #clip-bar{position:fixed;bottom:0;left:0;right:0;z-index:20;background:var(--surf);
border-top:1px solid var(--brd);padding:8px 28px;display:flex;align-items:center; border-top:1px solid var(--brd);padding:8px 28px;display:flex;align-items:center;
gap:10px;flex-wrap:wrap} gap:10px;flex-wrap:wrap}
/* display:flex above beats the UA's [hidden]{display:none} — restate it, or
the bar is visible from page load and the close button looks dead */
#clip-bar[hidden]{display:none}
#clip-bar audio{flex:1 1 240px;min-width:180px;height:32px} #clip-bar audio{flex:1 1 240px;min-width:180px;height:32px}
#clip-label{font-size:12px;color:var(--muted);font-family:ui-monospace,monospace; #clip-label{font-size:12px;color:var(--muted);font-family:ui-monospace,monospace;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:38%} white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:38%}
@@ -182,7 +185,7 @@ body.clip-open{padding-bottom:70px}
<label id="clip-auto-label"><input type="checkbox" id="clip-auto" checked> Auto-advance</label> <label id="clip-auto-label"><input type="checkbox" id="clip-auto" checked> Auto-advance</label>
<label id="clip-hl-label" title="J/K, Prev/Next and auto-advance skip everything outside the top sections"><input type="checkbox" id="clip-hl-only"> Highlights only</label> <label id="clip-hl-label" title="J/K, Prev/Next and auto-advance skip everything outside the top sections"><input type="checkbox" id="clip-hl-only"> Highlights only</label>
<label id="clip-top-label" title="How many top-scored sections count as highlights">Top <input type="number" id="clip-top" min="1" step="1" value="50" aria-label="Number of top-scored sections treated as highlights"></label> <label id="clip-top-label" title="How many top-scored sections count as highlights">Top <input type="number" id="clip-top" min="1" step="1" value="50" aria-label="Number of top-scored sections treated as highlights"></label>
<button id="clip-context" title="Open the full recording at this position">Open in file</button> <button id="clip-context" title="Open the full recording at this position (O)" aria-label="Open in file (O)">Open in file</button>
<button id="clip-close" aria-label="Close clip player">&times;</button> <button id="clip-close" aria-label="Close clip player">&times;</button>
</div> </div>
<script> <script>
@@ -422,7 +425,9 @@ document.getElementById('clip-audio').addEventListener('ended', () => {
if (document.getElementById('clip-auto').checked) if (document.getElementById('clip-auto').checked)
stepClip(1, highlightsOnly()); stepClip(1, highlightsOnly());
}); });
document.getElementById('clip-context').addEventListener('click', () => { // "Open in file": switch from the current clip to the full recording at the
// same position. Bound to the clip-bar button and the O key.
function openClipInFile() {
const c = clipQueue[clipCursor]; const c = clipQueue[clipCursor];
if (!c) return; if (!c) return;
document.getElementById('clip-audio').pause(); document.getElementById('clip-audio').pause();
@@ -434,7 +439,8 @@ document.getElementById('clip-context').addEventListener('click', () => {
row.scrollIntoView({block: 'center'}); row.scrollIntoView({block: 'center'});
} }
seekToSection(c.fileIdx, c.filename, c.start, c.end, null); seekToSection(c.fileIdx, c.filename, c.start, c.end, null);
}); }
document.getElementById('clip-context').addEventListener('click', openClipInFile);
// filename|margin|gap|minDur -> analysis result, so re-renders (filtering, // filename|margin|gap|minDur -> analysis result, so re-renders (filtering,
// refresh) never refetch what this session already has // refresh) never refetch what this session already has
@@ -513,15 +519,17 @@ async function analyse(idx, filename, cell, btn, force = false) {
} }
} }
// J/K = previous/next section, U/I = previous/next highlight (top-N by score). // J/K = previous/next section, U/I = previous/next highlight (top-N by score),
// O = open the current clip in the full file.
// With "Highlights only" checked, J/K behave like U/I. // With "Highlights only" checked, J/K behave like U/I.
// Only when focus is not in an input. // Only when focus is not in an input.
document.addEventListener('keydown', e => { document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.ctrlKey || e.metaKey || e.altKey) return; if (e.ctrlKey || e.metaKey || e.altKey) return;
const key = e.key.toLowerCase(); const key = e.key.toLowerCase();
if (key !== 'j' && key !== 'k' && key !== 'u' && key !== 'i') return; if (key !== 'j' && key !== 'k' && key !== 'u' && key !== 'i' && key !== 'o') return;
e.preventDefault(); e.preventDefault();
if (key === 'o') { openClipInFile(); return; }
const dir = (key === 'j' || key === 'u') ? -1 : 1; const dir = (key === 'j' || key === 'u') ? -1 : 1;
const hlOnly = key === 'u' || key === 'i' || highlightsOnly(); const hlOnly = key === 'u' || key === 'i' || highlightsOnly();