c84b7d8222
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>
4.8 KiB
4.8 KiB
CLAUDE.md
Guidance for Claude Code when working in this repository.
Rules
- Always update
README.mdwhen user-facing behaviour changes (flags, endpoints, Docker setup, features), and commit it in the same commit as the code change. README is the external reference; CLAUDE.md documents internals. - Run
python -m pytest tests/after changingisr.pyorweb.py(tests cover the recorder and the loud-section detector).
Files
| File | Purpose |
|---|---|
isr.py |
Recorder: streams (Icecast/HTTP) + ALSA soundcards, time-aligned file splits |
web.py |
Archive browser: HTTP server, file listing, RMS loudness analysis, cut/delete |
webui.html |
Single-page UI (HTML/CSS/JS), loaded by web.py at startup — must sit next to web.py and be copied in the Dockerfile |
config.ini |
Recording sources; copy from config.example.ini. [general] gives defaults, every other section is a source (type = stream or type = soundcard) |
asound.conf |
dsnoop device shared_mic so ISR and other ALSA apps can share a soundcard |
Commands
python isr.py [config.ini] # recorder; --list-devices to list ALSA inputs
python web.py # web UI on :8080 (--dir, --port, --margin, --min-gap, --analyses-dir)
python -m pytest tests/ # test suite
docker compose up -d / down # web UI mapped to host port 8050
Dependencies: requests (streams), numpy + soundfile (FLAC output and FLAC/waveform analysis — both optional, code degrades gracefully).
Non-obvious internals
- Recorder/web coupling is one file:
RecorderManageratomically writesrecordings/status.jsonevery 2 s listing in-progress files; deleted on clean shutdown.web.pyreads it to show REC badges and to refuse analyse/cut/delete on active files. In-progress WAV/FLAC headers are unfinalized, so durations are not read for active files. - Stream splits: OGG/Opus/FLAC codec headers are extracted from the first ~16 KB of each connection and prepended to every split file so each file plays standalone. A new file is always opened on reconnect (gap in stream). MP3/AAC need no headers.
- Split timing: files split at clock-aligned boundaries (
get_next_split_time()), e.g.split_minutes = 60→ on the hour. - ALSA: capture spawns
arecordas a subprocess, raw PCM read in 100 ms chunks by a thread. Device spec resolution:default→ exacthw:X,Y→ partial name → fallback to any literal ALSA PCM name (soshared_micfrom asound.conf works without appearing inarecord -l). - Shutdown: SIGTERM is converted to KeyboardInterrupt in
main();RecorderManager.stop()joins all threads against a single shared 25 s deadline to stay inside Docker'sstop_grace_period: 30s. - Loud-section detection is adaptive: per-window dB is compared against a rolling noise floor (
NOISE_PERCENTILE-th percentile perNOISE_BLOCK_SECONDSblock, min-smoothed over ±2 blocks so events can't raise their own floor; clamped to ≥MIN_RMS). A section needsmargindB of prominence and carries ascore(peak dB above floor) used for ranking. Tests intests/test_web.py. - Analysis cache: results stored as
<analyses-dir>/<file>.analysis.jsonkeyed by margin+min_gap; orphans pruned at web startup. In Docker the recordings mount is read-only for the web container, so the cache uses a separate./analysesbind mount. Themarginandmin_gapkeys MUST stay first in the cache JSON —_cached_analysis_params()reads only the first 256 bytes to avoid parsing the large embedded result. Oldthreshold-keyed caches never match and get overwritten on the next analyse. - Analyze responses:
/api/analyzereturnsrms_display(~800 points), never the full per-window RMS list — the UI doesn't use it and it is ~45x larger. - HTTP/1.1 keep-alive:
_Handler.protocol_version = 'HTTP/1.1'; every response path must set an accurateContent-Length._copy_to_response()force-closes the connection if it under-delivers (file truncated mid-serve). - Live playback: for files listed in status.json,
/stream/patches the header on the fly so the browser sees the duration recorded so far and can seek; responses getCache-Control: no-store. WAV:_live_wav_headerderives sizes from the byte count. FLAC:_live_flac_headerparses the sample count out of the last frame header in the file tail (CRC-8-verified to reject false sync matches) and rewrites STREAMINFO total_samples — duration is NOT derivable from byte size for FLAC. - Path safety: every file parameter in
web.pygoes through_safe_path(), which resolves and verifies the path stays inside the recordings dir. - dsnoop in Docker: sharing the soundcard requires
asound.confon the host andipc: hostin docker-compose (dsnoop uses shared memory across the container boundary).