A living piece of art that quietly reflects the birds visiting our garden.
This is not a dashboard, a smart display, or an IoT gadget. It is a framed editorial wildlife print that hangs beside the kitchen window and slowly evolves through the day as birds are heard in the garden. It never animates, never flashes, never notifies. It simply observes.
A small pipeline of independent components, each replaceable without touching the others:
microphone → BirdNET → SQLite → composer → renderer → e-ink display
- Listen — BirdNET performs local inference on the Raspberry Pi. No audio ever leaves the device; only confirmed detections (species, confidence, timestamp) are stored.
- Accumulate — detections gather in SQLite through the day.
- Compose — once an hour, the previous 24 hours are aggregated by species and turned into a balanced, organic poster layout. The most-heard bird is largest; rarer visitors are smaller.
- Render — the layout is drawn as an editorial print (HTML/CSS → headless Chromium) from a curated library of transparent bird illustrations. No AI images are generated at runtime — the art is deterministic, consistent, offline, and free to produce.
- Display — the poster refreshes on a large colour e-ink panel, once per hour. That slow cadence is the point.
The only things ever shown are the artwork, the title (Heard / Today), and the species names. No numbers, no graphs, no statistics. Abundance is communicated through composition.
A poster produced by the current pipeline (with placeholder illustrations):
- Calm technology — no interaction; changes are discovered, not pushed.
- Slow computing — hourly refresh; nothing is instant.
- Art first — it should be worth hanging even if it weren't smart.
- Minimal data — names and artwork only.
- Privacy by structure — recognition is local; no raw audio is retained.
Whenever an implementation choice arises, we prefer whichever option best preserves the illusion that this is simply a beautiful framed print quietly observing the garden.
src/heard_today/
domain.py Immutable, typed core values (Detection, Composition, …)
config.py One injected Settings object; the only reader of env vars
interfaces.py Protocols for every component boundary (the seams)
assets.py The illustration library (manifest-driven)
audio/ Detectors: BirdNET (Pi) and a replay fake (Mac dev)
database/ SQLite detection store + 24h aggregation
composer/ Deterministic organic layout engine (pure)
renderer/ HTML/CSS template → headless Chromium PNG
display/ E-ink driver (Pi) and preview display (Mac dev)
pipeline.py The hourly refresh orchestrator
scheduler.py Slow, on-the-hour scheduling
listener.py The continuous detection loop
factory.py Composition root — the one place wiring happens
cli.py `heard` command-line entry points
api/ Reserved for future read-only extensions
assets/birds/ Transparent bird illustrations + manifest.json
fonts/ Title & caption typefaces (optional; system fallback)
docs/ Architecture and decision records
scripts/dev/ Development helpers (placeholder asset generation)
tests/ Unit tests
Requires uv.
# Install with dev + rendering extras (no BirdNET / e-ink needed on a Mac)
uv pip install -e ".[dev,render]"
# Install the headless browser used for rendering
uv run playwright install chromium
# Generate placeholder illustrations (until the real library exists)
uv run python scripts/dev/generate_placeholder_assets.py
# Seed the database with a plausible stream of garden birds
uv run heard seed
# Compose, render, and open a poster
uv run heard previewTo watch it behave like the real thing, run the listener and the hourly refresh in two terminals:
uv run heard listen --replay # writes fake detections continuously
uv run heard run # refreshes the preview poster every hourInstall the hardware extras and use the real components:
uv pip install -e ".[render,audio,eink]"
uv run heard listen # real BirdNET inference
uv run heard run --eink # drive the e-ink panel hourly(The BirdNET capture loop and e-ink output are documented stubs until the
hardware is in hand — see src/heard_today/audio/birdnet.py and
src/heard_today/display/eink.py.)
uv run ruff check .
uv run mypy
uv run pytestStrong typing throughout, clear separation of concerns, dependency injection at
the seams, and every component replaceable in isolation. See docs/ for the
reasoning behind the major decisions.
MIT — see LICENSE.
