Back to portfolio
ProjectLive

The Aviary

Always-on acoustic bird identification at the edge: three family monitoring stations, one riding security-camera audio and two on budget lavalier mics, each feeding a living life list with AI-written field notes.

Runs privately on the family's own network; no public demo

ToddTech LLC · System architecture
The Aviary
Always-on acoustic bird ID at the edge — three yards, one record, nothing in the listening path leaves the house.
01Field stationsSame codebase, one env file of local facts per yard — a new microphone is a registry entry, not a code change
HomeLive · hub
Two ears: front and rear security-camera audio over RTSP — zero new hardware, and the two yards hear different birds. Into a Lenovo M920x bought cheap on eBay, minimal Ubuntu.
Full TensorFlowEcowitt on siteOllama host
BirdhouseLive · since 8/6
One ear: a clip-on lavalier mic in a window. Into a Dell Wyse 5070 thin client on minimal Ubuntu — no AVX, so LiteRT at 27× real time.
Mom's stationLiteRTOpenWeatherMap
KestrelLive · since 8/29
One ear so far: a USB-soundcard lavalier under the front eave, into a Dell Wyse 5070 thin client on minimal Ubuntu. A warbler32 Wi-Fi listener for the rear of the yard is next.
Aunt's stationLiteRTwarbler32 next
02earl — the listener serviceRuns on every station. All inference on the box, none in a cloud.
Capture
3-second windows
48 kHz, per mic, around the clock
Screen
YAMNet
Speech → dropped. Dog, siren, thunder → ticker.
Identify
BirdNET
Geo + week mask; wind raises the threshold
Collapse
Visit debounce
One singing cardinal is one visit, clip upgrades in place
Record
SQLite + clips
Append-only, published on MQTT
Speech is never recorded.
A model enforces it, not a policy: a window whose top label is human speech produces no event and no clip — in code, under test, never a setting.
03   Per station, self-contained
Broker, database, dashboard
Each yard runs its own Mosquitto, SQLite and masthead. The web tier never writes the record.
04   The Aviary app
Next.js over SQLite
Read-only routes hydrate, MQTT merges live: life list, ticker, species profiles, field journal. Each species page leads with that bird's finest recording to date.
05   Enrichment, worklist-driven
Best clip, then field notes

Curation keeps one clip per species, re-ranked on vocal richness rather than confidence alone, after a NumPy DSP pass makes faint birds audible.

Ollama, self-hosted, writes two notes per bird — daily rhythm, and how weather moves its odds — narrating exposure-normalized figures Python already computed. It never sees a raw row, so it cannot invent a statistic.

06   Operations
The fleet deploys itself. A systemd watcher on every box fast-forward pulls and restarts: home boxes track main, and the family stations ride a stable release branch promoted nightly by a GitHub Action, at most a day of changes at a time. Tailscale ties the three yards into one private mesh — how a station in someone else's house gets maintained, with no ports opened and no visit.
Roadmap
The community layer. All three stations are reporting, so the fleet becomes a friendly competition — widest roster, best eagle recording, first hummingbird of spring.
toddtech.llc/portfolio/aviaryKalamazoo, MI

Tech Stack

PythonBirdNET (Cornell Lab)YAMNetLiteRTNumPy DSPFFmpegMQTT (Mosquitto)SQLiteEcowitt weather stationOpenWeatherMap APINext.js (App Router)TypeScriptTailwind CSSOllama (local LLM)systemd / UbuntuTailscalepytest / Vitest

Overview

The Aviary is the public face of a wildlife-monitoring platform that listens to the yard around the clock. An always-on listener service pulls live audio from outdoor microphones, runs Cornell's BirdNET classifier on every 3-second window, and files each accepted detection into an append-only bird record: a life list of every species ever heard (its first-heard moment and first recording preserved forever), one curated audio clip per visit, and the visit history behind them. The Aviary web app presents that record as something a family actually wants to open: a life-list grid of species tiles, a live ticker of arrivals with inline clip playback, and per-species profile pages with portraits, visit-rhythm charts, and AI-written field notes.

The field notes are the feature I'm proudest of. Each species gets two short essays, one on its daily rhythm and one on how weather moves its odds, built by joining every detection against time of day and the yard's own weather record. At home that record comes from an Ecowitt weather station on the property, logging wind, temperature, and rainfall every five minutes; stations without weather hardware read OpenWeatherMap instead. Hyperlocal truth where it exists, a clean fallback where it doesn't. The statistics keep themselves honest: every claim is an exposure-normalized rate ('shows up more in rain' only counts against how many rainy hours there actually were), computed inside each species' own active hours so a dawn effect can't masquerade as a weather effect, and the on-site rain sensor outranks the weather API when the two disagree about whether it's raining.

Inside the app

Screenshot gallery

The life list

Every species the yard has ever announced, one tile each, with portraits, visit counts, search, and sort. Counts tick live as detections arrive over MQTT, and new lifers append without reshuffling the grid, because the UI's standing rule is that nothing on the page ever moves under the reader's eyes.

Latest events

The live arrivals ticker: each row is a visit opening, with species, confidence, source microphone, and an inline player for the ~9-second clip. Playback routes through a WebAudio gain-and-limiter graph because real yard birds are often faint, and the clip server speaks RFC 7233 byte ranges because without them iOS Safari refuses to play audio at all, a lesson learned the hard way on a real phone.

A species profile

Each species gets a magazine-style page: Wikipedia portrait and description, standings against the rest of the yard, and a visits chart that pans back through the whole record, with daily bars for how often and an hourly curve for when in the day. The curve is a monotone (Fritsch–Carlson) spline so sparse counts can never draw an impossible −2 visits at 3 a.m.

The field journal

Two pages of AI-written field notes per species, one on the bird's daily rhythm and one on how weather moves its odds, rendered as a journal spread with margin figures drawn from the stored statistics: a 24-hour activity strip and weather-effect pills. The numbers come from joining detections against the yard's own weather record (an on-site Ecowitt station at home, OpenWeatherMap at stations without one), and the locally hosted LLM only narrates the precomputed figures, so the figures and the prose can never disagree.

The full record

The browsable archive: infinite scroll under sticky day headers, combinable species filter pills, jump-to-date, and shareable filtered URLs. All of it reads from the same append-only SQLite store the listener writes; the web tier never writes the bird record.

A family station

The same codebase in its station role at my mom's house: one env file of local facts, its own broker, database, and dashboard, and a masthead that says whose yard it is. It updates itself from a stable release branch promoted nightly (a systemd watcher pulls and restarts), and it has run unattended on a fanless mini PC and a clip-on mic since early August.

How it was built

Selected Design Decisions

The statistics are computed in Python; the LLM only narrates them. Hand any model raw event rows and ask for the pattern, and it invents percentages, invisibly. So every figure the field notes may speak is computed first (exposure-normalized visit rates against the species' own baseline, with confound controls for dawn hours and wind), stored as JSON beside the generated prose, and the model's only job is turning audited facts into two charming paragraphs. Auditing output against the stored stats caught the model inverting a direction and hedging from boilerplate; both were fixed in the prompt rather than by reaching for a bigger model.

The best sensor is the one already installed. The home station pulls the audio track alone off the property's security cameras over RTSP; the cameras were already aimed at the yard, already powered, already networked. The station at my mom's proves the opposite end: a budget lavalier mic and the smallest PC that works. Audio sources live in a config registry keyed by kind, not by name, so adding a camera or retiring a mic is a data edit rather than a code change.

Want to talk about this work?

I'm available for contract and full-time roles, and happy to walk through any of these systems in detail.

Get in touch