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>
- 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>
- 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>
- 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>
- _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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
_attachFileRowHandlers uses getElementById to wire up buttons; those
elements must already be in the document. Moving section.appendChild
and container.appendChild ahead of the file rows loop fixes the null
reference that silently dropped all rendered content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each day is now a div.day-section with a heading bar and its own
independent <table>, rather than rows inside a single monolithic table.
Toggle shows/hides the per-day table element directly. Highlights panel
is a div between the heading and table, not a table row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All days collapsed by default; only today auto-expands
- Day Highlights chips now show absolute HH:MM:SS timestamps and are
clickable — jumps to the right file's player, auto-opening it and
closing the previous one when switching files
- J/K keys navigate across all files in the highlighted day (day-level
mode) instead of staying within a single file; falls back to per-file
mode when no day highlights are active
- Collapsing a day clears its cross-file section state
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
The forward-skip threshold was `cur + 0.5`, which is smaller than
the preroll offset applied when landing on a section. After K placed
the cursor at `s.start - preroll`, the next K press found the same
section again because `s.start > (s.start - preroll) + 0.5` is true
whenever preroll > 0.5. Using `cur + preroll` as the threshold ensures
the current section's start is not > cur + preroll, so only the next
section matches.
Cut metadata: WAV and FLAC are now re-encoded (pcm_s16le / flac)
instead of stream-copied, so the container header is always rewritten
with the correct duration. Lossy formats keep -c copy.
J/K shortcuts: now also update the cut panel start/end fields to match
the section they landed on, and announce the section aloud.
Screen reader: added aria-live polite region; chip clicks and J/K
announce 'Section N of M: start to end'; boundary presses announce
'Beginning of sections' or 'End of sections'.
Pre-roll: configurable 'Pre-roll' seconds input in the controls bar
(default 3 s); all section seeks subtract the pre-roll from the target
position so the listener hears context before the loud section begins.
Adds a Cut panel inside every player row (✂ Cut button in the actions
column opens the row and focuses the Start field). Users type start and
end times in m:ss or h:mm:ss format; Download cut triggers
GET /api/cut which runs ffmpeg -c copy (no re-encode) and streams the
result as an attachment.
Clicking an analysis chip now also pre-fills the cut panel start/end
with the loud-section boundaries.
Server switched to ThreadingHTTPServer so ffmpeg runs don't block
other requests. ffmpeg added to Dockerfile apt install.
Client-side filter bar with live filename search and from/to date
pickers. Rendering is split into renderFiles() + applyFilters() so
filters can be re-applied without re-fetching. Subtitle shows
'N of M shown' when a filter is active. Clear button resets all fields.
Analysis chips are now buttons. Clicking one opens the player (if
not already open) and seeks to the section start. J skips to the
previous loud section, K to the next. Shortcuts are suppressed when
focus is inside an input field.
With preload=none the browser never fetches metadata, so Chrome
cannot populate the duration field for FLAC files. On player open:
set preload=metadata and call audio.load() to trigger a metadata-only
fetch. Also render a server-computed duration label beneath the audio
element as a fallback for formats the browser cannot parse.
WAV nframes and FLAC total_samples are both unfinalized while the
recorder has the file open, producing wildly wrong durations
(e.g. 53375995583:39:01). Return None (shown as —) instead.
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.
- Show total audio storage used and disk free/total in the page header
- Add per-page threshold input (seeded from server --threshold) so the
loudness threshold can be adjusted without restarting the server;
each Analyse request sends the current UI value to the backend
- Fix empty Duration column: FLAC, OGG, and Opus files now report
duration via soundfile header metadata (no full decode required)
- New /api/storage and /api/config endpoints support the above features
Clicking Analyse on a file currently being recorded caused libsndfile to
fail with 'Internal psf_fseek() failed' because an in-progress FLAC file
has no seektable (written only on close()).
Client: Analyse button is disabled with a tooltip while isRec is true.
Server: _api_analyze checks status.json and returns 409 with a readable
message if the file is still being recorded, before attempting to open it.
AudioSystem.find_device() now falls back to treating an unmatched spec as
a direct ALSA PCM name when the ALSA backend is available. Virtual devices
defined in asound.conf (dsnoop, plug, etc.) never appear in 'arecord -l'
so they were always rejected as 'not found', even when valid.
ALSAStream now captures arecord stderr via a reader thread instead of
discarding it, so errors like 'Device or resource busy' are logged as
warnings and visible in docker compose logs.
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.
_safe_path now uses path.is_file() instead of path.exists() so empty or
directory-traversal filenames never resolve to a directory and get served
as a 0-byte octet-stream download. This was the cause of the browser
downloading a file named 'recordings' with 0 bytes when Play was clicked.
Removed Content-Disposition: inline from _stream responses — it is not
needed for <audio> playback and it was what labelled the erroneous
directory response as 'recordings' in the save dialog.
- 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
docker-compose.yml: mount ./recordings at /app/recordings (matches
output_directory = recordings in config.ini); previously the recorder
wrote to /app/recordings while the web container read from /recordings,
causing all files to appear missing — explaining the 9-byte Not-found
download from /stream/ and the 0-byte recordings in the UI.
Add stop_grace_period: 30s so Docker waits long enough for files to close.
isr.py: replace per-thread join(timeout=5) with a shared 25 s deadline;
with N recorders the old code could exceed Docker's SIGKILL window and
leave WAV/FLAC files unclosed (corrupt headers).
web.py: add Content-Disposition: inline to /stream/ responses so
browsers never treat the audio response as a file download.
CLAUDE.md: document web.py endpoints, status.json lifecycle, corrected
Docker volume layout, and web.py CLI flags.
web.py:
- Extend loudness analysis to FLAC files via soundfile (numpy required)
- Add /stream/ endpoint with HTTP Range support for seekable inline playback
- Add collapsible ▶ Play button per row (hidden by default); src loaded lazily
- Add /api/status endpoint returning active filenames from status.json
- Animated ● REC badge on in-progress files, polled every 5 s
- Full WCAG: skip link, aria-expanded/controls, aria-label, role=img on
waveform SVG, role=list on loud-section chips, focus-visible outlines,
aria-live on subtitle, focus moved to <audio> when player opens
isr.py:
- Write recordings/status.json atomically every 2 s while recording
- Delete status.json on clean shutdown so web UI shows no stale state