Commit Graph
30 Commits
Author SHA1 Message Date
adminandClaude Opus 5 c91264c253 refactor: split web.py — detector and audio-format helpers into isrweb/
Pure move, no behaviour change. web.py 1221 -> 807 lines.

The reason is not file size but isolation of change. CLAUDE.md records
that the next step for detection, should flagging ever need improving, is
a spectral filter or an optional Silero VAD over candidate sections —
that work now lands entirely in isrweb/loudness.py and never has to open
the HTTP layer. The byte-level format code (FLAC frame parsing, CRC-8,
live header patching) is a second self-contained cluster, touched only
when a container format misbehaves.

  isrweb/loudness.py   229  detector + its tuning constants
  isrweb/audiofile.py  250  filename clock, WAV/FLAC headers, MIME types
  web.py               807  cache, listing, handler, main()

Import direction is strictly web.py -> {loudness, audiofile}; the two
leaves import neither each other nor web.py, which is what lets the
detector be imported and tested without an HTTP server.

A package rather than flat modules because `audiofile` is a real PyPI
name and `detect` is generic enough to collide; a local file silently
shadows an installed one, which fails confusingly rather than loudly.
web.py stays at the repo root so `python web.py`, the docker-compose
command and the webui.html lookup are all unchanged.

Each module keeps its own optional numpy/soundfile try-import rather than
sharing one. That block cannot meaningfully diverge — if numpy is
installed every copy says True — and sharing it would stop isr.py being a
standalone file you can drop on a Pi and run.

Tests mirror the modules: test_loudness.py, test_audiofile.py, test_web.py
(+ unchanged test_isr.py). New tests/conftest.py owns the sys.path setup
that was previously an inline hack in test_isr.py — which only worked for
the other modules because that file happened to import first — and
provides the write_wav fixture the split would otherwise have duplicated.

Verified:
* 78 passed, same count as before the split; each test file also passes
  standalone.
* All 21 moved functions diffed against HEAD:web.py — byte-identical.
* Import graph asserted acyclic; both leaves import with `web` absent
  from sys.modules.
* An analysis cache written *before* the split still hits afterwards,
  with identical sections — the one way a "pure" move could quietly
  invalidate every user's cache.
* Endpoint smoke (analyze bounds, cache hit, cached_analysis in the
  listing, clip Content-Length, 8-bit rejection) and the byte-serving
  harness (ranges, download Range, HEAD, 405, live-header patching whole
  and straddling) both re-run green.
* Dockerfile COPY set verified by building the image's file tree in a
  temp dir and importing web.py with nothing else present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 23:14:54 +02:00
adminandClaude Opus 5 138f6db0a5 refactor: one Range-capable file sender; HEAD support; assorted hygiene
web.py
* _download and _stream each had their own copy loop; _download had no
  Range support at all, so an interrupted download of a multi-hundred-MB
  recording had to restart. Both now go through _serve_bytes(), which
  keeps _stream's live-header `prefix` trick (the patched bytes are the
  same length as the header they replace, so Range math is unaffected).
* do_HEAD answers / , /stream/ and /download/ as a body-less do_GET.
  /api/clip and /api/cut return 405 instead — generating a clip or
  shelling out to ffmpeg only to discard the result is worse than not
  answering.
* _api_delete drops the deleted file's _DURATION_CACHE entry, which was
  never evicted.
* WAV durations are rounded to 2dp like FLAC's already were.

isr.py
* _open_output_file flushed the buffer and then called
  close_current_file(), which is overridden to flush again.
* log_level/log_file were merged into the dict copied into every
  source's config; no recorder reads them. Read them where they are used.

webui.html
* scoreOrder() is memoised on the section array's identity — U/I
  otherwise re-sorted a whole day (thousands of sections) per keypress.

Verified with a byte-level harness: full/partial/open-ended/unsatisfiable
ranges byte-identical to disk, download Range, HEAD on all three
supported paths returning headers with an empty body, 405 on /api/clip,
and live-header patching still correct both whole-file and for a range
straddling the patched/disk boundary.

Not done from the audit: BaseRecorder.generate_filename's mkdir per file
open. It runs once per split (once an hour by default), so "once at
startup suffices" was not worth a behaviour change. `nul` stays in
.gitignore — it guards against a Windows shell artifact on a Windows box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:29:57 +02:00
adminandClaude Opus 5 0e7ea82da6 refactor: de-duplicate webui.html — shared ranking step, one param reader, one day-arm
No behaviour change. Targeted extractions only: the globals-heavy state
machine is left alone deliberately, since its interactions encode
screen-reader semantics with no JS test harness to catch a regression.

* The U/I loudness walk was implemented twice — once in stepClip's
  by-score branch, once in the keydown handler's in-player branch —
  including the anchor resolution, the bounds check and both boundary
  announcements. Extracted nextInOrder(order, pos, dir, jump). Verified
  behaviourally identical to both originals by exhaustive comparison
  over every (length, cursor, direction, shift) combination.

* The five coupled globals that arm a day's queue were set in two
  places (fresh build, re-open of a kept panel). Now armDayQueue().

* The three analysis inputs were read in three places, two of them with
  hardcoded fallbacks '12'/'2'/'0.5' — duplicating web.py's constants
  and the value= attributes, so the same three numbers lived in four
  client-side places. analysisParams() is now the only reader and the
  fallbacks are gone: an empty input sends an empty param, which
  _qs_float() resolves to the server default. hlParams() is its values
  joined.

* recMap (idx -> filename) duplicated allFiles; dropped, with
  fileNameByIdx() for the one label that needed it. pollStatus iterates
  allFiles and lets a missing badge element mark an unrendered row.

* analyse()'s btn is optional, so autoloadDayAnalyses stops fabricating
  a detached button just to have something to disable; the error path
  still produces a retry button either way.

* Unused dayId parameter on _attachFileRowHandlers; stale "waveform"
  CSS comment; vestigial .chip / button.chip split (all chips are
  buttons).

Verified: getElementById cross-check against declared and dynamic ids
(no unresolved), page serves and parses with every required id present,
pytest 78 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:15:49 +02:00
adminandClaude Opus 5 625ee0e7db refactor: simplify web.py — one param parser, plain cache reads, no generated handler class
No behaviour change beyond dropping an unread response field.

* _api_analyze had three identical parse-and-clamp blocks differing only
  in name/default/bounds. One _qs_float() helper; adding a fourth param
  is now a line rather than a copy of the block.

* _cached_analysis_params regexed four keys out of the first 256 bytes of
  each cache file to avoid parsing "the embedded result (hundreds of
  KB)", which forced those keys to be written first — a constraint
  documented in three places and silently fatal if ever violated. The
  RMS arrays that justified it were dropped when the waveforms went; a
  cache is now a few floats and a section list, so json.load is both
  simpler and immune to key order.

* /api/analyze no longer returns `window`. It existed to place RMS bars
  and has had no consumer since the waveform SVGs were removed.

* main() generated an anonymous _Handler subclass just to inject five
  attributes, needing an alias to work around class-body scoping. Set
  them on _Handler directly.

* `import sys` moved out of _Server.handle_error to the top of the file.

Verified with the CLAUDE.md endpoint smoke pattern: /api/analyze section
bounds, cache round-trip through the rewritten reader, cached_analysis
surfacing in /api/files, and /api/clip Content-Length == len(body) ==
44 + frames*channels*2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:46:14 +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 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 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 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 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 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 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 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
admin e67e27f047 fix: share soundcard between darkice and ISR via ALSA dsnoop
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.
2026-04-26 14:21:31 +02:00
admin f84b36687b docs: update README for Docker fix, FLAC/streaming features; add README rule to CLAUDE.md
- 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
2026-04-26 13:17:26 +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 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