跳到正文
DEV Community· Melwin Siril Joel V·· 4 小時前AI 評分44

我發現 BirdNET 的問題,並以地區物種清單修正識別結果

I found a bug in this model, and i corrected it

AI 導讀

作者開發開源工具 chidambaram-birdnudge,按所在地區物種清單篩選 BirdNET 的鳥類識別結果,處理模型把遠方物種判作本地鳥類的問題。工具以 Node.js 和 ONNX 在本機執行,無需 Python、GPU、帳戶或網絡訊號,錄音不會上傳;模型標示 11,560 種,min-conf 預設為 0.05。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

I started this project because I noticed something wrong with how bird identification works in my own town.

BirdNET is a genuinely impressive open-source project. Point it at a recording and it will confidently tell you what you heard. I tried it on some audio recorded on my hostel campus in Chidambaram, Tamil Nadu, and it told me about Rose-ringed Parakeet, Common Grackle, and Dark-eyed Junco. The grackle is a North American species. It has never been in India. It was never in my garden. The model was not confused - it was sure, and wrong.

That is the whole problem in one clip. A global classifier trained on global audio will happily tell you about birds that live four thousand kilometres away, because nothing in its architecture knows or cares where you are standing. There is no "not in my region" button.

chidambaram-birdnudge is that button. It runs the same open weights on your own machine, then filters every prediction through a species list built from real observations near you. It costs nothing to run, needs no account, works with no signal, and never sends your recordings anywhere.

A local Node.js pipeline that takes phone recordings, identifies birds with an open-weight audio model, filters the results to a regional species list, and tells you where to go next.

The pipeline is five scripts:

script what it does
download-model.mjs fetches the BirdNET+ weights and label set
fetch-pack.mjs builds the local species list from iNaturalist observations within a radius
convert.mjs batch-converts .m4a / .mp3 / .ogg to the 32 kHz mono WAV the model wants
analyze.mjs runs ONNX inference, writes a detection CSV per clip
summarize.mjs scores predictions against the pack, writes docs/overview.md
case.mjs generates the case study from the detection data

Two design decisions are worth calling out.

The model runs whole-file peak-normalised, so a quiet outdoor clip and a loud indoor one are comparable. This matters more than it sounds - my outdoor clip was about 6 dB quieter than my indoor ones, and without normalisation the comparison would have been meaningless.

min-conf defaults to 0.05, not 0.25. BirdNET's own web demo defaults much higher, which means it silently discards low-confidence-but-real detections. I lowered it so the local filter gets to see everything, then let the pack do the rejecting. The pack is a better filter than an arbitrary threshold, because it is specific to where you are.

Repo: https://github.com/Emanuelmsjv/chidambaram-birdnudge

Demo

Real output from a real run, on the five clips described above:

model    ...\assets\BirdNET+_V3.0-preview3.1_Global_11K_FP16_pruned.onnx
labels   11560 species
settings chunk=3s overlap=0s minConf=0.05

model load: 971ms
inside-2.wav  9.9s audio  0.4s compute  6 detections
    0.870  Yellow-billed Babbler  (Argya affinis)
    0.589  Rufous Treepie  (Dendrocitta vagabunda)
    0.068  Indian White-eye  (Zosterops palpebrosus)
inside.wav  17.9s audio  0.5s compute  15 detections
    0.812  Yellow-billed Babbler  (Argya affinis)
    0.415  Common Grackle  (Quiscalus quiscula)
    0.283  Oriental Magpie-Robin  (Copsychus saularis)
outside.wav  17.5s audio  0.5s compute  9 detections
    0.873  Rose-ringed Parakeet  (Psittacula krameri)
    0.731  Common Myna  (Acridotheres tristis)
    0.509  Red-vented Bulbul  (Pycnonotus cafer)

5 file(s), 47 detections

And what the filter actually does with that output:

$ npm run summarize
Clips: 6  detections: 146
In-pack (top-5): 15/27 = 55.6%
In-pack (all 53 distinct): 24 = 45.3%

Code

The whole pipeline is open source under CC BY-NC-SA 4.0. Everything below runs on your
own machine with Node 18+, no Python, no GPU, and no API keys.

GitHub logo Emanuelmsjv / chidambaram-birdnudge

Offline open-weight bird ID for Chidambaram, Tamil Nadu. BirdNET V3.0 via ONNX Runtime in pure Node - no Python, no GPU. Output is filtered against an iNaturalist-derived local species pack, and scored with a label-free measure of how plausible that output is for this location.

chidambaram-birdnudge

Point an open-weight bird sound model at a 10-second recording of somewhere in Chidambaram, Tamil Nadu. Get back one sentence telling you which species plausibly live near you, whether they are in season right now, what time of day they are worth listening for, and a real place within reach to go and stand.

The model already exists and is very good. It is also, on published evidence close to useless in this part of India. That gap is the project.

The premise

A 2026 global evaluation of BirdNET across 4,224 soundscapes, 67 sites and every continent, annotated by local experts, reported PR AUC of:

Region PR AUC
North America / Oceania / Europe 0.16 – 0.23
Central & South America 0.13
Africa and Asia 0.03 – 0.04

Chidambaram sits in that last band. Globally, the share of predictions that are correct (precision) is around 0.57–0.71, so roughly a…

Repo: https://github.com/Emanuelmsjv/chidambaram-birdnudge

A one-command setup fetches the 68 MB model weights and builds the local species pack:

git clone https://github.com/Emanuelmsjv/chidambaram-birdnudge
cd chidambaram-birdnudge
npm install
npm run setup        # downloads the 68 MB model, builds your species pack

npm run convert      # phone .m4a -> 32 kHz mono WAV
npm run analyze clips --out data/detections
npm run summarize    # writes docs/overview.md
git clone https://github.com/Emanuelmsjv/chidambaram-birdnudge
cd chidambaram-birdnudge
npm install
npm run setup        # downloads the 68 MB model, builds your species pack

npm run convert      # phone .m4a -> 32 kHz mono WAV
npm run analyze clips --out data/detections
npm run summarize    # writes docs/overview.md

Requires Node 18+ and ffmpeg on PATH. No Python, no GPU, no API keys.

Licence: CC BY-NC-SA 4.0. BirdNET weights are CC BY-NC-SA. iNaturalist observation data retains its own per-record licences, referenced in data/pack.json.

How I Built It

Model BirdNET+ V3.0-preview3.1 Global 11K FP16 (pruned)
Weights format ONNX, 68.2 MB
Runtime onnxruntime-node
Labels 11,560 species, plain CSV
Hardware CPU only. No GPU. No Python.

The model is a TarsNet2 classifier - a frozen convolutional backbone with a linear classification head. That architecture is the reason this project is tractable offline: the expensive part is fixed, and the part you retarget is one dense layer.

Speed: the five-clip case study below is 83.8 seconds of audio, analysed in under 4 seconds after a ~1 second model load. That is roughly 25x realtime on a laptop CPU with no accelerator.

Why Does Open Innovation Matter?

The challenge asks where an open approach did better than a closed one. I can answer that with a measurement rather than an opinion.

Same weights, same code, two recordings:

audio in-pack fraction
upstream BirdNET sample, recorded in New York 1 / 20
my recordings, Chidambaram 23 / 33

Nineteen of the twenty species the model proposed for the New York audio do not exist within 30 km of Chidambaram. The twentieth is House Sparrow at 0.051 - rank 20 of 20 by confidence, barely clearing my own threshold. Every confident prediction was a bird that does not live here:

A hosted bird-ID service gives you a fixed species list, the one its provider chose. If that list does not contain your local birds, you cannot add them. With open weights I can change what the model is allowed to say. That is the entire difference between 1/20 and 23/33, and it is not available behind an API.

The other reasons the open pieces mattered:

It runs with no signal. After a one-time 68 MB download, npm run analyze never touches the network. A bird call identifier is only useful where bird calls happen, which is usually nowhere with good connectivity. I did not get to actually test this on a trail with no bars - I tested it on a laptop, and the mobile app I originally used needed a working connection. I am not claiming more than I measured.

Your recordings never leave your machine. The case study below is audio recorded at a university hostel. Uploading that to a third-party inference endpoint hands someone a location-precise recording of where a person lives and when they were there. Local inference means the audio is read once by decodeWav and never transmitted.

It costs nothing. No API key, no per-minute billing, no quota. A hosted audio-classification endpoint bills by the minute precisely when you most want to use it and least want to depend on it.

The model is replaceable. The labels are a CSV and the regional list is JSON. Retargeting to another city is a data edit, not a code change. Nothing about the pipeline is specific to BirdNET except the inference call.

Honest limits of the open approach

Being straightforward about where this does not win:

  • The weights are CC BY-NC-SA, so this is non-commercial. That is fine for a personal tool and a real constraint if you wanted to build a product.
  • Fine-tuning for a local species set is architecturally trivial here (frozen encoder, linear head, logistic regression on embeddings, trainable on CPU) but I did not do it. With four labelled clips and no working route to bulk training audio, any claim I made about it would be unverified.
  • BirdNET's Python pipeline has features my port does not - proper spectrogram caching, multi-label handling, and more careful segmenting. I ported the part that made my question answerable, not the whole project.

I took it outside

The brief offers "a bird call identifier that works on the trail with no signal" as an example. I took it literally.

I heard a group of Yellow-billed Babbler (Argya affinis) on my hostel campus and recorded five clips on my phone - two indoors, one outdoors, and two while I was talking, to see what happened. Then I ran them through the pipeline.

The species was already in my local pack at rank 18 of 130, with 19 iNaturalist observations in range - a locally well-established bird, not a rarity.

clip babblers audible? model's best score
inside-2 yes 0.870
inside yes 0.812
speech-2 yes 0.961
speech no none
outside yes none

Three of the four clips where I confirmed the bird was audible got a correct first-place identification.

The two I find most interesting are the speech pair. Both were recorded in the same room, minutes apart, with me talking. I listened back afterwards to check what was in the background. In one, babblers were chirping behind my voice; in the other, there were none. The model returned 0.961 for the one where they were there and nothing at all for the one where they were not.

That distinction matters, because the cheap explanation would have been that the model just fires on human voices and happened to point at a common local babbler. Two clips with identical foreground content and opposite answers rules that out.

The one result I still cannot fully explain is speech-2, where the babbler is buried under speech and still scored 0.961. I am reporting it because it happened, not because I understand it.

What it got wrong

Every one of these is in the repo, not just here.

It missed the bird entirely on the outdoor clip. I was there, the babblers were there, and the model proposed parakeet and myna instead. That clip was also my quietest recording. I still do not know whether the model failed or the microphone failed. It is the first thing I would try to fix.

Confidence is not a probability. BirdNET will hand you 0.9 and mean something much weaker. The New York control demonstrates this directly - the model was extremely confident about species that were not present at all. The number is a ranking signal, nothing more.

My species pack is biased, and I can show how. It is built from iNaturalist, which is a photography platform. I pulled 1,000 research-grade observations from around Chidambaram and checked the result: Indian White-eye appeared zero times. It is one of the most common birds in India and it is, to my ear, constantly audible from my window. The pack over-represents birds people stop to photograph - Pied Stilt is the second most-recorded species in range - and under-represents small common passerines nobody bothers to upload.

So "not in your local pack" is weaker evidence than it appears. It means not established as local by this data source, not absent. I would not have found this without checking the raw observations against my own pack.

I had a real bug for most of this project. The pack builder wrote entries as { rank: i + 1, ...s } - the spread lands after, so the taxonomic rank from iNaturalist overwrote my ordinal in all 130 entries. Every rank field was the string "species". The ordering was still correct, which is exactly why I did not notice for weeks. Fixed in a087f8f.

My recordings were not independent data. I collected them, then evaluated the model on them. There is no held-out set, and with four positives any accuracy number would be meaningless. I have not claimed one anywhere.

My Agent Session

Here is the session where the agent found the rank bug, in full. It is twelve messages: you can watch it notice that rank prints as "species" instead of a number, work out that { rank: i + 1, ...s } lets iNaturalist's own rank string land on top of the ordinal, and fix it. The interesting detail is that nothing was broken as far as anyone could tell - the pack was still sorted correctly the whole time. The bug only became visible because a species position was checked against a literal 18.

View this session on DEV

This project was built end to end with an AI coding agent (opencode, tracked with DevRelay). The interesting part is not that it worked, but the two places where it did not:

  • The rank bug above survived several rounds because the agent optimised the thing I asked about and the symptom I noticed - ordering - happened to still be correct.
  • A ffmpeg download was corrupted twice in transit, producing a 66 MB "executable" that Windows rejected with "not a valid application for this OS platform." The PE header was valid and the machine type was correct; the file was truncated. Diagnosing that took longer than installing ffmpeg should have.

A third failure worth naming: I spent a deadline-night attempt adding a local Gemma 3 270M model to write the field brief in plain English. It ran, and every single output was invented - weather forecasts at 35 C, links to websites that do not exist, and not one mention of the bird it was supposed to describe. I deleted the feature rather than ship a birder being sent to check a website. The detection and the species filter are the project. The prose was decoration, and bad decoration is worse than none.

Prize Categories

  • Overall - this is what I am actually aiming for
  • Best Use of Entire - the project was built end to end in an agent session, the session above is embedded, and the write-up is the account of where that help stopped being useful

I considered claiming DigitalOcean and Render and deliberately did not. Deploying a deliberately-offline tool to a cloud runtime to win a hosting prize would contradict the entire premise of the project. Same reasoning for Sentry Agent Tracing - this is deterministic DSP with an ONNX model, no LLM calls, no tokens, no spans to trace.

Full write-up

The complete case study, including the control methodology and every non-local species the model proposed, is in the repo: docs/case-report.md

來源:DEV Community · dev.to