DEV Community

James Ngandu
James Ngandu

Posted on

Touch Grass Birder: a bird call identifier that works with no signal

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission 🌿

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass.

What I Built

Touch Grass Birder identifies bird calls, and it does the whole thing on your own machine.

You tap Listen, hold your phone up while a bird sings for about six seconds, and it tells you what you are hearing. Then you put the phone away and go look for the bird. That is the point: the screen is only in the way for a moment.

I built it for the places where phone apps tend to fall over. A trailhead with one bar. A tent before sunrise. A park where the warblers are moving through and you have no idea what you are listening to.

What it does:

  • Records six seconds of audio in the browser and encodes it to WAV on the device.
  • Identifies the species locally with BirdNET, the open acoustic classifier from the Cornell Lab of Ornithology.
  • Writes a short field note about the bird with Gemma 3 1B, an open-weight model running through llama.cpp.
  • Keeps a field log of everything you have heard, in a plain JSON Lines file you own.
  • Installs as a PWA and keeps working with the network turned off.

There is no API key, no audio upload, and no network round trip.

Demo

Live demo: https://touch-grass-birder.onrender.com

The hosted demo runs BirdNET, so you can point it at a recording right now. Gemma field notes only work when you run it yourself, because a free Render instance is too small for the 0.8 GB model. Run make models && make run for those.

Code

https://github.com/mukuvi/touch-grass-birder (MIT licensed).

How I Built It

Two open models run on the device, wired together behind a small FastAPI server.

flowchart LR
  A[Phone browser<br/>records 6s, encodes WAV] --> B[FastAPI]
  B --> C[ffmpeg<br/>decode to 16-bit mono WAV]
  C --> D[BirdNET acoustic model<br/>open, local inference]
  D --> E[species + confidence]
  E --> F[Gemma 3 1B<br/>open-weight via llama.cpp]
  F --> G[plain-language field note]
  E --> H[(data/sightings.jsonl<br/>your field log)]
  G --> A
  H --> A

The pieces:

  • Browser side: the MediaRecorder API captures six seconds, an AudioContext decodes it, and a small function in app.js writes a real 16-bit mono WAV Blob before it is sent. The server never has to touch the audio to get a usable clip.
  • BirdNET does the acoustic work. It is trained on thousands of species for passive acoustic monitoring, and it is the reason this works offline at all. It loads through the LiteRT runtime and stays warm in memory after the first call.
  • Gemma 3 1B (Q4_K_M, about 0.8 GB) runs through the llama.cpp CLI to write a two or three sentence field note: what the bird sounds like, where and when you would hear it, and one field mark to look for. If the model is not on disk, the app skips the note and still identifies the bird.
  • FastAPI ties it together with four endpoints (/api/identify, /api/sightings, /api/health, and the static app). The field log is data/sightings.jsonl.

Everything is overridable with environment variables (LLAMA_BIN, GEMMA_MODEL, MIN_CONF, LLAMA_THREADS, BIRDER_DATA_DIR), so the same code runs on a laptop, a Raspberry Pi, or something sitting next to a phone.

Why Does Open Innovation Matter?

The short version: this project only exists because the models are open.

The best places to hear birds are the places with no signal, and a cloud API cannot run there at all. Every closed identifier needs a round trip to a server. Running BirdNET on the device means the app is not a worse version of a cloud tool in the backcountry. It is the only version that works there. The recording is six seconds because nothing has to be uploaded and there is no spinner to wait on. The answer is already on the phone.

A rare sighting is sensitive. A closed app can learn where you were and when, which matters if you are near a nesting site or reporting a scarce species. Here the only record is a line in a file you own, on hardware you control. There is no server for it to go to.

Open weights also mean I can change the model. Gemma runs from a local GGUF file, so moving to a larger Gemma, a different quantization, or a different model entirely is a path and an environment variable, not a conversation with a vendor. I can fine-tune on a regional species list later. The classifier itself, BirdNET, is open and inspectable, which feels important when the output is a claim about a living thing.

And it costs nothing to run. No per-identification fee, no rate limit, no key that expires. A birder on a budget, a school group, or a citizen science project can run it as long as they want.

The open parts are the point. For this app the offline, private, zero-cost version is simply the better one.

My Agent Session

I built this with an agent. The session is saved, so you can see how it went:

Building Touch Grass Birder for the HF26 Week 1 challenge
You

Pasted the Hacktoberfest Open-Source AI Challenge Week 1 (Touch Grass) announcement: build something with open-source AI at its core that gets people outdoors, due Oct 11. Can you do this?

Agent

I checked the workspace. There is already a project: Touch Grass Birder, a bird-call identifier (BirdNET + Gemma 3 1B) that runs fully locally. Tests pass. I loaded the challenge skill and confirmed Week 1 is active (id 79, due Oct 12 06:59 UTC).

You

Scope: Draft the DEV post, repo polish + commit, deploy to Render, save + embed the agent session, do it all end-to-end.

Agent

Audited the code (identify.py via BirdNET/LiteRT, fieldnotes.py via llama.cpp Gemma 3 1B, FastAPI main.py, PWA static app, 3 passing tests). Drafted the full submission post to docs/dev-submission-draft.md and added an MIT LICENSE.

You

Decisions: rename repo to touch-grass-birder, use MIT license, deploy a BirdNET-only demo on Render free tier (Gemma notes run locally).

Agent

Reconciled the local repo onto the real remote, replaced Apache-2.0 LICENSE with MIT (matching README/pyproject), committed the post draft + license, and pushed to GitHub.

You

proceed bro, you got all the keys

Agent

Verified the push landed, pointed local origin at the real repo, and set out the remaining steps: rename on GitHub (needs a one-click settings change from the user - SSH cannot rename repos), deploy the Render blueprint, embed this saved agent session in the post, then publish the draft.

Prize Categories

  • Best Use of Gemma: Gemma 3 1B runs locally through llama.cpp and writes the field notes.
  • Best Use of Render: the live demo runs on Render with the included render.yaml.

Built for Hacktoberfest 2026. Files, models, and data stay on the machine, with no key, no upload, and no network.

Top comments (0)