A privacy-first, on-device noise monitor that timestamps barking events and sound-level spikes and turns them into a clean report — so the next time a downstairs neighbor complains, you have objective data instead of a he-said-she-said. It measures sound levels and event metadata only. It never records, stores, or transmits audio. By design, there is no recording to leak, subpoena, or wiretap.
Status: Beta · Track: Personal (on-device monitor + report generator) · License: MIT · Data: on-device/local
Supported versions: pre-1.0, and no version has been tagged or released yet (CHANGELOG.md; CITATION.cff carries no date-released for the same reason). The policy for when one is: only the latest 0.y release line receives fixes; no LTS branch (REL-24).
You've been on the receiving end of vague noise complaints about Olive with nothing concrete to point to. A small device that runs in your apartment and logs when sound crossed a threshold and for how long gives you an honest, time-stamped record — useful for property management or just for understanding the real pattern — without the legal and ethical problems of recording your home (or your neighbors).
- Listens for levels, not content: computes sound level (dBFS, with a documented calibration offset) frame-by-frame in memory and discards the audio immediately.
- Detects events: threshold + minimum-duration + debounce → a "bark/noise event" with start, duration, and peak/average level.
- Logs to SQLite: events only — timestamps and levels, no audio.
- Generates reports: daily/hourly distributions, quiet-hours summaries, and event counts as an accessible HTML report with charts and a methodology + limitations section. An optional, opt-in tagged PDF/A-3a export (
--pdf, needs thepdfextra) is also available — see Standards Conformance for exactly what its accessibility claim does and does not cover. - Runs on-device: a Raspberry Pi service (primary) or a browser PWA (zero-hardware alternative), no network required.
Enforced by merge-blocking tests, not just promised:
- No audio, ever. Raw frames are processed in memory and discarded — audio bytes are never written to disk and never transmitted anywhere. Only derived levels and event metadata are persisted (this is the central design gate and has a merge-blocking test).
- Local-only. No cloud, no telemetry; the only optional output channel is the local, emit-only automation socket described below.
- Honest reports. Every report states its methodology and limitations — uncalibrated dBFS is relative, not absolute SPL unless calibrated, and the device cannot prove a sound's source. Data is presented to inform, never fabricated or cherry-picked to manufacture a case.
- The caveats travel with every export. The "what this can and cannot prove" cover
block leads every artifact either implementation produces — HTML report, quiet-hours
report, the local status page, and both CSVs (as a
#preamble, so the data rows still parse) — and anything reporting a quiet-hours count also says a count is a measurement and not a determination, either as that sentence or as the cover bullet that says the same. The strings live inspec/report/cover.jsonand are replayed against Python and the browser edition. The gate discovers export paths from source by what they build — a whole HTML document, or a CSV — not by what they are called, so a new one cannot ship without them. It used to discover them by name, and the status page slipped through that way and shipped uncovered; the behavioural half exists because the name-matching half missed a real artifact for as long as that artifact existed.
Agent-facing build instructions live in CLAUDE.md.
make dev # create .venv and install (dev extras)
make verify # lint, type, coverage, security, a11y, PWA tests, i18n gate
make report # render report.html from a demo session (no hardware)
# Live capture on a Pi/laptop (optional audio dependency):
uv sync --locked --group dev --extra live
.venv/bin/olive-tune --config config.sample.json # live meter; suggests a threshold
.venv/bin/olive-calibrate --config config.sample.json --reference-db 70 # store SPL offset
.venv/bin/olive-monitor --config config.sample.json # logs events; Ctrl-C to stop
.venv/bin/olive-report --config config.sample.json --out report.html --csv events.csvThe core (level math, detector, store, report) has zero runtime dependencies and runs
on any Python 3.9+ with no installs; only live microphone capture needs the live extra.
Calibration is a single source of truth. Events are stored as raw dBFS and the calibration offset is applied at report time from an append-only history owned by
olive-calibrate. Thecalibration_offset/calibration_notefields inconfig.sample.jsonare bootstrap-only (deprecated for steady-state use): they seed a database that has never been calibrated and are ignored onceolive-calibratehas run. The monitor never writes calibration, soolive-calibratefollowed byolive-monitorwith a default config no longer reverts the device to uncalibrated.threshold_dbfsis defined against the same raw stored scale, so recalibrating never changes detection sensitivity. Render-time calibration is applied identically to the HTML report and to the--csv/--violations-csv/--violations-htmlexports; each CSV row records the offset included in its levels (calibration_offset_db) and whether that offset was in force when the row was measured or back-applied from a calibration taken later (calibration_basis); the report states how many readings the first calibration postdates.
| Command | What it does |
|---|---|
olive-monitor |
Run the monitor: capture → level → detect → SQLite. Creates a capture session (lineage), writes a heartbeat file, reconnects on device failure, prunes per retention_days. |
olive-tune |
Show the live level so you can pick a threshold by ear; prints a suggestion. |
olive-calibrate |
Measure mean level against a reference SPL reading and append a calibration offset (with optional --reference-instrument provenance). This is the only writer of calibration; it is an append-only history applied at report time, so recalibrating never rewrites earlier events. |
olive-report |
Render the accessible HTML report (distributions + day×hour calendar heatmap + quiet-hours summary). Optional --csv event export, --violations-csv / --violations-html for an honest quiet-hours report suitable for a neighbor/landlord/HOA submission, and --pdf / --violations-pdf for a tagged PDF/A-3a of either (needs the pdf extra; see Standards Conformance). |
Every quiet-hours export states how much of the window it observed. Monitored vs wall-clock hours appear in the Summary block above the counts, each recorded monitoring gap is listed with its length, and every event row carries a
monitoredflag. Hours the device was not listening are reported as not monitored, not quiet — a count is only readable against the time it was counted over. That includes hours with no monitor running at all: those leave no gap row behind (writing one takes a running monitor), so coverage is measured against the capture-session ledger, and any stretch between one run ending and the next beginning is listed by date and subtracted. The figure is still an upper bound: an interruption inside a run that the monitor never got to record cannot appear in it, and the export says so. Where coverage cannot be determined from the record at all — including a log with no capture sessions, which cannot show off-air time in either direction — the export says that instead of leaving it out.
When health_path is configured, the monitor writes a static status.html next to
the heartbeat file on every check-in. You can instead enable only the page by setting
status_path explicitly. No server or network is involved. Open it straight from disk
(double-click, or open status.html) for an at-a-glance
ops view: heartbeat freshness (with a stale-heartbeat warning if the monitor has gone
quiet), the most recent level, frame coverage, recorded monitoring gaps, and a recent
summary (event count, minutes with events, busiest hour, quiet-hours totals). The page
is atomically rewritten, so you never catch it half-written, and it auto-refreshes every
60s if left open in a browser. It inherits the report's accessibility (keyboard-complete,
scoped table headers, reduced-motion) and the same no-audio guarantee, and — because it
prints quiet-hours counts and is a page you can hand to someone — the same cover block
and no-verdict line as every other artifact.
For home-automation confounder context — e.g. correlating a doorbell, robot vacuum, or
smart speaker with a logged spike — the monitor can emit its heartbeat and each event to a
local AF_UNIX datagram socket.
It is off by default, one-way, and emit-only: nothing is ever read back and no
network socket is opened, so the no-egress guarantee is unchanged (there is a merge-blocking
test that permits socket only in monitor/ipc.py, and only for AF_UNIX). Enable it with
--ipc-socket /run/olive/ipc.sock (or "ipc_socket" in the JSON config; "" = disabled).
Sending is nonblocking and best-effort: if the listener is missing, stalled, or unable to
accept a datagram, that update is dropped instead of delaying sound capture.
A Home Assistant listener (same host) can pick up the JSON datagrams via a shell/command_line
sensor that reads the socket, e.g. with socat:
# configuration.yaml — reads one JSON line per datagram from the local socket.
command_line:
- sensor:
name: Olive Bark Event
command: "socat -u UNIX-RECV:/run/olive/ipc.sock,fork - "
value_template: "{{ value_json.peak_level | default('idle') }}"
json_attributes:
- type
- start
- duration
- peak_level
- session_idPayloads are {"type": "event", "session_id", "start", "duration", "peak_level"} per event
and the heartbeat health dict on each beat. Levels and metadata only — never audio.
-
Raspberry Pi service:
scripts/setup-pi.shinstalls PortAudio + a venv and thedeploy/olive-monitor.servicesystemd unit (auto-restart, network-isolated, sandboxed). -
Browser PWA (zero hardware):
pwa/— Web Audio levels, IndexedDB events, same no-audio guarantee, works offline. Seepwa/README.md. -
Container:
Dockerfilebuilds the report/analysis side for reproducible CI. -
Definition of done: the monitor runs unattended, logs noise events (levels + timestamps, zero audio) to local SQLite, and produces an honest, accessible report with charts and a stated methodology — all applicable
/STANDARDSgates green (see Standards Conformance below) and the no-audio test passing. Full checklist:DEFINITION_OF_DONE.md.
Tier C — OTel tracing out-of-scope (no network surface). Opt-in --log-format json
ships (monitor/log.py, --log-format json or "log_format": "json" in the config):
every operator line is emitted as one JSON object per line for a log shipper, using only
the standard library. text stays the default and is byte-for-byte the previous output.
See GAP-OBS-1, addressed 2026-07-14.
Alongside it: a heartbeat JSON file (monitor/service.py) with no secret/PII fields by
design.
Inherits /STANDARDS (this table is the individual declaration DOC-11
requires; a bare "inherits" statement with no table is the exact silent-omission defect
the standard forbids — a prior version of this README made that mistake). Applies — open gap recorded in GAP-NN rows resolve to a real, dated, append-only entry in
docs/GAP-LEDGER.md (a GitHub issue was the original plan, but
this repo's tooling correctly refuses unsolicited issue creation as an external
write-effect, so gaps live here instead — see that file's header for why).
| Standard | State |
|---|---|
| Quality & Metrics | Applies — open gap recorded in GAP-QM-1 (DORA ledger; release-gate checklist exists in DEFINITION_OF_DONE.md but has never been run, since no release has happened) |
| Code Quality | Applies — open gap recorded in GAP-CQ-1 (Python-floor divergence recorded in ADR-0002; pre-commit enforcement, hatchling, and src/ layout still open) |
| Security & Supply-Chain | Applies — hardened posture (ASVS L2); open gap recorded in GAP-SEC-1 |
| CI/CD | Applies — open gap recorded in GAP-CICD-1 (the live protect-main ruleset matches the committed .github/rulesets/main.json since the 2026-08-21 reconciliation — PR required, strict checks, required_signatures deliberately dropped with a reasoned note, verified by make ruleset-check exit 0. On 2026-08-26 five of the eleven required checks were removed: test-matrix (macos-latest, 3.9–3.13) were satisfied by an echo on an ubuntu runner and could not fail. Six required checks remain and all six do real work; the nightly macOS sweep and the live ruleset are now themselves checked inside verify. On 2026-08-27 this row and .github/rulesets/main.json stopped claiming no bypass actors: the repository-admin role can bypass every rule, always, and could for as long as that claim stood. The bypass is deliberate and stays; the file was amended to it. CI's --scope public run cannot read that field at all, so make ruleset-check with a maintainer token is the only thing that checks it; that check holds the live ruleset and the committed file against the owner's bypass independently rather than comparing them, so a lockout is a finding whichever side it happens on. Still open: zizmor + CodeQL-actions) |
| Release & Versioning | Applies — release-producing deployed app; open gap recorded in GAP-REL-1 (tag-triggered release.yml now exists, REL-14 — no tag cut yet, and PyPI/GHCR/cosign are still open; CITATION.cff intentionally carries no date-released until a tag exists) |
| Accessibility | Applies — open gap recorded in GAP-A11Y-1 (pwa/index.html is scanned by axe on every push and PR since 2026-07-11; still open: no Lighthouse, walkthrough stale since 8a9f1eb, no ACR/VPAT, no NVDA or iOS VoiceOver pass) and GAP-A11Y-2 (the optional tagged PDF/A-3a export's structure is tested; its PDF/UA/"fully accessible" conformance is not verified — no human AT walkthrough has been done) |
| Observability | Applies — Tier C: OTel out-of-scope (no network surface); opt-in --log-format json shipped 2026-07-14 (GAP-OBS-1: Addressed) |
| Internationalization | N/A — single-user tool, operator-only English output (docs/I18N.md) |
| AI Evaluation | N/A — no model/prompt/retrieval surface; nothing in this codebase calls an LLM SDK |
| Documentation | Applies — open gap recorded in GAP-DOC-1 (/STANDARDS vendoring blocked on a portfolio-level tag prerequisite; ADR migration in progress) |
| Performance | Applies — the HTML report and the PWA are shipped human-facing surfaces, so this is in scope rather than exempt; no performance budget and no Lighthouse performance run are committed, recorded here as an open gap |
| Incident Response | Applies — SECURITY.md is the private reporting channel and carries the acknowledgment and fix SLA; no severity-label convention and no postmortem template are committed, recorded here as an open gap |
| Data Governance | Applies — docs/audits/data-card.md records the inventory, lineage, retention, and schema versioning; what is written is timestamps and levels only, with no audio persisted, held by the merge-blocking tests/test_no_audio.py |
| AI Development Measurement | Applies — no measurement artifact is committed in this repository, recorded here as an open gap |
| Responsible-Tech Framework | Applies — this repo's strongest standard: no-audio, no-egress, and honest-report-content are merge-blocking tests (tests/test_no_audio.py, tests/test_no_egress.py, tests/test_report_content.py); full treatment in docs/RESPONSIBLE-TECH-AUDITS.md; open gap recorded in GAP-RTF-1 (per-section sign-off dates) |
Last full audit: 2026-07-05 (audit-2026-07-05/olive-bark-logger-AUDIT.md,
33/138 controls PASS before that day's remediation pass; this table reflects the
post-remediation state and will drift from a fresh audit run — treat the audit file as
the point-in-time evidence trail, this table as the current claim).
This is independent, unpaid work. If it has been useful to you, you can