- Go 72%
- Svelte 12.5%
- TypeScript 11.8%
- Shell 2.6%
- Python 0.4%
- Other 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
* fix(classifier): give the primary classifier a stale-path fallback and stop silent model substitution The primary BirdNET v2.4 slot was the one configured model path with no fallback at all. Its Tier-3 branch took settings.BirdNET.ModelPath whenever the string was non-empty and returned the os.ReadFile error, so under the conditions GitHub #4201 and #4204 describe (a gallery variant switch, or a container HOME change that invalidates a stored absolute path) a user who had selected a DFT primary variant got a hard NewBirdNET failure. That is worse than the secondary case it mirrors: no analysis at all, rather than one missing optional model. Resolve the configured primary path once, before the identity tier switch, through a resolver injected into NewBirdNET. Resolving before the switch is what makes the recovered-to-empty case behave exactly as if no path had ever been configured: Tier 3 stops firing, Tier 4 resolves the default, and remapV24ToONNXOnARM64 (which returns early on a non-empty CustomPath) is free to remap arm64 to the INT8 ONNX build. Every load-path consumer now reads the resolved value through configuredModelPath(), so identity resolution, backend dispatch and the file open can no longer disagree about which file is loaded. settings.BirdNET.ModelPath is never mutated; the repair goes through the existing correction queue, and the primary family is model-only because applyConfigForPrimarySwap documents that BirdNET.LabelPath must survive a variant swap. reloadModelInternal re-resolves through the same resolver. Without that, a start that successfully recovered a stale path would fail its very next settings reload: the identity gate would compare the recovered path against the still-stale config and refuse with "requires orchestrator restart", so a user who hit the original bug would get a second, louder one on their next save. The primary keeps its configured path on an indeterminate stat error rather than falling back, unlike the secondaries, because silently swapping the primary would attribute every detection written meanwhile to the wrong model. Separately, an unreadable (not missing) configured path substituted a different model with no user-visible signal. pathResolution now carries substituted and repairable independently, so that case is queued and reported while config.yaml is still never rewritten, and planPathCorrection returns an explicit outcome instead of a nil snapshot that meant two different things. isGalleryManagedPath refuses any path whose written form differs from its expanded form, so a repair can no longer flatten $HOME or ~ into an absolute path, which is the fragility the self-heal exists to recover from. Verified against a built binary: with a stale birdnet.modelpath, origin/main fails to start; this recovers the installed variant when one is present and boots the built-in model when none is, leaving the user's configuration intact in both cases. * fix(classifier): scope the primary recovery to the v2.4 family and to usable backends Gate review of the previous commit found three defects in it, all of which turn the recovery against the users it exists to help. The primary slot is family-selectable through birdnet.version, but both the recovery target and the queued correction label were hard-wired to the BirdNET v2.4 registry ID. A stale v3.0 primary path would therefore have been recovered onto a v2.4 model file: a 32 kHz/5 s identity pinned to a 48 kHz/3 s model with a different label set, which fails late with a label-count mismatch or, were the counts ever to agree, would attribute every detection to the wrong model. That is exactly the cross-variant pairing hazard resolveFamilyPaths prevents for the secondary families. The resolver is now installed only when the slot really is the v2.4 family; every other family keeps the previous verbatim behaviour. Every installed primary variant is an ONNX build, because the built-in baseline declares no files. Recovering onto one where ONNX Runtime is unavailable turned a recoverable stale path into a hard startup failure, since initializeModel has no TFLite fallback once usesONNXBackend is true. The recovery now checks the variant's backend first and falls through to the built-in baseline instead, with an injected seam so both sides of that decision are testable without a runtime. reloadModelInternal matched no case when the configured path resolved away to nothing, so ModelInfo kept naming the vanished file while initializeModel loaded the baseline underneath it and the reload reported success. It now refuses, but only when the resolution CHANGED from a real file to nothing. Keying on "substituted" alone would fire in the steady state after a successful recovery onto the baseline, where config keeps the stale path by design, and would fail every settings save forever. Both directions are pinned by tests. The expand guard moved off isGalleryManagedPath, which serves all four families from one call site, into the primary's repairable decision, so the three secondaries keep the behaviour they have on main. The substituted notification now carries an explicit unreadable flag rather than deriving the wording from "not repairable", which has more than one cause and told a user whose file was absent to go and check its permissions. correctionNoop is now the enum zero value so the only outcome that writes config.yaml is never the accidental one. Also restores TestNewBirdNET_RecoversStaleConfiguredPath, which an editing mistake in the previous fix pass deleted along with the test above it. * fix(classifier): let an OpenVINO host recover, and name the path the reload actually rejected Round 3 of the gate, closing damage the round 2 fixes introduced. The backend gate added in round 2 asked whether ONNX Runtime was available, but initializeModel tries OpenVINO FIRST for the v2.4 identity and only falls through to ONNX Runtime when OpenVINO declines. An openvino-tagged build on an A76/Pi5 or an Intel iGPU runs the DFT-truncated primary variants with no ONNX Runtime installed at all, so gating on ORT alone refused a variant that would have loaded: such a host silently dropped to the embedded model and was told no installed model was available, which was false. The gate now mirrors initializeModel's own order and refuses only when neither backend can run the file. The reload refusal interpolated the configured path AFTER rollback had already restored the previous settings snapshot, so the error, and the Sentry context beside it, named the old path and asserted a healthy file was unusable. The path is now captured before the rollback. Two guards added in earlier rounds had no test that could fail if they were removed. The family gate is extracted into primaryPathResolverFor so the decision is reachable without constructing a real model, and the notification wording no longer being derived from repairable is pinned by a test carrying two records that share a repairable value and differ in whether the file exists. Both were verified by reverting the guard and watching the suite fail. Also corrects three comments that described the design before these changes rather than after them. * fix(classifier): require the OpenVINO runtime to actually load, and name the version the user typed Round 4, closing damage the round 3 fixes introduced. The review of that round found both, and CI could not have. The backend gate accepted a variant whenever openVINOPlanFor reported a plan, but that is an eligibility answer, not a loadability one. initializeModel also falls through to ONNX Runtime when OpenVINO is eligible and then FAILS to load, and the CPU branch of the planner answers yes from the CPU's f16 support without ever opening the library. So a host with a broken or missing OpenVINO library and no ONNX Runtime was handed the ONNX path it has no runtime for: the hard startup failure this gate exists to prevent, reached from the other side. The gate now requires the runtime to open. That change also falsified an existing test rather than updating it. The test stubbed only the ONNX Runtime seam, so it passed on a bare runner and failed under -tags openvino on any host with a plannable device. CI compiles and vets that tag but never runs its tests, so it would have reached the openvino image rather than a red pull request. Both backends now have a seam, the test stubs both, and two new subtests cover the legs that were unreachable before: OpenVINO alone being sufficient, and an eligible-but-unloadable runtime not counting. The read-after-rollback fix from round 3 was applied at one site and left at its identical sibling: the unknown-version branch built its message after rollback had restored the previous settings, so a user who typed a bad birdnet.version was told their previously valid version was unknown, or shown an empty string. Both sites now capture before the rollback, and both are pinned by tests that use distinct old and new values, which the round 3 test did not, so the mutation survived. * test(classifier): stop the OpenVINO recovery test depending on the developer's host The subtest covering "an OpenVINO plan alone is enough to recover" gated on the openvino build tag, but the tag is necessary and not sufficient: openVINOPlanFor also needs a usable device, so an openvino build on a plain amd64 runner or an ARMv8.0 core yields no plan and the assertion inverts. It passed here only because this machine has a real OpenVINO install with a usable GPU. That is the same host-dependence, mirrored, that the round it replaced had shipped. It now branches on whether a plan is actually obtainable, verified by forcing both device probes false and confirming the test still passes. Also moves openVINOLoads below isGalleryManagedPath. Inserted above it, the new function absorbed that function's doc comment, leaving isGalleryManagedPath undocumented and attributing to openVINOLoads a claim that is false of it: the absorbed text states the function takes no Orchestrator lock and is reached only after the loaders release o.mu, whereas openVINOLoads runs under bn.mu and takes inference.ovInitMu. go doc now shows each comment on its own function. * fix(classifier): report the resolved model path on the locked inference error paths Review feedback on PR #4215. Two of the three ModelContext decorations in Analyze run after bn.mu is taken, so they can read bn.primaryPath safely and now report the model file the instance is actually running rather than the configured one, which after a stale-path recovery names a file that was never opened. The decoration before the lock keeps reading the configured value: bn.primaryPath is written under bn.mu by reloadModelInternal, so reading it there would be a data race, and reporting the resolved path on that path needs a lock-free published copy alongside bn.identity. Also records what the deferred config-repair drain in loadAdditionalModels would mean if that function ever gained an error return, since the previous comment justified only panic safety. |
||
| .agents/skills/preflight | ||
| .claude/skills | ||
| .cursor/rules | ||
| .devcontainer | ||
| .github | ||
| .husky | ||
| .serena | ||
| cmd | ||
| data | ||
| dist | ||
| doc | ||
| Docker | ||
| docs | ||
| examples | ||
| frontend | ||
| internal | ||
| Podman | ||
| rules | ||
| scripts | ||
| tools | ||
| ui | ||
| Unraid | ||
| vm-images | ||
| .air.toml | ||
| .dockerignore | ||
| .gitignore | ||
| .golangci.yaml | ||
| .mockery.yaml | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| AUTHORS | ||
| CLAUDE.md | ||
| cliff.toml | ||
| config.schema.json | ||
| CONTRIBUTING.md | ||
| Dockerfile | ||
| generate-multi-taxa | ||
| go.mod | ||
| go.sum | ||
| install.sh | ||
| LICENSE | ||
| LICENSES.md | ||
| main.go | ||
| podman-install.sh | ||
| PRIVACY.md | ||
| README.md | ||
| release-manifest | ||
| reset_auth.sh | ||
| soundscape.wav | ||
| support.sh | ||
| Taskfile.yml | ||
| tawnyowl.wav | ||
| TESTING.md | ||
BirdNET-Go
Realtime soundscape analyser for birds, wildlife, and bats.
Self-hosted, 24/7, local AI inference. BirdNET-Go ingests soundcard input or network audio streams, runs multi-model classification, and presents detections in a fast web UI. Runs on a Raspberry Pi.
Highlights
- Multi-model AI gallery: install BirdNET v2.4, Google Perch v2, BattyBirdNET regional bat classifiers, and the BirdNET Geomodel v3.0 from inside the app, no rebuild needed.
- Run multiple models in parallel against separate audio sources and let cross-model agreement boost confidence on shared detections.
- Live spectrogram streaming and detection heatmaps rendered straight in the browser.
- Alert rules engine that routes detections to Discord, Slack, Telegram, ntfy, Pushover, Gotify, Matrix, webhooks, browser push, MQTT (with Home Assistant discovery), shell scripts, and BirdWeather.
- Production-ready ops: onboarding wizard, OIDC/SSO, TLS certificate management, hot-reload settings, system health page, database doctor, and one-click support dumps.
- Installable as a PWA, with 15 UI languages and species names in 40+ languages.
- Local-only by default. Optional Sentry telemetry is strictly opt-in.
Quick install
Debian, Ubuntu, and Raspberry Pi OS:
curl -fsSL https://github.com/tphakala/birdnet-go/raw/main/install.sh -o install.sh
bash ./install.sh
Docker images are published for linux/amd64 and linux/arm64. Pre-built binaries for Linux, Windows, and macOS ship with each release. See the installation guide, hardware recommendations, and security guide for details.
Web Dashboard
Features
Detection
- BirdNET v2.4 (default, embedded, 6,500+ bird species)
- Google Perch v2 via ONNX (14,795 species across birds, insects, amphibians, and mammals) with taxonomy-based species names
- BattyBirdNET bat classifiers, 11 regional models covering Africa, the Americas, East Asia, Europe, the Middle East, South Asia, Southeast Asia, and the USA (Linux + ultrasonic-capable device)
- BirdNET Geomodel v3.0 for location-based range filtering (12,012 species)
- Cross-model detection consensus: agreement between models strengthens confidence and flags disagreements for review
- Custom classifiers: bring your own TFLite model and label set
- Configurable false-positive filtering for accurate results: Deep Detection (repeat-confirmation within a 15-second window), per-species dynamic thresholds, location-based range filter, privacy and dog-bark filters, and per-classifier bat false-positive levels (guide)
- Per-model and per-source confidence thresholds
Audio inputs
- Soundcard capture and RTSP / RTSPS streams, including multiple sources in parallel with independent model assignment
- Sample rates up to 256 kHz for ultrasonic bat detection
- Audio liveness watchdog with tiered recovery for flaky streams
- Stream sample-rate probing and per-model recommendation banners in the UI
- Audio equalizer, per-source quiet hours, daylight filter, and extended capture mode
- Offline analysis of audio files
Interface
- Svelte 5 + TypeScript single-page app
- Installable as a Progressive Web App (PWA)
- Onboarding wizard for first-run setup
- Live spectrogram visualization for active streams (live audio streaming)
- Detection heatmaps with ONNX-accelerated rendering
- Customizable dashboard layout, color schemes, and a "Currently Hearing" card
- Multiselect and bulk actions on the detections list
- Browser terminal (xterm.js over WebSocket PTY) for in-app administration
- 15 UI languages: English, German, French, Spanish, Portuguese, Dutch, Polish, Italian, Czech, Slovak, Hungarian, Finnish, Swedish, Danish, Latvian
- Species names in 40+ languages
Alerts and integrations
- Configurable alert rules engine with per-rule conditions, schedules, and delivery targets
- Multi-target delivery via shoutrrr: Discord, Slack, Telegram, ntfy, Pushover, Gotify, Matrix, Bark, IFTTT, and more
- Webhooks with custom templates, shell-script hooks, and browser push notifications
- MQTT publishing with Home Assistant auto-discovery
- BirdWeather.com API integration
- Prometheus metrics endpoint
- Live spectrogram and realtime log output for OBS overlays on bird-feeder streams
Storage and data
- SQLite (default) or MySQL with retry-aware write paths for contention
- Automatic backups with real-time status polling
- Format-aware audio clip export
- Embedded eBird/Clements taxonomy (2,374 genera, 254 families, 11,145 species) for fast offline lookups
Operations
- System Health diagnostics page covering audio pipeline, models and inference, network, and the datastore
- Database doctor for diagnosis and schema repair
- Help & Support page with guided bug reporting and one-click support dumps
- OIDC / SSO with Google, GitHub, and generic providers, including RP-Initiated Logout
- TLS certificate management UI with transactional writes and backup/restore
- Hot-reload for settings, model installs, and per-source model assignments (no restart)
- Optional, opt-in Sentry telemetry with strict privacy filtering
Platform
- Linux, Windows, and macOS
- Single static binary with the BirdNET TFLite model embedded
- Optional ONNX Runtime for Perch, BattyBirdNET, and BirdNET Geomodel v3.0 (install guide)
- Multi-arch Docker images
- Runs comfortably on a Raspberry Pi 4 or equivalent 64-bit single-board computer
Documentation
- FAQ - common questions, issues, and workarounds
- User guide
- Installation
- Hardware recommendations
- ONNX Runtime installation
- Detection pipeline
- Database Doctor
- Training a custom classifier
- Cloudflare Tunnel
- Security
- Telemetry and privacy
- RTSP troubleshooting
Development setup
See CONTRIBUTING.md for the full guide.
git clone https://github.com/tphakala/birdnet-go.git
cd birdnet-go
# Install Task (if not already installed)
# Linux: sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
# macOS: brew install go-task
task setup-dev # installs Go 1.25, Node LTS, build tools, linters, Playwright
task # build
task dev_server # hot-reload dev server (or: air realtime)
Community
Join the Discord server for support, discussions, and updates.
Related projects
Core and extensions
- BirdNET-Analyzer: upstream BirdNET project
- BirdNET-Go Classifiers: enhanced BirdNET classifiers with additional species
- BattyBirdNET-Analyzer: bat classifier models, installable via the in-app gallery
Companion tools
- birda: fast CLI for offline bird species detection on existing audio files, using BirdNET and Perch AI models
- birda-gui: desktop GUI for birda (Electron + Svelte 5) for audio analysis, detection browsing, and species visualization
System integration
- Cockpit BirdNET-Go: web-based system management plugin using the Cockpit framework
Migration tools
- BirdNET-Pi2Go: database converter for migrating from BirdNET-Pi (deprecated, not compatible with the current BirdNET-Go database schema; a native import is planned)
Hardware solutions
- BirdNET-Go ESP32 RTSP Microphone: ESP32-based RTSP streaming microphone
- ESP32 Audio Streamer: alternative ESP32 RTSP streaming solution
- M5Stack Atom Echo RTSP Mic: RTSP audio server for M5Stack Atom Echo, no soldering required
- M5Stack AtomS3 Lite PDM Mic: RTSP audio server with MEMS PDM microphone
Mobile apps
- Perch: open-source Android/iOS companion app. Connects to BirdNET-Go via the BirdWeather API. Live detection feed, audio playback, species browser, 14-day chart, and notifications for favourite species. MIT licensed.
Contributing
Contributions are welcome.
For setup, workflow, and quality gates, see CONTRIBUTING.md:
- TL;DR quick start: 5-minute setup
- Development workflow: hot reload, git hooks, testing
- License and privacy: CC BY-NC-SA 4.0, privacy by design
All contributions must follow privacy-by-design principles, the automated code-quality gates, and the CC BY-NC-SA 4.0 license terms.
Data sources
Taxonomy data
BirdNET-Go embeds taxonomy data derived from the eBird/Clements Checklist:
- Source: eBird API v2
- Copyright: (c) Cornell Lab of Ornithology
- License: used under eBird API Terms of Use for non-commercial purposes
- Attribution: taxonomy data powered by eBird.org
- Coverage: 2,374 genera, 254 families, 11,145 species
For more information, see the eBird Taxonomy.
License
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.
Authors and acknowledgements
Created and maintained by Tomi P. Hakala.
A growing list of community contributors keeps the project moving forward. The current list lives on the GitHub contributors page.
The BirdNET AI model is by the K. Lisa Yang Center for Conservation Bioacoustics at the Cornell Lab of Ornithology in collaboration with Chemnitz University of Technology (Stefan Kahl, Connor Wood, Maximilian Eibl, Holger Klinck).
Google Perch v2 ONNX conversion by Justin Chuby. BattyBirdNET bat classifier models by R.D. Zinck. BirdNET label translations by Patrick Levin for the BirdNET-Pi project by Patrick McGuire.