Generic enforcement lives in
/STANDARDS. This document carries the decisions and project-specific values. Last verified: 2026-05-31 · Recheck cadence: per recording-law review + audio-stack/hardware change.
An on-device noise monitor that detects and logs barking/noise events — timestamps, durations, and sound levels — and generates an honest report, while never recording, storing, or transmitting audio. Primary target is a Raspberry Pi service; a browser PWA is a zero-hardware alternative. Built to provide objective data for a neighbor noise dispute.
- Problem. Vague noise complaints about Olive with no objective record on your side; recording the home would create legal/ethical problems and isn't necessary.
- Primary user. You (single-user, your apartment).
- Jobs to be done. "Log when it was actually loud, and for how long." · "Give me a clean report I can show property management." · "Do this without recording anyone."
- Evidence basis. A labeled test session (known barks/quiet) to validate event detection and thresholds.
- Vision. Honest, level-only evidence of the real noise pattern — no audio, no exaggeration.
- Scope (MoSCoW).
- Must: in-memory level computation (dBFS); event detection (threshold + min-duration + debounce); SQLite event log (no audio); report generator (charts + methodology + limitations); local-only operation.
- Should: configurable quiet-hours ✅; calibration helper (offset toward approximate SPL) ✅; a small local dashboard ✅ (EXP-05: static, serverless
status.html, see below). - Could: coarse on-device event tagging (bark-like vs ambient) computed from features without storing audio; CSV export.
- Won't (ever): record/store/transmit audio; cloud upload; any claim the device can prove a sound's source.
- Non-goals. Not surveillance; not a recorder; not a courtroom-grade SPL meter.
- Recording-law basis. Document why level-only + no-audio sidesteps two-party-consent/eavesdropping concerns; keep this front-and-center in design and report.
- Acoustics. Decide level metric (RMS → dBFS), the calibration-offset approach, and the device's stated limits (relative, not absolute, unless calibrated).
- Detection validation. Run a labeled session; tune threshold/duration/debounce; record false-positive/negative behavior.
- Headless + report-first. The monitor runs unattended; the deliverable is the report.
- Report design. Daily/hourly distributions, quiet-hours compliance, event counts, and a plain-language methodology + limitations section so it reads as honest, not adversarial.
- Accessibility. Reports/dashboard are keyboard-complete; every chart has a data-table equivalent; severity/levels not color-only. Release gate.
- Shape (Pi). Python service using
sounddevice/PortAudio: read frames → compute level in memory → event detector → SQLite (events only) → report generator (PDF/HTML + charts). - Shape (PWA alt). Web Audio API
AnalyserNodefor levels, IndexedDB for events, same report generator logic; still audio-never-persisted. - Data model.
Event(start, end, duration, peak_level, avg_level, [coarse_tag]);Calibration(offset, note). No audio fields exist anywhere. - Key decisions (ADRs). Level-only, audio-never-persisted (rejected: recording — legal/ethical, and unnecessary). Pi primary for reliable always-on (PWA as no-hardware option). In-memory processing with immediate discard (rejected: buffering raw audio to disk). Honest methodology section mandatory (rejected: bare numbers with no limitations).
- Zero-dependency, pure-Python core. Level math, detector, store, and report use only the standard library;
make verifyruns with no installs and only the optionalliveextra (sounddevice) is needed for real microphone capture. (Rejected: numpy in the core — unnecessary for RMS and adds a dependency to the always-run path.) - JSON config, not TOML. Target runtime is Python 3.9, which lacks
tomllib; config is JSON via the stdlib. (Rejected: a third-party TOML parser — avoid a dependency for config.) - Hand-rendered inline-SVG charts. Charts are deterministic SVG with a paired data-table, so report output is byte-stable (snapshot-testable) and accessible without a plotting library. (Rejected: matplotlib — heavy, non-deterministic output, harder a11y.)
- Fixed UTC-offset bucketing (
tz_offset_hours). A single-site monitor lives in one offset; bucketing against a fixed offset makes reports reproducible across machines. (Rejected: machine-local time — non-reproducible reports.) - Structural a11y gate as the enforced floor.
tests/test_a11y.pyenforces the mechanically checkable WCAG subset everywhere (no browser needed); pa11y/axe runs as a deeper layer inmake a11y/CI; the screen-reader walkthrough stays review-gated indocs/audits/. - Type-checking under 3.10 semantics. Code targets 3.9 at runtime (via
from __future__ import annotations) but is checked under mypy 3.10 (current mypy dropped 3.9 support); safe because annotations are not evaluated at runtime.
| Metric | Target | Measured by | Gate |
|---|---|---|---|
| Audio bytes written to disk or transmitted | 0 | no-audio test (asserts no audio write/IO path) | merge-blocking |
| Network egress in monitor | none | no-egress test | merge-blocking |
| Report includes methodology + limitations | always | report-content test | merge-blocking |
| Event-detection accuracy vs labeled session | meets stated threshold | eval test | review-gated |
| Report reproducibility (same log → same report) | deterministic | snapshot test | merge-blocking |
| axe violations (report/dashboard) | 0 | pa11y-ci | merge-blocking |
| Coverage | ≥ 85% / ≥ 80% | coverage | merge-blocking |
Testing. Unit (level math, detector thresholds/debounce, report assembly), integration (frame pipeline → event → log → report), eval (detection vs labeled session), a11y. A dedicated test proves no code path persists or transmits audio.
monitor/ (capture, level compute, event detector) [pi]
pwa/ (web-audio variant) [optional]
store/ (sqlite events, calibration)
report/ (charts + pdf/html + methodology)
docs/
- M0 — Scaffold & gates. ✅ Repo + CI (
/STANDARDSgates + axe + the no-audio + no-egress tests). Done:make verifygreen; no-audio test passes. - M1 — Level pipeline. ✅ In-memory RMS→dBFS with immediate frame discard. Done: levels stream with zero audio persisted (test-proven).
- M2 — Event detection. ✅ Threshold + min-duration + debounce → events to SQLite. Done: labeled-session eval passes.
- M3 — Report generator. ✅ Charts + distributions + quiet-hours + methodology/limitations. Done: report renders with limitations; structural a11y gate green, pa11y in CI.
- M4 — Calibration + config. ✅
olive-calibrateoffset helper,olive-tunelive meter, quiet-hours config. Done: calibration stored + shown in report. - M5 — PWA variant (optional). ✅ Web Audio version (
pwa/) sharing detection + report logic, IndexedDB, offline. Done: events logged with audio never persisted; Node tests pass. - M6 — Polish + validation. ✅ Detection tuning, CSV/print export, property-based tests. Done: all §7 gates pass and the eval threshold is met.
- Claude Code approach. Build the no-audio guarantee first and design so there is literally no API to write audio; make the limitations section non-optional in the report.
- DST-safe time zones. Bucketing and quiet hours use an IANA zone via
zoneinfo(not a fixed offset), so they stay correct across daylight-saving changes. Falls back to UTC if tzdata is absent. (Rejected: fixed UTC offset — wrong half the year.) - Frame-coverage accounting. The capture path counts frames seen vs dropped; the report's "Measurement conditions" discloses coverage so silent backpressure can't quietly undercount events. (Integrity complement to the no-audio guarantee.)
- Clock-integrity guard (FIX-10). ✅ Done. A
ClockGuardtracks wall (time.time) vs monotonic (time.monotonic) time during capture; divergence beyond a configurable tolerance (clock_jump_tolerance_s, default 2 s) is persisted to aclock_anomaliestable (schema v3) and disclosed in the report's "Measurement conditions" as a forward/backward jump with before/after wall times and delta — otherwise it states no anomalies. Important on RTC-less Pis where NTP sync lurches the clock and would silently smear event timestamps. Table is deliberately minimal and compatible with FIX-03's later gap table. (Integrity complement to frame-coverage accounting.) - Durability & lineage. SQLite runs in WAL with
synchronous=NORMAL; schema is versioned viauser_versionwith in-place migrations; asessionstable records device/placement/calibration/tz + coverage and links each event for traceability. Retention pruning is config-driven. (Rejected: ad-hoc schema with no upgrade path.) - Detection-parameter provenance (FIX-02). ✅ Each capture session records the threshold, min-duration, debounce, and sample-rate/frame-size in force when its events were logged (schema v3). The report sources the Methodology numbers from the session and, when settings changed across sessions, renders a "parameter epochs" table plus a disclosure that each event is described under the parameters active when it was logged. Legacy pre-v3 sessions read those columns back as
Noneand fall back to config. Done: migration + round-trip + two-session report tests pass. - Unattended ops.
resilient_sourcereconnects on device failure with capped backoff; a heartbeat JSON file is written atomically for watchdogs; a hardenedsystemdunit (PrivateNetwork,ProtectSystem) enforces local-only at the OS level too. - Time-driven heartbeat & crash-safe counters (FIX-04, done 2026-07-02). The heartbeat and the session frame counters are flushed on a wall-clock cadence (
checkpoint_interval_s, default 30 s), not only on events and in the finally block. Acheckpointed(...)generator piggybacks the periodic write on frame arrival (~10 Hz) with no timer thread and no sockets — the heartbeat stays file-based so the egress gate still holds — so a silent night keepsupdated_atfresh and a power cut leaves the last-checkpointedframes_seen/frames_droppedon disk instead of losing the run. (Rejected:sd_notify/socket watchdog — would import a network module and trip the no-egress guardrail.) - Runtime egress proof. In addition to the static import scan, a test booby-traps
socketand runs the full pipeline + report to prove no network access at runtime. - EXP-11 — Local automation hooks over AF_UNIX (Home Assistant). ✅ Opt-in, emit-only one-way feed of the heartbeat and per-event dict to a local
AF_UNIXdatagram socket (--ipc-socket/ipc_socket,""= disabled), so a same-host home-automation listener can supply confounder context. Sends are nonblocking and best-effort so a stalled listener cannot freeze capture. Allsocketuse is confined tomonitor/ipc.pybehind a surgical carve-out in the no-egress gate; canary tests prove the module opens onlyAF_UNIX(neverAF_INET/AF_INET6) and the default path still opens no socket. Documented HAcommand_lineexample in the README. (Rejected: an INET/localhost port — that would be network egress.) - Coarse tagging (opt-in). A cheap zero-crossing-rate feature, computed in memory and discarded, classifies events as bark-like/ambient; surfaced as a clearly-hedged "hint" in the report. No audio stored.
- ✅ Event anatomy (EXP-02) — bounded per-event envelope stats. Each event stores three seconds-valued shape descriptors — rise time to threshold+6 dB, total time spent above +6 dB, and the longest unbroken loud run — so reports tell one long drone from hundreds of sharp barks. Computed as O(1) running counters over levels/timestamps (no audio, no buffering) and carried end to end (detector → SQLite v7 → CSV/violations exports). Done: nullable columns migrate old rows to
None; the no-audio gate lists the new fields deliberately. - PWA parity. The browser variant re-implements the detector/level/report in JS with its own Node tests; documented as a parallel implementation sharing semantics and the honest framing.
- FIX-05 — PWA correctness: real timestamps and background-proof capture (done). Events were stamped with
AudioContext.currentTime(seconds since the context was created) while the report/CSV expect unix-epoch seconds; apwa/clock.jsanchor (Date.now()/1000 − currentTime) now maps every reading to epoch seconds viatoEpochSeconds. TherequestAnimationFramesampling loop (throttled to ~0 Hz in a backgrounded tab) is replaced by a steadysetIntervalcleared on stop;visibilitychangerecords{ kind: 'gap', start, end }coverage holes for hidden/locked periods, whichsummarize/CSV exports exclude from event counts and the report surfaces as honest "monitoring gaps." Any in-progress detector event is flushed on stop. Covered bypwa/clock.test.mjs. - FIX-06 Cross-implementation conformance harness (Python ↔ PWA). ✅ Done.
spec/detector/*.jsonholds language-neutral golden vectors (threshold>=boundary, min-duration filtering, debounce bridging/splitting, flush-at-end, zero min-duration, peak/avg over loud readings only). Bothtests/test_conformance.py(pytest,monitor.detector.Detector) andpwa/conformance.test.mjs(node --test,pwa/detector.js) replay the same vectors and assert equality to1e-9, so the two ports cannot drift silently. Intentional divergences (nocoarse_tag/calibration/sessions in the PWA; CSV timezone divergence) and the rule "changing detection semantics means changing a vector on purpose" are documented inspec/SEMANTICS.md, which also notes the quiet-hours/summarize extension point. - EXP-05 — Local ops console (static status page). ✅ The monitor renders a static
status.html(report/status.py) on each periodic check-in — latest level, heartbeat freshness, frame coverage, recorded monitoring gaps, and a recent summary. Written atomically (temp +os.replace), enabled fromhealth_pathor an explicitstatus_path, and best-effort so rendering can never stop capture. It reuses the report's structural-a11y floor and requires no server, network, or audio. (Rejected: a live HTTP server — a static file keeps the local-only, zero-egress guarantee.)
Setup guide (Pi + PWA); a documented "why level-only" note others in similar disputes can reuse.
- Recording law. Level-only + no audio is the core compliance posture; documented in the report and README.
- Honest use. The report states methodology and limitations and never claims to attribute a sound to a specific source.
- Privacy. Local-only, minimal data, no audio.
- Hosting/cost. A Raspberry Pi (or just a browser); no running cost; no cloud.
- Maintenance. Periodic recalibration; re-validate detection if the device moves.
- Sustainability. Self-contained and offline; nothing to keep paying for.
Top risks: (1) recording audio of the household or neighbors → never captured, stored, or transmitted (tested); (2) misleading evidence → mandatory methodology + limitations, no source-attribution claims; (3) any data leaving the device → local-only, no egress (tested); (4) inaccessible reports → full a11y with chart data-tables. Full treatment in RESPONSIBLE-TECH-AUDITS.md.