diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..57c09a2 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,154 @@ +# ISR Roadmap + +## notify.py — NTFY Loudness Notifications + +### Context + +Street ambience recorder. Goal: detect notable audio events (speech, thunder, +sustained unusual sounds) in hourly recording files and push a notification via +a self-hosted NTFY server. Generic short events (car horn, passing vehicle) +should be filtered out by a minimum section duration. + +### Design decisions + +| Topic | Decision | +|---|---| +| Detection | RMS + minimum section duration filter (KISS — no FFT for now) | +| Timing | Configurable: `immediate` / `daily` / `both` | +| Config | `[notify]` section in existing `config.ini` | +| Code structure | `notify.py` imports `analyze_wav` / `analyze_flac` from `web.py` (DRY) | +| Source name | Included in notification body; configurable display name per source | + +--- + +### Config additions (`config.example.ini`) + +Add a `[notify]` section to `config.ini`: + +```ini +[notify] +enabled = true +ntfy_url = https://ntfy.example.com/mytopic ; full URL incl. topic +mode = immediate ; immediate | daily | both +daily_time = 08:00 ; HH:MM — used in daily and both modes +debounce_minutes = 60 ; immediate mode: suppress repeat notifications within this window +min_section_duration = 2.0 ; seconds — sections shorter than this are ignored (filters car horns etc.) +min_sections = 1 ; number of qualifying sections required to trigger a notification +loudness_threshold = 0.05 ; RMS 0–1, same scale as web.py analysis threshold +``` + +Per recording source, add an optional `display_name`: + +```ini +[radio1] +type = stream +url = http://icecast.example.com:8000/live +display_name = Street mic north ; shown in notification; defaults to section name [radio1] +``` + +--- + +### Notification format + +``` +Title: ISR — Notable audio · Street mic north +Body: radio1_20260427_0300.wav + 3 notable sections (≥ 2.0 s each) + → 00:12 – 00:18 + → 01:45 – 01:52 + → 47:03 – 47:11 + Peak RMS: 0.312 +``` + +Daily digest example: + +``` +Title: ISR Daily Digest · 2026-04-27 +Body: Street mic north — 4 files with notable events + 03:00 file · 3 sections (peak 0.312) + 07:00 file · 1 section (peak 0.091) + 14:00 file · 2 sections (peak 0.204) + 21:00 file · 1 section (peak 0.178) +``` + +--- + +### Implementation plan + +#### Phase 1 — Core + +1. **`config.example.ini`** — add `[notify]` section and `display_name` key to + source section examples (as shown above). + +2. **`notify.py` — file watcher** + - Polls `recordings/status.json` every 30 s. + - Tracks which files were in `active` on the previous poll. + - When a file disappears from `active` it was just closed → queues it for + analysis. + - Skips files with extensions that cannot be analysed (anything other than + `.wav` / `.flac`). + +3. **`notify.py` — analysis + filter** + - Imports `analyze_wav` / `analyze_flac` from `web.py`. + - Applies `loudness_threshold` from `[notify]` config. + - Filters resulting sections to those with duration ≥ `min_section_duration`. + - Counts filtered sections against `min_sections` threshold. + +4. **`notify.py` — NTFY HTTP POST** + - Plain `urllib` POST to `ntfy_url` (no extra dependencies). + - Sets `Title` and message body as described above. + - Logs success / failure to stdout. + +#### Phase 2 — Cadence modes + +5. **Immediate mode with debounce** + - Fires right after the file closes and analysis passes. + - Persists last-notification timestamp per source to a small + `notify_state.json` in the recordings directory. + - Suppresses sending if last notification for that source was within + `debounce_minutes`. + +6. **Daily digest mode** + - Appends qualifying events to `notify_log.jsonl` in the recordings + directory (one JSON line per event: timestamp, source, filename, sections, + peak RMS). + - On each poll checks whether `daily_time` has passed today and no digest + has been sent yet (tracked in `notify_state.json`). + - Reads all undigested entries from `notify_log.jsonl`, groups by + `display_name`, sends one notification per source with notable activity. + - Marks entries as digested. + +7. **Both mode** + - Immediate path: only fires when peak RMS exceeds a second, higher + threshold (`alarm_threshold`, default `0.3`; add to `[notify]` config). + - Daily digest path: fires for everything that passes `min_sections`. + +#### Phase 3 — Integration + +8. **Docker** — optional `notify` service in `docker-compose.yml`: + ```yaml + notify: + build: . + command: python notify.py + volumes: + - ./recordings:/app/recordings + - ./config.ini:/app/config.ini:ro + restart: unless-stopped + ``` + +9. **README** — new section documenting `notify.py` usage, config keys, and + Docker setup. + +--- + +### Open questions (decide before implementing) + +- **Log rotation**: `notify_log.jsonl` grows indefinitely. Options: cap at N + days (configurable), cap at N MB, or leave cleanup to the user. No decision + made yet. +- **Multiple NTFY topics per source**: current design uses one global topic. + If per-source topics are ever needed, `ntfy_url` could be moved to the source + section and override the global one. +- **FFT / frequency analysis** (future): distinguishing thunder (low rumble, + 50–200 Hz) from speech (300–3000 Hz) from vehicles would reduce false + positives further. Deferred — requires `numpy` and adds meaningful complexity.