Commit Graph
74 Commits
Author SHA1 Message Date
adminandClaude Opus 5 b661e166ed fix: detector rejects non-16-bit WAV and stops padding the last section
_compute_rms_windows_wav honours sampwidth for frame arithmetic but
decodes as int16 unconditionally, so an 8- or 24-bit WAV produced
meaningless RMS values and therefore meaningless sections, silently.
The recorder only writes 16-bit, but --dir points at arbitrary
directories. analyze_wav now refuses anything else, turning a silent
wrong answer into a visible one.

_loud_sections closed mid-file sections at last_loud_t + window_dur but
the trailing one at the file duration. A file whose last loud event is
followed by less than min_gap of quiet reported a section stretched by
up to min_gap seconds, and the clip served for it carried that silence.
Now capped at the last loud window, consistent with every other section
and with the score, which only ever covered the loud windows.

DETECTOR_VERSION 2 -> 3 so existing caches are recomputed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:30:46 +02:00
adminandClaude Opus 5 9bb1229aa9 fix: two error paths that made a failure worse than the thing that failed
/api/clip: _clip_wav/_clip_flac send audio headers and then stream frames.
A decode failure mid-stream unwound into _api_clip's handler, which called
_json_err(500) — writing a second status line and a JSON body into a
response that had already promised Content-Type: audio/wav and an exact
Content-Length. Under HTTP/1.1 keep-alive that desyncs the connection for
every subsequent request on it, so one bad clip corrupted unrelated
traffic. Track whether the headers went out; if they did, log and force
the connection closed instead, the same way _copy_to_response already
handles under-delivery.

config.ini: the int() coercion for split_minutes/max_retries/etc sat
outside the try that wraps recorder construction, so `split_minutes =
sixty` aborted the entire config load with an unhandled ValueError rather
than skipping that one section like every other config error. Moved
inside. split_duration is also clamped to >= 1 — 0 divided by zero in
get_next_split_time().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:29:54 +02:00
adminandClaude Opus 5 1541110ffb refactor: collapse the single-implementation backend abstraction, fix device resolution
isr.py carried a plugin architecture — AudioBackend ABC, a _backend_classes
registry, _discover_backends, available_backends/get_backend, a
preferred_backend config knob — for exactly one implementation. It was
also the source of three real bugs:

* find_device() returned None before reaching the ALSA PCM-name fallback
  whenever `arecord -l` enumerated nothing, so the documented
  `device = shared_mic` (dsnoop from asound.conf) case failed outright on
  any host with no hardware card. The fallback is the whole point: those
  PCMs never appear in `arecord -l`.

* `device = monitor` could never resolve. ALSA has no generic monitor
  source and list_devices hardcoded is_monitor=False, so the keyword —
  advertised in config.example.ini as the "EASY" way to capture system
  audio — only ever produced "device not found". Removed; it is a
  PulseAudio idea that outlived the PulseAudio backend.

* list_all_devices() deduplicated by lowercased name "in case a device
  appears in multiple backends". With one backend what it actually did
  was delete the second of two identical USB cards. Dedup dropped.

find_device() now always returns a device, so _resolve_device's
unreachable "Available devices:" error is gone; an unknown spec is
passed to ALSA verbatim and logged. AudioDevice loses backend/
is_monitor plus channels/sample_rate — the latter were hardcoded 2/44100
and only ever printed by --list-devices, i.e. numbers with no bearing on
what gets recorded.

--list-devices now says outright that it lists hardware cards only, which
is where the shared_mic confusion started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:28:30 +02:00
adminandClaude Opus 5 4aa7f7253e fix: day grouping compares against the local date, not the UTC one
renderFiles() derived "today" from new Date().toISOString(), which is
UTC, and compared it to f.date — the recording start in local time,
parsed server-side out of the filename. In any non-UTC timezone the two
disagree for part of every day, and the current day's group (the one
always wanted open) renders collapsed: 00:00-02:00 local in UTC+2, most
of the evening in UTC-5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:21:17 +02:00
adminandClaude Opus 5 46af41b12d fix: soundcard capture detects a dead arecord instead of silently recording nothing
ALSAStream._read_loop just returned when arecord died or hit EOF, and
__exit__ never checked the exit status. SoundcardRecorder.record's inner
loop only slept and flushed, so nothing ever noticed: the recorder stayed
"running", kept rolling over to a new split file on schedule, and wrote
zero bytes into every one of them. The max_retries/reconnect path below
it was unreachable. Worst kind of failure for an unattended recorder —
files keep appearing, at the right times, with the right names.

ALSAStream now tracks when audio last arrived and exposes check_alive(),
which raises if the process exited or nothing has been captured for
SILENCE_TIMEOUT (15 s). The record loop calls it every 0.5 s tick, so a
dead device unwinds into the existing retry handler.

retry_count resets only when the stream actually delivered audio —
resetting on every healthy tick would let a device that opens but never
captures retry forever, which is the same silent failure by another
route.

Also: join the stderr thread in __exit__, and break _read_loop on EOF
rather than looping until poll() catches up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:21:05 +02:00
adminandClaude Fable 5 24569efe20 fix: clip queue resolves row index by filename, not stored _idx
clipQueue entries snapshotted fileIdx (= _idx) when the queue was armed,
but _idx is reassigned positionally on every load(), and pollStatus()
reloads the list whenever a recording starts, stops, or rolls over to a
new split file. With the newest-first sort a rollover shifts every index
by one, so an armed queue then pre-filled the wrong row's cut fields and
"Open in file" (O) opened the wrong row while streaming the clip's file
into it — during exactly the workflow of reviewing yesterday while
today's recording runs.

Store only the filename in queue entries and resolve the row index at
use time (fileIdxByName); if the file left the listing, Open in file
does nothing instead of acting on a wrong row.

Also drop seekToSection's sectionIdx parameter: the only remaining
caller passed null, leaving the announce branch dead since chips moved
to clip playback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 00:00:51 +02:00
adminandClaude Opus 4.8 89c95a70a2 feat: loudness walk (U/I) keeps its own cursor, independent of J/K
The clip bar had one shared cursor, so pressing U/I re-derived the
loudness rank from wherever you currently were. A J/K detour to review
clips around an interesting spot therefore hijacked the loudness walk:
returning to U/I continued from the J/K position, not from where the
ranking left off.

Add a separate scoreCursor that only U/I (and "highlights only" auto-
advance) move; J/K never touches it. So: U/I to a loud moment, J/K to
review the time-adjacent clips, U/I again resumes the ranking exactly
where you left it. scoreCursor resets to -1 on every explicit jump /
queue re-arm (hideClipBar, chip clicks, day-highlights arm) so the next
U/I re-anchors on the selected section.

Also label the position count "by time" (J/K) or "by loudness" (U/I) so
the blind user can hear which dimension the count is in — the two looked
identical before and switched meaning silently when changing keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:28:52 +02:00
adminandClaude Opus 4.8 46efbd6d75 docs: README clip-bar label and J/K vs U/I navigation accuracy
Bring the clip-playback section in line with the current UI:
- label example now shows the dB prominence and notes the visible label
  and screen-reader announcement are one identical string
- tooltip holds only the filename + in-file offset (score moved into
  the label)
- explain that the position count is the chronological index under J/K
  and the loudness rank under U/I, with the denominator always the full
  set (U/I has no top-N cutoff; only the chip display is capped)
- document that J/K and U/I share one queue and cursor, so U/I to find a
  spot then J/K to review the time-adjacent clips works as expected

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:17:42 +02:00
adminandClaude Opus 4.8 c5c63a76e8 fix: loudness navigation shows rank position, not time-order index
When stepping the clip queue by loudness (U/I, or "highlights only"
auto-advance), the position count showed the section's time-order index
(e.g. "30 / 426") instead of its place in the loudest-first ranking, so
walking highlights produced a count that jumped around. playClip now
takes a byScore flag: the displayed position is the rank in scoreOrder()
when navigating highlights ("1 / 426" = loudest), the time-order index
otherwise. The in-player U/I path already announced the rank.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 07:41:12 +02:00
adminandClaude Opus 4.8 8a532fcf57 fix: clip label and screen-reader announcement use one identical string
The clip bar's visible label and its aria-live announcement were two
different formats ("17:09:56 to 17:09:57 (30 / 426)" vs "Clip 368 of
426: 21:25:44 to 21:25:50"). Build one string and use it for both, and
include the section's dB prominence (as the highlight chips do):

  17:09:56 to 17:09:57 · +30 dB (30 / 426)

The dB moves out of the hover tooltip into the spoken/visible label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 07:36:35 +02:00
adminandClaude Opus 4.8 682b0522d3 feat: Shift+J/K/U/I jump to first/last/loudest/quietest section
Holding Shift with the section-navigation keys jumps straight to the
extreme in that direction instead of stepping one at a time:
- Shift+J / Shift+K -> first / last section in time order
- Shift+U / Shift+I -> loudest / quietest section

Works in both clip-queue navigation (stepClip gains a jump flag) and
in-player full-file navigation. Visible key-hint note and README updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 07:33:43 +02:00
adminandClaude Fable 5 5a9518e262 fix: screen-reader fixes for day highlights button and chips toggle
The Highlights button's aria-label overrode its content, so the visible
"· analysed" suffix was never announced — the analysed state is now
mirrored into the label (at render and when a highlights run completes).
The chips toggle's accessible text started with a stray space (left over
once the aria-hidden arrow is dropped), which read as an indent; the
separator space now lives inside the aria-hidden arrow span.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:16:39 +02:00
adminandClaude Fable 5 9c58e35546 feat: drop rms_display from /api/analyze
The UI no longer draws waveforms, so the server stops computing and
sending the ~800-point RMS preview; the payload is now just sections,
duration, window. Old analysis caches stay valid: rms/rms_display are
popped when serving a cache hit (same pattern as the earlier full-RMS
removal), so no DETECTOR_VERSION bump.

Verified: 63 tests pass; endpoint smoke test (fresh + cached analyze)
confirms no RMS keys and correct section detection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:02:47 +02:00
adminandClaude Fable 5 91701ce4d3 feat: remove per-file waveform SVG from the analyse view
Same rationale as the day timeline: purely visual, useless via screen
reader. The section count it carried in its aria-label moved into the
meta line ("N loud sections - margin: 12 dB - gap: 2s - min: 0.5s").
drawWave() and the svg.wave CSS are gone; the UI now renders no SVG at
all. /api/analyze still returns rms_display for API stability, but the
bundled UI no longer reads it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:58:14 +02:00
adminandClaude Fable 5 41d921a42a feat: remove decorative day-timeline SVG, linear highlights panel
The user is blind; the day activity timeline (and its hour labels) was
purely visual, non-interactive, and carried no information not already
in the chips/summary. The highlights panel is now linear text and
buttons in reading order: summary line ("N files analysed - M loud
sections"), key-hint note (now always shown, J/K/U/I is the primary
interface), chips toggle, chips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:49:59 +02:00
adminandClaude Fable 5 653084e90b feat: cleaner day highlights panel
- 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>
2026-06-12 11:39:00 +02:00
adminandClaude Fable 5 98d2d7085d feat: auto-advance radio, J/K decoupled from highlights, U/I ranked stepping
- 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>
2026-06-12 08:35:31 +02:00
adminandClaude Fable 5 2b0403d05d feat: wall-clock clip labels, collapsible day Highlights with analysed marker
- 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>
2026-06-12 08:28:23 +02:00
adminandClaude Fable 5 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
adminandClaude Fable 5 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
adminandClaude Fable 5 f6031cfa16 feat: onset-aware section scoring so slow swells rank at the bottom
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>
2026-06-11 14:57:19 +02:00
adminandClaude Fable 5 6431918989 docs: add a "too many sections per day" tuning note to the README
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>
2026-06-11 14:43:30 +02:00
adminandClaude Fable 5 f52eb62215 feat: U/I keys and "Highlights only" mode to review top-scored sections
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>
2026-06-11 14:38:56 +02:00
adminandClaude Opus 4.8 5e7620627b feat: name cut clips by wall-clock time; fix recording filename format
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>
2026-06-11 14:30:30 +02:00
adminandClaude Fable 5 2caf23f17d 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>
2026-06-11 09:03:32 +02:00
adminandClaude Fable 5 f3716d3ff1 feat: minimum section duration filter (--min-duration, default 0.5 s)
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>
2026-06-11 09:00:37 +02:00
adminandClaude Fable 5 e4d82483b5 docs: code map, regression findings, and HTTP API reference
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>
2026-06-10 16:21:37 +02:00
adminandClaude Fable 5 119e631faf feat: instant section playback via server-rendered clips
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>
2026-06-10 16:13:39 +02:00
adminandClaude Fable 5 c84b7d8222 feat: adaptive noise-floor loudness detection with section scoring
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>
2026-06-10 15:36:48 +02:00
adminandClaude Fable 5 16dd7cbe51 feat: duration and seeking for in-progress FLAC recordings
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>
2026-06-10 12:37:55 +02:00
adminandClaude Fable 5 fa055fc80a perf: do not hold the audio buffer lock during disk writes
_flush_buffer_to_file wrote (and for FLAC, encoded) every chunk while
holding buffer_lock, blocking the capture callback for the duration of
each disk flush. Swap the buffer out under the lock and write outside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:29:21 +02:00
adminandClaude Fable 5 8e496ec2c4 perf: faster page loads, live-recording playback and seeking fixes
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>
2026-06-10 12:29:13 +02:00
adminandClaude Fable 5 c445eb3e04 docs: slim CLAUDE.md down to rules, file map, and non-obvious internals
Drop class-by-class architecture listings that are derivable from the
code; keep only constraints a model cannot infer (status.json coupling,
header injection, read-only mount, shutdown deadline, dsnoop/ipc).
Add webui.html to the file map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:50:09 +02:00
adminandClaude Fable 5 907fd90a5e refactor: extract web UI page into webui.html
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>
2026-06-10 11:49:35 +02:00
adminandClaude Fable 5 2e3945dfa0 refactor: deduplicate web UI JS and improve screen reader support
- Remove fmtT (identical duplicate of fmtDur)
- Extract closePlayer() shared by togglePlayer, day collapse, and
  cross-file day section jumps
- Reuse seekToSection from jumpToDaySection instead of a copied
  open/seek/announce block
- Fix chip semantics: role=listitem on <button> overrode the button
  role for assistive tech; use role=group on the container instead
- Drop redundant aria-hidden toggling on the REC badge (hidden already
  removes it from the accessibility tree)
- Respect prefers-reduced-motion for the REC pulse animation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:44:30 +02:00
adminandClaude Fable 5 b9089f9c18 refactor: deduplicate web.py server code
- Extract _is_active() helper for the three duplicated status.json
  active-recording checks (analyze, delete, cut)
- Extract _copy_to_response() for the four duplicated 64 KB chunk
  copy loops (download, stream full/range, cut)
- Inline _get_wav_info into _get_audio_duration (sample rate and
  channels were never used)
- Remove unused HTTPServer import, dead frame_pos counter, and the
  redundant (ValueError, Exception) catch in _safe_path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:42:45 +02:00
adminandClaude Fable 5 792f2b1fd5 refactor: remove dead code from isr.py
- Drop unused struct import, AudioDevice.extra/description fields
- Remove unused get_preferred_backend() and the backend priority
  machinery (only one backend exists)
- Deduplicate SoundcardRecorder.close_current_file via super()
- Remove duplicate config-exists check in main()
- Simplify --list-devices output: drop dead monitor grouping and the
  nonexistent pipewire backend example

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:41:37 +02:00
adminandClaude Sonnet 4.6 4539ff78fa 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>
2026-06-03 09:32:04 +02:00
adminandClaude Sonnet 4.6 77e7e4ca9e fix: resolve NameError on startup, move analyses into recordings, remap port to 8050
- 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>
2026-06-03 08:46:49 +02:00
adminandClaude Sonnet 4.6 9ba084107b chore: remove dead test code and fix stale comment
tests/test_isr.py:
- Remove unused imports: io, SimpleNamespace, call
- Remove test_mp3_chunks_written_to_file — it mocked connect_stream but
  never called record(), making all the mock scaffolding dead; the actual
  assertion (bytes written to a file) is already covered by
  TestAudioFileWriter and TestGenerateFilename

isr.py:
- Update AudioDevice.backend comment: only the ALSA backend exists now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 23:32:34 +02:00
adminandClaude Sonnet 4.6 4aea07ae40 fix: separate analyses dir so caching works with read-only recordings mount
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>
2026-06-02 23:29:09 +02:00
adminandClaude Sonnet 4.6 b6b328dfb8 fix: switch audio to preload=auto when player opens or seek is triggered
preload='metadata' only fetches the header; every seek then requires a
fresh Range request and buffering delay. Switching to 'auto' lets the
browser start buffering the file immediately so seeking into it is fast.
Set both in togglePlayer (on open) and in seekToSection/jumpToDaySection
(in case the player was already open with the old metadata-only mode).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 23:24:17 +02:00
adminandClaude Sonnet 4.6 a68af56421 fix: cap day-highlights chips at 50; show J/K hint when over limit
Rendering 793 individual buttons is both visually overwhelming and slow.
When a day has more than 50 loud sections, replace the chip list with a
single note ("N sections — use J / K to navigate"). J/K navigation and
the SVG timeline still cover all sections.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 23:23:54 +02:00
adminandClaude Sonnet 4.6 7821f8823d feat: show analysis progress with per-file counter and cached tally
Day highlights: replaces the static "Analysing N files…" spinner with a
live progress bar, current filename ("3 / 8 — recording.wav"), and a
running tally of cached vs freshly-analysed files. Completes with a
summary line ("Done — 8 files (6 cached, 2 analysed)").

Single-file analyse: adds a small "cached" badge in the waveform box
when the result was served from cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:40:19 +02:00
adminandClaude Sonnet 4.6 75434ca96d feat: flag cached analysis responses with cached:true
Allows the UI to distinguish instant cache hits from live computation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:39:34 +02:00
adminandClaude Sonnet 4.6 eb774a0876 feat: move analysis cache to recordings/analyses/, prune orphans on startup
- 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>
2026-06-02 22:33:26 +02:00
adminandClaude Sonnet 4.6 e22c0059f6 feat: cache analysis results alongside audio files
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>
2026-06-02 22:30:12 +02:00
adminandClaude Sonnet 4.6 df32c263bc chore: remove NTFY notification roadmap
Not being pursued — the planning doc just adds noise.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:29:42 +02:00
adminandClaude Sonnet 4.6 af8113ba03 fix: auto-play audio when jumping to a loud section
seekToSection and jumpToDaySection were only seeking (setting currentTime)
but never calling play(), so the player would open and position correctly
but stay paused. The loadedmetadata-deferred path already handles slow audio
loading; play() is now called there too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:28:20 +02:00
adminandClaude Sonnet 4.6 d3ded71873 fix: proper WCAG heading structure for day sections
Wrap the disclosure button in <h2> per the WAI-ARIA accordion pattern.
Screen readers now announce a heading level 2 for each day and users
can navigate between days with the H key. The button's accessible name
comes from its text content (date + meta), not a redundant aria-label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 16:02:28 +02:00