{"slug": "trilha-identifying-birds-by-ear-on-the-trail-with-no-signal", "title": "trilha: identifying birds by ear on the trail, with no signal", "summary": "A developer built trilha, an offline command-line tool that identifies birds from audio recordings by combining BirdNET 3.0 (11,560 species) with BirdNET's geographic range model, which narrows candidates to 596 species at a given latitude, longitude and week. The tool runs entirely on CPU in 5.2 seconds with no account, API key or upload, and uses Gemma 3 via Ollama only to write field-journal prose, never to touch identification. The developer found that the language model, given bare coordinates, invented a nonexistent place name (\"Reserva do Pampa\") for São Paulo, illustrating the need for a hard architectural boundary between detection and generation.", "body_md": "*This is my submission for the [Hacktoberfest Open-Source AI Challenge: Week 1](https://dev.to/challenges/hf26) — theme: Touch Grass.*\n\nYou're on a trail. Something calls from the brush, twice, and then stops. You don't know what it was.\n\nThe normal move is to hold your phone at the canopy and wait for a bar of signal. The places worth birding are exactly the places that don't have one. So you get home six hours later with a recording you can no longer place, and it sits in your phone forever.\n\n**trilha** is a command-line tool that does the whole thing on your laptop, offline. It listens to a recording, names what it heard, and writes the outing into a field journal in prose.\n\n``` bash\n$ trilha listen manha.wav --lat -23.55 --lon -46.63 --place \"Parque do Carmo\"\n\n  manha.wav\n  596 species in range · 5.2s on CPU · week 40\n\n   0.97 ###################  Rufous-bellied Thrush\n        Turdus rufiventris · first at 30s · 22 window(s)\n   0.94 ###################  Rufous-collared Sparrow\n        Zonotrichia capensis · first at 42s · 26 window(s)\n   0.92 ##################   Southern Lapwing\n        Vanellus chilensis · first at 63s · 4 window(s)\n   0.59 ############         Southern Yellowthroat\n        Geothlypis velata · first at 78s · 2 window(s)\n\n  Field note\n  The presence of the Rufous-bellied Thrush, Rufous-collared Sparrow, and\n  Southern Lapwing strongly suggests a mixed habitat of open woodlands and\n  grasslands within Parque do Carmo. Next, while on this trail, pay particular\n  attention to the calls of the Southern Yellowthroat — they often favour dense\n  undergrowth and shrubbery, and their distinctive song is relatively\n  high-pitched.\n```\n\nNo account, no API key, no upload. 5.2 seconds on CPU.\n\nThree open pieces, each doing the one thing it's good at.\n\n**The ear — BirdNET 3.0, through ONNX on CPU.** It knows 11,560 species. It slices the audio into 3-second windows and scores each window independently, which is why the output can tell you a bird was *first heard at 78s across 2 windows* rather than just handing you a label.\n\n**The filter — BirdNET's range model, 14,082 species.** This is the part I find most interesting and it's the part nobody demos. Give it latitude, longitude and the week of the year, and it narrows the candidates to what could plausibly be there right now. At `-23.55, -46.63` in week 40 that's **596 species instead of 11,560**.\n\nThat reduction is the difference between a useful answer and a list of everything that has ever had feathers. A classifier asked to choose from 11,560 classes will cheerfully hand you a Himalayan bird in São Paulo. Asked to choose from the 596 that are actually in range in October, it won't.\n\n**The voice — Gemma 3, through Ollama, on my machine.** It turns the detection list into a few sentences for the journal. It never touches the identification.\n\nThat last point is a hard architectural boundary, not a disclaimer: if Ollama isn't running, the note is skipped and the detections still print. The language model is the part with taste in it, so it's the part that's allowed to be absent.\n\nI had the thing working and almost wrote it up at that point. Then I ran it once more, properly, and read the output instead of just checking that it ran.\n\nThe field note came back like this:\n\n```\n  Field note\n  Aqui está a nota de campo em português:\n\n  **Nota de Campo – Reserva do Pampa**\n\n  A combinação de corruíras, sabiás-laranjeira e sabiá-poca sugere um habitat\n  de matagal aberto...\n```\n\nTwo separate problems in one paragraph.\n\n**The small one:** the model announced what it was about to do, then added a Markdown heading. In a terminal pretending to be a paper journal, `**Nota de Campo –**` is noise.\n\n**The one that actually mattered:** *Reserva do Pampa*. There is no such place in this story. I passed no `--place`, so the prompt sent bare coordinates — `-23.55, -46.63`, which is São Paulo. The model read the numbers, decided it knew where that was, invented a reserve name, and put the wrong biome on it. The Pampa is 1,000 km south.\n\nThis is the failure mode that would have killed the project. The whole premise of a field journal is that it's a record of where you actually were. One invented place name and the entire artifact becomes untrustworthy — not just that line, all of it, retroactively. A tool that is right about the birds and wrong about the ground is worse than no tool, because you'd believe it.\n\nThe root cause was in the prompt contract, not the code path. My original `SYSTEM` already said *\"never invent a species that is not listed, and never invent a count\"* — I had thought about hallucination, but only about the taxonomy. I never constrained the geography. So I added the rule I'd been missing:\n\n```\nOnly ever refer to the location exactly as it is given to you. When you are given\ncoordinates and no place name, leave the place unnamed — never guess a park,\nreserve, region, biome or city from them.\n\nReturn only the note itself: no preamble, no sign-off, no heading, no title, no\nMarkdown formatting, no restating of this instruction. Start with the first\nsentence of the note.\n```\n\nSame recording, after:\n\nA forte presença de corruíras e sabiás indica um habitat de matagal aberto, possivelmente com árvores frutíferas na região. A identificação do sabiá-poca com menor confiança sugere a possível existência de áreas mais sombreadas ou de vegetação densa próximas. Próximo, procure por cantos altos nas árvores, pois o sabiá-laranjeira tende a se exibir em posições elevadas para anunciar sua presença.\n\nNo preamble, no invented place, and it still flags the low-confidence detection as low-confidence.\n\nThen I checked the inverse, because a fix that only works in one direction isn't a fix: with `--place \"Parque do Carmo\"` it uses the real name. And one more paranoid check — the English note told me to listen for Southern Yellowthroat, a species I hadn't noticed in the list. Was that a third hallucination? No: it's there at 0.59 confidence, 2 windows. The species rule had been holding the whole time. Only the location was unguarded.\n\n**The lesson I'm taking:** when you write an anti-hallucination rule, you're enumerating the categories of thing the model must not invent. I enumerated species and counts and felt covered. Place was a category I hadn't thought of, and it was sitting right there in the prompt as a bare pair of floats. Worth asking, of any prompt: *what else is in this context window that the model could decide it recognizes?*\n\nThat session, if you want to watch the diagnosis happen:\n\nI want to be straight about something: I have not yet taken this out on a real trail. The challenge offers bonus points for doing that and I'd rather tell you what I actually did than dress up a demo as a morning in the woods.\n\nWhat I did instead is arguably harder on the tool. [iNaturalist](https://www.inaturalist.org) has an open API full of bird observations that carry an audio recording, the exact coordinates, the exact date, and a species identification verified by the community (*research grade*). That is a labelled test set with real geography attached — and nothing about it was chosen by me to flatter the tool. I pulled eight at random across six countries and ran each one with its own true coordinates and date.\n\n| Where | Truth | trilha's top guess |  | \n|---|---|---|---|\n| 🇧🇷 Parque Nacional das Emas | *Melanopareia torquata* | **Collared Crescentchest**`0.98` | ✅ | \n| 🇺🇦 Lviv Oblast | *Corvus corax* | **Northern Raven**`0.95` | ✅ | \n| 🇮🇹 Reggio Calabria | *Certhia brachydactyla* | **Short-toed Treecreeper**`0.94` | ✅ | \n| 🇺🇸 Bannock County, Idaho | *Corvus brachyrhynchos* | **American Crow**`0.93` | ✅ | \n| 🇩🇪 Bad Wurzach | *Rallus aquaticus* | **Water Rail**`0.92` | ✅ | \n| 🇩🇪 Berg im Gau | *Tadorna ferruginea* | **Ruddy Shelduck**`0.91` | ✅ | \n| 🇩🇪 Duisburg | *Larus canus* | Common Whitethroat `0.52` →**Common Gull**`0.32` at #2 | ⚠️ | \n| 🇹🇭 Chanthaburi | *Arborophila cambodiana* | nothing above `0.10` | ❌ | \n\n**Six of eight correct on the first guess, seven of eight in the top three, 3.2–4.9 seconds each on CPU.** The range filter shrank the candidate pool differently at every site — 592 species in the Brazilian cerrado in January, 232 in western Ukraine in late November — which is the whole mechanism working as intended.\n\nThe two that didn't land are the interesting ones.\n\n**Duisburg** is a city gull recording, and the tool put a warbler on top of the real answer. The gull is right there at #2. This is the honest shape of the thing: on a messy urban recording with several birds and traffic, you get a ranked list, not an oracle. The confidence bars exist so you can see when it isn't sure, and `0.52` versus `0.32` is visibly not sure.\n\n**Chanthaburi is the one that taught me something.** Complete miss. So I asked the obvious question — does the model even know this bird? I grepped the acoustic model's label files:\n\n``` bash\n$ grep -i arborophila ~/.local/share/birdnet/.../labels/no.txt\nArborophila cambodiana_khmerhøne\n```\n\nIt knows it. All 11,560 labels include the Chestnut-headed Partridge. Then I checked what the range filter allowed at that coordinate:\n\n``` bash\n$ trilha here --lat 12.9257 --lon 102.1796 --date 2024-03-16 --limit 0 | grep -i partridge\n  Green-legged Partridge  (Tropicoperdix chloropus)\n```\n\n429 species in range, and *Arborophila cambodiana* is not one of them. The ear could have identified this bird. **The filter — the component I'd just finished praising as the thing that makes the tool trustworthy — had made the correct answer unreachable.** It's a Cardamom Mountains endemic with a tight range, exactly the kind of bird a range map is least confident about, and exactly the kind of bird someone pointing a recorder at a Thai forest most wants named.\n\nWhile tracing that, I found this in `detect.py`:\n\n```\nraise RuntimeError(\n    \"The range model expects no species at this coordinate. \"\n    \"Check --lat/--lon, or pass --no-range to listen worldwide.\"\n)\n```\n\nThere was no `--no-range` flag. I had written the error message for a feature I never built, and nothing caught it because that code path is hard to reach — you need a coordinate where the range model expects nothing at all.\n\nThe Thai partridge turned it from a cosmetic lie into a missing feature with a concrete use case, so I built it. Same recording, filter off:\n\n``` bash\n$ trilha listen th-partridge.wav --lat 12.9257 --lon 102.1796 \\\n    --date 2024-03-16 --no-range --min-conf 0.1\n\n  11560 species worldwide · 3.6s on CPU · week 11\n\n   0.35 #######              Chestnut-headed Partridge\n   0.34 #######              Taiwan Barbet\n   0.21 ####                 Taiwan Scimitar Babbler\n   0.17 ###                  Malayan Partridge\n   0.10 ##                   Blue-throated Barbet\n```\n\nCorrect answer, first place. And look at what came with it: Taiwan Barbet at `0.34`, a hair behind, from 4,000 km away. Taiwan Scimitar Babbler at `0.21`.\n\nThat is the trade-off made legible in one screen. With the filter you get `0.98` on a Brazilian crescentchest and a wall in front of a Cambodian partridge. Without it you can reach the partridge, but Taiwan is suddenly a live hypothesis in Thailand. Neither mode is correct in general, so the tool now does what it should have done from the start: default to the filter, and give you a documented way out when you have reason to think it's the thing standing in your way.\n\nThe blind test and the Chanthaburi diagnosis, end to end:\n\nNot as a philosophical preference. This tool does not function without it.\n\n**No signal is the normal case, not the edge case.** Every model here is a file on disk. You run `trilha doctor` once at home on wifi, it confirms the weights are cached, and then it tells you: *\"Models are cached on disk. After this check you can go offline.\"* A hosted API is a hard dependency on a network that, by the nature of the activity, isn't there.\n\n**Coordinates are a movement profile.** Where you go birding, at what hour, how often, how it changes over a year — that's not a bird fact, it's a map of your habits and your free time. Sending that to someone else's server to get a bird's name back is a bad trade, and it's a trade you'd be making weekly for years. Here it never leaves the machine. There is no server to leak it.\n\n**I could read the filter's mind.** This is the one I didn't expect to matter and it turned out to be the whole story of the Thai recording. I could grep the label files to confirm the species existed, query the range model directly with `trilha here` to see what it allowed, find the discrepancy, and ship a flag to work around it — in an afternoon, without filing a ticket with anyone. Against a closed endpoint the entire investigation would have been *\"it returned nothing, I guess the bird isn't supported.\"* Open weights let you distinguish *the model doesn't know this bird* from *my pipeline threw the answer away.* Those are completely different bugs with completely different fixes, and from the outside they look identical.\n\n**Swappable voice.** `--model` takes anything Ollama runs. The note is the subjective part, so it should be the replaceable part — `gemma3:4b` on a laptop in the field, `gemma3:12b` at a desk with RAM to spare. Same tool, no code change, no vendor to ask permission from.\n\n**It costs nothing per use.** That blind test was 9 inference runs over 8 recordings, plus every re-run while I was chasing the partridge. On per-call pricing I would have tested three and called it good — and the Chanthaburi miss, the only genuinely instructive result of the day, was the eighth.\n\n```\ngit clone https://github.com/Tinhomagri/trilha-offline\ncd trilha-offline\npython3 -m venv .venv\n.venv/bin/pip install -e .\nollama pull gemma3:4b\ntrilha doctor\n```\n\nTwo xeno-canto recordings are included so you can try it before going outside:\n\n```\ntrilha listen samples/corruira-XC436932.mp3 --lat -23.55 --lon -46.63 --lang pt\n```\n\nThere's also `trilha here --lat --lon`, which lists what's in range at a coordinate with no audio at all — useful the night before, to know what you might be about to hear. And `trilha journal`, which prints every outing you've recorded, or exports it to Markdown.\n\n**Code:** [https://github.com/Tinhomagri/trilha-offline](https://github.com/Tinhomagri/trilha-offline) — MIT.\n\n**Best Use of Gemma** — Gemma 3 through Ollama writes every field note, locally, and it's the component that makes the journal readable rather than a CSV of confidence scores. It's also deliberately scoped: constrained by prompt to the detections it was handed, forbidden from naming the ground, and fully optional at runtime. The identification never depends on it.\n\nBuilt with BirdNET ([birdnet-team/birdnet](https://github.com/birdnet-team/birdnet)), [Gemma 3](https://ai.google.dev/gemma) and [Ollama](https://ollama.com). Test recordings from [iNaturalist](https://www.inaturalist.org) observers under their respective licenses; samples from [xeno-canto](https://xeno-canto.org).", "url": "https://wpnews.pro/news/trilha-identifying-birds-by-ear-on-the-trail-with-no-signal", "canonical_source": "https://dev.to/wellington_filipe_fccda4c/trilha-identifying-birds-by-ear-on-the-trail-with-no-signal-30k7", "published_at": "2026-10-06 22:05:16+00:00", "updated_at": "2026-10-06 22:17:36.975410+00:00", "lang": "en", "topics": ["artificial-intelligence", "machine-learning", "ai-tools", "natural-language-processing"], "entities": ["trilha", "BirdNET", "Gemma 3", "Ollama", "ONNX", "São Paulo", "Parque do Carmo"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/trilha-identifying-birds-by-ear-on-the-trail-with-no-signal", "markdown": "https://wpnews.pro/news/trilha-identifying-birds-by-ear-on-the-trail-with-no-signal.md", "text": "https://wpnews.pro/news/trilha-identifying-birds-by-ear-on-the-trail-with-no-signal.txt", "jsonld": "https://wpnews.pro/news/trilha-identifying-birds-by-ear-on-the-trail-with-no-signal.jsonld"}}