Add /api/clip: decodes a WAV/FLAC slice server-side and returns a small
standalone 16-bit WAV with exact Content-Length (capped at 600s, cached
client-side since finished recordings are immutable). Active recordings
are refused like analyse/cut/delete.
Section chips and J/K now play these clips through a bottom player bar
instead of seeking the full recording - FLACs have no seek table, so
browser seeks bisected hundreds of MB with Range requests and playback
lagged or never started. The bar steps through a queue (one file's
sections or a whole day's via Highlights), auto-advances to the next
section on end for continuous review, and "Open in file" jumps to the
same position in the full recording for context.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the fixed RMS threshold with prominence over a rolling noise
floor (20th percentile per 30s block, min-smoothed so events cannot
raise their own floor, clamped to -54 dBFS). Slow ambience changes such
as rain or daytime traffic hum move the floor instead of flagging
everything; sections now need `margin` dB (default 12) of prominence.
Each section carries a score (peak dB above floor); day-highlight chips
show the top 50 by score when there are too many to list, so the most
striking events are reviewed first.
--threshold is replaced by --margin; analysis caches are now keyed by
margin+min_gap, old threshold-keyed caches never match and are
overwritten on the next analyse. Detector covered by tests/test_web.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server (web.py):
- /api/analyze no longer returns the full per-window RMS array (~45x
larger than the rms_display the UI actually renders); old caches are
stripped on read
- /api/files reads only the first 256 bytes of each analysis cache to
get threshold/min_gap instead of parsing the whole JSON
- durations cached by (mtime, size) instead of re-opening every audio
header per request; stat() race with deleted files guarded
- /api/storage no longer walks the recordings tree (used bytes now
computed client-side from the file list)
- HTTP/1.1 keep-alive enabled; short writes force-close the connection;
client-disconnect tracebacks from aborted seeks silenced
- all file copies bounded by the advertised Content-Length so files
growing during a response cannot desync the connection
Live recording playback:
- /stream/ patches in-progress WAV headers to the current file size so
browsers show real duration and can seek (on-disk header says 0
frames until the recorder closes the file)
- active files served with Cache-Control: no-store
- reopening the player for a recording file reloads the source to pick
up newly captured audio
UI loading:
- analyses lazy-load only for expanded day groups; collapsed days defer
fetching until opened, and auto-load only when cached parameters
match the current controls (no surprise mass recompute)
- client-side analysis cache shared by file rows and day highlights, so
re-renders and filters never refetch
- filename filter debounced (200 ms)
- file list auto-refreshes when the active recording set changes,
unless audio is playing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
web.py was 1700 lines, more than half of it a single embedded HTML
string. The page now lives in webui.html (loaded once at startup),
so the frontend gets real syntax highlighting and web.py is pure
Python. Dockerfile copies the new file alongside web.py.
Also: ASCII startup banner (the arrow glyph crashed web.py on Windows
when stdout was redirected to a cp1252 file), and README fixes —
document the ALSA PCM-name device fallback and drop the monitor
device row, which the ALSA backend never supported.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>