Commit Graph

34 Commits

Author SHA1 Message Date
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 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
admin 0309faa993 fix: append day section to DOM before file rows so getElementById works
_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>
2026-05-20 15:55:58 +02:00
admin 1e6ad2f3de refactor: independent per-day heading + table, no nested mega-table
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>
2026-05-19 20:26:30 +02:00
admin f7e7d5bfaa feat: day-collapsed-by-default, cross-file highlights navigation
- 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>
2026-05-19 20:13:19 +02:00
admin d84056929a feat: day-grouped table, day highlights timeline, and grace period for loud sections
- 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
2026-05-07 04:06:30 +02:00
admin 47ce682821 fix: K key re-jumping to same section instead of advancing
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.
2026-04-29 21:06:02 +02:00
admin a70701f260 fix: cut metadata, J/K cut fields, screen reader section announcements
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.
2026-04-29 20:56:51 +02:00
admin eff9240b5d feat: add ffmpeg audio trim / cut download
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.
2026-04-29 20:41:18 +02:00
admin de667821b7 feat: add filename and date-range filters to recordings list
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.
2026-04-29 20:37:14 +02:00
admin d583620f8c feat: click loud-section chips to seek audio; J/K keyboard shortcuts
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.
2026-04-29 20:33:27 +02:00
admin 6d16b2c0a3 fix: resolve FLAC audio player showing 00:00 duration
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.
2026-04-29 20:25:05 +02:00
admin 7db0e0870f fix: skip duration read for active recordings to prevent garbage values
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.
2026-04-29 19:54:38 +02:00
admin c3575c712e feat: add delete button for recordings in web UI
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.
2026-04-27 00:14:56 +02:00
admin 8121564e8c feat: add storage info, UI threshold control, and FLAC/OGG duration
- 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
2026-04-26 17:04:58 +02:00
admin d7524afeff fix: disable Analyse on active recordings; guard server-side too
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.
2026-04-26 14:44:37 +02:00
admin 357b3e6ed9 fix: reject directories in _safe_path; remove Content-Disposition from _stream
_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.
2026-04-26 13:53:36 +02:00
admin da67523170 fix: Docker volume path, graceful shutdown deadline, inline stream headers
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.
2026-04-26 13:10:14 +02:00
admin 624f1f2664 feat: FLAC analysis, inline player, WCAG, live-recording status
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
2026-04-26 12:53:01 +02:00
admin 8254ccde86 Add Docker support, fix stale docs, translate UI to English
- Dockerfile + docker-compose.yml: two services (recorder + web) sharing
  ./recordings bind mount; recorder maps /dev/snd for ALSA soundcard access
- requirements.txt: requests, numpy, soundfile
- .dockerignore, updated .gitignore (add __pycache__, .pytest_cache)
- isr.py: add SIGTERM handler for clean Docker shutdown; fix stale error
  message that referenced removed PulseAudio/PipeWire/PortAudio backends
- web.py: translate all German UI strings to English
- config.example.ini: remove PipeWire/PulseAudio/PortAudio backend refs,
  simplify soundcard tips to ALSA only
- README.md: full rewrite as user guide (quick start, config reference,
  Docker notes, how it works)
- CLAUDE.md: update architecture section to reflect ALSA-only backend
- Delete changelog.txt and guide.md (internal session notes)
2026-04-26 10:56:55 +02:00