- Day timeline axis now labels round wall-clock hours with tick marks
on the bar (start/end fallback when the span has <2 round hours),
replacing the arbitrary start/midpoint/end labels.
- Long chip lists (>12) collapse behind an aria-expanded toggle button
so the panel is not a wall of 50 buttons; J/K/U/I are unaffected.
- Chips group aria-labels shortened to "Day loud sections" / "Loud
sections" - the key-binding explanation lives only in the visible
note, not repeated in the group name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- J/K and Prev/Next now always step the clip queue in time order; the old
"Highlights only" checkbox could silently change what J/K did.
- Auto-advance is a three-way radio: don't auto-advance / auto-advance
(next in time) / auto-advance highlights only (next by loudness). It
only affects what plays when a clip ends.
- The "Top" highlight-count input is gone. U/I (and highlights-only
auto-advance) walk the full loudest-first ranking from scoreOrder()
with no top-N cutoff - review simply stops when the user stops.
- In-player U/I (full-file playback) step the same ranking, anchored on
the section under the playhead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Clip bar label is now the wall-clock time of occurrence plus queue
position ("03:46:20 to 03:46:22 (73 / 187)"); filename and score moved
to the hover tooltip. Works for both per-file and day queues via an
absStart epoch on every queue entry, derived from the filename clock
(listing date), falling back to in-file offsets for non-standard names.
- Day Highlights button toggles the panel; re-expanding reuses the
already-built results (re-armed J/K queue from dayHlSections) and only
recomputes when margin/gap/min-duration changed. A "analysed" suffix
marks days where every file has a cached analysis for the current
params; fetchAnalysis keeps f.cached_analysis fresh client-side.
- Day timeline now positions files by the filename clock instead of
mtime-duration, and the three axis labels (span start / midpoint /
end) carry explanatory tooltips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
A section's score is now its peak dB above the noise floor capped by
the sharpest rise within ONSET_SECONDS (0.5 s). Real events (voices,
impacts, barks) rise fast and keep their full prominence; a gradual
swell that outruns the 30 s floor blocks (gusts, distant approaching
cars) still flags but scores near zero, so score-ranked review (chips,
U/I highlights, "Highlights only" mode) surfaces events first. A
section starting in a file's first 0.5 s is scored against the floor
instead, so events cut off by a file split are not punished as swells.
Old cached analyses carry now-wrong scores, so the cache gains a
leading "detector" version key (DETECTOR_VERSION = 2) checked by both
_cached_analysis_params() and the /api/analyze cache hit path; v1
caches never match and are recomputed on the next analyse.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consolidates the existing per-knob hints (margin, grace period, min
duration, highlights-only review) into one ordered list of what to
adjust when the detector flags too much, and why ranking by score is
preferable to thinning the list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
J/K still steps through every queued section in time order; U/I steps
through only the highlights, defined as the top-N sections by score
(new "Top" input in the clip bar, default 50, matching the day chips).
A "Highlights only" checkbox makes J/K, Prev/Next, and Auto-advance
skip non-highlights too, so a day with thousands of detections plays
as a short reel of just the loudest events. Both key pairs also work
during full-file playback, and modified keypresses (Ctrl+K, Ctrl+U)
are no longer hijacked from the browser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cut downloads were named by byte offsets (`..._cut_740s-750s.flac`). They are
now named by the actual recording time the slice covers, e.g.
`20260523_22-31-30_22-32-30.flac` for a 22:31:30->22:32:30 cut of a recording
started at 22:00:00.
To make this reliable, the recording filename is now a fixed
`%Y%m%d_%H%M%S` start-time format (`FILENAME_FORMAT`) shared by isr.py and
web.py, replacing the user-configurable `filename_pattern` (web.py never reads
config.ini, so a custom pattern could not be parsed back). web.py parses the
start time out of the filename via `_recording_start()` and builds cut names
with `_cut_filename()`. The DATE column now also comes from the filename
(falling back to mtime only for non-standard names), since mtime is the last
write, not the start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
A single 100 ms RMS window above the noise floor used to become its own
section, so isolated pops (clicks, single raindrops) flooded a day with
thousands of sub-second clips like "21:18 to 21:18". Sections shorter
than min_duration (measured after min_gap merging, so a cluster of blips
spanning longer still flags) are now discarded.
Wired through all coupled places: CLI flag, /api/config, controls-bar
input, /api/analyze query param, and the analysis-cache head keys (old
two-key caches no longer match and are recomputed on next analyse).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CLAUDE.md gains a function-level code map of web.py/webui.html, a
"verifying changes" section (test scope, no-node UI checks, endpoint
smoke pattern, Windows commit-message gotcha), and findings recorded as
guard rails: why detection must stay adaptive (fixed threshold produced
~600 useless sections/day), why section playback must stay clip-based
(libsndfile FLACs have no SEEKTABLE so browser seeks bisect the file),
and the five places analysis params are coupled.
README gains the adaptive-detection and clip-review features up top, an
HTTP API table for scripting, and a corrected Docker analyses-cache
paragraph (the rw mount is ./recordings/analyses layered over the ro
recordings mount, not a separate ./analyses dir).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
FLAC duration cannot be derived from byte size (variable compression),
so unlike WAV the header cannot be patched from st_size alone. Instead,
every FLAC frame header carries its own frame/sample number: read the
last 64 KB of the growing file, scan backwards for a frame sync,
CRC-8-verify the header to reject false matches in compressed data,
and compute the exact samples recorded so far. STREAMINFO
total_samples (36 bits at a fixed offset) is rewritten in the served
bytes only - the on-disk file is never touched.
Overhead: one tail read per /stream request, active files only.
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>
- web.py: Python class bodies can't close over a name they also assign;
use a temporary alias _analyses_dir to break the self-reference
- docker-compose.yml: mount ./recordings/analyses:/recordings/analyses
instead of a separate ./analyses volume so cache lives inside recordings;
remove --analyses-dir flag (default now resolves correctly); remap
external port 8080→8050 for reverse proxy
- README.md / CLAUDE.md: update Docker port references to 8050
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Docker web container mounts ./recordings as :ro, causing every cache
write to fail silently (PermissionError swallowed by bare except).
Fix: add --analyses-dir flag (default: <recordings>/analyses for local runs).
docker-compose.yml adds ./analyses:/analyses (writable) and passes
--analyses-dir /analyses to web.py. Cache write failures now print a
warning instead of being swallowed silently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cache files now live in recordings/analyses/<filename>.analysis.json
(mirroring the relative path for files in subdirectories) rather than
alongside each audio file.
- _api_delete now removes the corresponding cache file after deleting audio.
- prune_orphan_analyses() runs at startup and removes any .analysis.json
whose audio file no longer exists (handles files deleted outside the UI).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After the first analysis of a WAV/FLAC file, the result is written to
<filename>.analysis.json next to the audio. Subsequent requests with the
same threshold and min_gap parameters return the cached result immediately
without re-reading the audio data. The cache is invalidated automatically
if either parameter changes. Written via temp-then-replace for thread safety.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Recordings table now groups files by day with collapsible headings
showing file count, total duration, and size; most recent day expands
by default, older days start collapsed
- Day highlights button runs loudness analysis across all WAV/FLAC files
in a day and renders a combined SVG activity timeline (blue = file
extents, orange = loud sections) with time labels
- Grace period control (default 2 s, max 300 s) merges loud sections
separated by less than this gap; exposed in the controls bar, via
--min-gap CLI flag, and as a per-request min_gap param on /api/analyze
- Delete re-renders via applyFilters so day headings stay consistent
Adds a DELETE /api/files/<name> endpoint that refuses to remove files
currently being recorded (409). The UI shows a red '✕ Delete' button per
row (disabled while REC), confirms before proceeding, and removes both
the data row and the hidden player row from the DOM on success without
a full page reload. README updated accordingly.
hw:0,0 is an exclusive ALSA device — darkice holding it caused arecord
to fail silently (stderr was /dev/null), leaving all recordings at 0 bytes
with no errors in the log.
asound.conf: defines a dsnoop virtual device 'shared_mic' that opens
hw:0,0 once and lets multiple processes capture simultaneously.
docker-compose.yml: mount asound.conf into the container as
/etc/asound.conf; add ipc: host so the container shares the host IPC
namespace (dsnoop uses System V shared memory which does not cross the
container IPC boundary without this).
config.example.ini: document the dsnoop setup and shared-device pattern.
README, CLAUDE.md: document the full setup procedure.
- Remove stale instruction to set output_directory = /recordings in Docker
- Document that docker compose down + up --build is required when updating
- Fix output_directory and log_file table descriptions
- Expand Web UI section: inline playback, FLAC waveform, live REC badge, WCAG
- Fix Docker notes: log_file path and log_file in Docker guidance
- Add rule to CLAUDE.md: always update and commit README.md with code changes