diff --git a/CLAUDE.md b/CLAUDE.md index 8ee77aa..4af8041 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,14 +5,16 @@ Guidance for Claude Code when working in this repository. ## Rules - **Always update `README.md`** when 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 changing `isr.py` or `web.py` (tests cover the recorder and the loud-section detector). +- Run `python -m pytest tests/` after changing `isr.py`, `web.py` or `isrweb/` (tests cover the recorder, the detector, the filename clock and the HTTP layer). Test files mirror the modules: `test_isr.py`, `test_loudness.py`, `test_audiofile.py`, `test_web.py`; `tests/conftest.py` owns the sys.path setup and the `write_wav` fixture. ## 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 | +| `web.py` | Archive browser: HTTP server, file listing, analysis cache, cut/delete. Entry point — stays at the repo root so `python web.py` and the `webui.html` lookup work | +| `isrweb/loudness.py` | The adaptive loud-section detector. Pure computation, imports nothing from the app — future spectral/VAD work lands here and never touches HTTP | +| `isrweb/audiofile.py` | What an audio file is and what its name means: the filename clock, FLAC/WAV header parsing, live-header patching. Also a leaf | | `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 | @@ -30,11 +32,17 @@ Dependencies: `requests` (streams), `numpy` + `soundfile` (FLAC output and FLAC ## Code map -`web.py`: +**Import direction is strictly `web.py → {isrweb.loudness, isrweb.audiofile}`.** The two leaves never import each other or `web.py`; keep it that way, it is what lets the detector be tested without an HTTP server. Each module carries its own optional numpy/soundfile try-import — that boilerplate cannot meaningfully diverge, and sharing it would stop `isr.py` being a standalone file. + +`isrweb/loudness.py`: - Detection: `_compute_rms_windows_wav()` / `analyze_flac()` produce 100 ms RMS windows (at 48 kHz — `WINDOW_SAMPLES` is a fixed sample count, so windows are wider on lower-rate files; `analyze_wav()` rejects anything that is not 16-bit, which the RMS decoder assumes) → `_noise_floor_db()` estimates the rolling floor → `_loud_sections()` emits scored sections → `_package_result()` shapes the `/api/analyze` payload. -- Clips: `_api_clip()` validates params, `_clip_wav()` / `_clip_flac()` stream the decoded slice, `_wav_header()` builds the 44-byte PCM header. + +`isrweb/audiofile.py`: - Filenames as a clock: `_recording_start()` parses the start time out of a filename stem; `_cut_filename()` turns a (stem, ext, start, end) into a wall-clock-named cut. Both the listing `date` field and `_api_cut()` use them. -- Live headers: `_live_wav_header()`, `_live_flac_header()` (+ `_flac_frame_samples()`, CRC-8 verified). +- Live headers: `_live_wav_header()`, `_live_flac_header()` (+ `_flac_frame_samples()`, CRC-8 verified). Plus `_wav_header()` (44-byte PCM header for clips), `_get_audio_duration()`, `MIME_TYPES`, `AUDIO_EXTENSIONS`. + +`web.py`: +- Clips: `_api_clip()` validates params, `_clip_wav()` / `_clip_flac()` stream the decoded slice. - Serving: `_serve_bytes()` is the single Range-capable file sender — `_stream()` (live header patch via its `prefix` argument, `no_store`) and `_download()` (`Content-Disposition`) both go through it, and it is what makes `do_HEAD` a body-less `do_GET`. Plus `_copy_to_response()`, `_safe_path()` (path traversal guard). `webui.html` (one `