This is my submission for the Touch Grass challenge.
The Problem With Bird Apps
Most bird identification apps do the job and ruin the walk. You hear something in the trees, pull out your phone, open an app, hold it up, and stare at a spectrogram while the bird flies off.
I wanted the opposite: a tool that makes you look up.
What I Built
Trailbird is a bird call identifier that runs entirely on a laptop, with no internet and almost no screen time.
You start it, put the laptop in your backpack, and walk. Every few seconds it listens. When it recognizes a bird, it says the name in your earbud, in one calm sentence, something like "Northern cardinal, probably in the shrubs to your left." New species get flagged for your life list. The screen is only for the start and the end of the walk.
Why Open Matters Here
This project only works because every piece is open and local.
- No signal needed. The best birding spots, like ridgelines, wetlands and forest edges, tend to have the worst reception. A cloud API would fail exactly where I need it.
- My location history stays mine. A log of when and where I was in the woods is personal. Trailbird writes to a local SQLite file and nothing else. No account, no upload.
- Every part is swappable. The detector, the language model and the voice are separate pieces. Changing the model is a one-line edit, which is much harder with a closed API.
- It costs nothing to run. No API keys, no per-call fees, no subscription for a hobby.
How It Works
mic -> BirdNET (species detection) -> Gemma via Ollama (field note) -> Piper TTS (earbud) -> SQLite life list
| Piece | Role | Open? |
|---|---|---|
| BirdNET-Analyzer | Identifies species from audio | Open source (the model weights are CC BY-NC-SA, so non-commercial use only) |
| Gemma, served by Ollama | Turns raw detections into one short spoken sentence | Open weights |
| Piper | Local text-to-speech for the earbud | Open source |
| SQLite | Personal life list | Open source |
BirdNET does the hard acoustic work. Gemma's job is only the last step: turning "Cardinalis cardinalis, 0.82" into something a person wants to hear while walking.
The core loop
import sounddevice as sd, sqlite3, subprocess, time
from birdnet_analyzer import analyze # adjust to your installed BirdNET version
import ollama
SAMPLE_RATE = 48000
CLIP_SECONDS = 3
MIN_CONFIDENCE = 0.6
COOLDOWN_SECONDS = 120
db = sqlite3.connect("lifelist.db")
db.execute("CREATE TABLE IF NOT EXISTS sightings (ts REAL, species TEXT, conf REAL)")
def listen():
audio = sd.rec(int(CLIP_SECONDS * SAMPLE_RATE),
samplerate=SAMPLE_RATE, channels=1)
sd.wait()
return audio.flatten()
def field_note(species, conf, is_new):
prompt = (
f"You are a calm birding guide. In ONE short sentence, tell the "
f"walker I just heard a {species} (confidence {conf:.0%}). "
f"{'This is a NEW species for their life list!' if is_new else ''} "
f"Suggest where to look. No emoji."
)
r = ollama.chat(model="gemma3:4b",
messages=[{"role": "user", "content": prompt}])
return r["message"]["content"].strip()
def speak(text):
subprocess.run(["piper", "--model", "en_US-lessac-medium", "--output-raw"],
input=text.encode(), check=False)
seen = {row[0] for row in db.execute("SELECT DISTINCT species FROM sightings")}
last_spoken = {}
while True:
clip = listen()
for species, conf in analyze(clip, SAMPLE_RATE):
if conf < MIN_CONFIDENCE:
continue
if time.time() - last_spoken.get(species, 0) < COOLDOWN_SECONDS:
continue
is_new = species not in seen
speak(field_note(species, conf, is_new))
db.execute("INSERT INTO sightings VALUES (?,?,?)",
(time.time(), species, conf))
db.commit()
seen.add(species)
last_spoken[species] = time.time()
Design decisions that mattered
- A confidence threshold plus a cooldown. Without them, one persistent bird would announce itself over and over. A 120-second cooldown per species keeps the earbud quiet.
- One sentence only. A language model's default is a paragraph, which is the last thing you want in your ear on a trail. Forcing a single sentence turns the tool from a lecturer into a quiet companion.
- A small model on purpose. A 4B Gemma runs on a laptop without a GPU. Bigger would be smarter, but "works in a backpack with no internet" is the goal.
- Speech over screen. If the tool needs you to look at it, it has failed the theme. Audio out is the whole interface.
Taking It Outside
I took Trailbird to for about on a , with the laptop in my backpack and one earbud in.
What I noticed most was that after the first few minutes I stopped thinking about the tool and just listened. The screen stayed shut for the entire walk, which was the point.
Limitations I expect to hit, and that anyone trying this should know about:
- Wind and traffic noise cause false positives, so the confidence threshold matters.
- Laptop battery drains faster with a mic and a model running continuously.
- BirdNET is strongest on common species and weaker on rare or overlapping calls, so treat results as "probably", not gospel.
What I'd Do Next
- Run it on a Raspberry Pi or similar board so no laptop is needed
- Add a wind filter to cut false positives
- Load a regional species list so it prioritizes what is plausible for the season and place
- Export the life list to eBird when I'm back online, as an opt-in step
Closing Thought
A good outdoor tool should make itself forgettable. Open, local models are what make that possible here: no signal, no account, no server holding my walks. Just me, an earbud, and the birds.
*Built with open-source and open-weight tools: BirdNET, Gemma (via Ollama), and Piper.
Top comments (0)