Realtime BirdNET soundscape analyzer
  • Go 72%
  • Svelte 12.5%
  • TypeScript 11.8%
  • Shell 2.6%
  • Python 0.4%
  • Other 0.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tomi P. Hakala ad1b5f2f9c
fix(classifier): give the primary classifier a stale-path fallback and stop silent model substitution (#4215)
* 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.
2026-08-28 20:47:07 +03:00
.agents/skills/preflight chore(preflight): add regression/backward-compat reviewer and maintainer-confirm triage 2026-06-01 10:51:58 +03:00
.claude/skills chore: stop tracking .claude/scheduled_tasks.lock (#3396) 2026-06-06 18:49:27 +03:00
.cursor/rules feat: Enhance SSE implementation with improved reliability and performance - Increase SSE client channel buffer from 10 to 100 to handle high detection periods - Add rate limiting middleware (10 requests/minute) to SSE stream endpoint - Add RetryConfig field to SSEAction for improved failure resilience - Update SSEBroadcaster to return errors for proper error handling - Add comprehensive error logging with retry status indicators - Provide fallback error handling for transient SSE broadcasting failures 2025-06-02 17:02:28 +03:00
.devcontainer build: move TensorFlow headers from $HOME to project-local .cache/ (#2955) 2026-06-16 13:01:52 +03:00
.github ci(deps): bump docker/setup-buildx-action in the actions group (#4184) 2026-08-25 17:05:04 +03:00
.husky feat: per-visitor species name localization via client-side dictionaries (#3463) 2026-06-12 17:25:51 +03:00
.serena chore: track Serena project.yml (go, typescript, svelte) 2026-06-12 11:37:31 +03:00
cmd fix(classifier,analysis,api): recover from stale model paths and register gallery models with running sources (#4207) 2026-08-28 15:34:36 +03:00
data feat: add latest.json file containing data for various ostrich and cassowary species, enhancing biodiversity information 2025-04-25 13:05:44 +03:00
dist docs(dist): add platform-specific READMEs to release tarballs 2026-05-19 21:22:42 +03:00
doc feat(privacy): stream per-source VAD on the upstream sequence model (#4183) 2026-08-25 00:24:39 +03:00
Docker feat(podman): enable rootless sound card access in shipped artifacts (#4122) 2026-08-11 17:41:26 +03:00
docs feat(release): publish and maintain a machine-readable release manifest (#3631) 2026-06-22 18:51:32 +03:00
examples docs: add example script for testing secure MQTT connections 2025-07-02 19:17:45 +03:00
frontend fix(classifier): give the primary classifier a stale-path fallback and stop silent model substitution (#4215) 2026-08-28 20:47:07 +03:00
internal fix(classifier): give the primary classifier a stale-path fallback and stop silent model substitution (#4215) 2026-08-28 20:47:07 +03:00
Podman feat(podman): enable rootless sound card access in shipped artifacts (#4122) 2026-08-11 17:41:26 +03:00
rules fix: modernize codebase with Go 1.26 patterns and address linter issues (#1959) 2026-02-14 14:47:11 +02:00
scripts fix(profiling): tie off the batched follow-ups from the profiling epic (#4073) 2026-08-01 12:30:14 +03:00
tools fix(db-doctor): add precipitation columns to hourly_weathers definitions (#4053) 2026-07-28 09:59:41 +03:00
ui feat: implement modern Svelte 5 desktop UI with comprehensive component library 2025-07-19 10:04:27 +03:00
Unraid docs: format MD files using prettier to meet markdown formatting requirements 2025-08-09 10:12:17 +03:00
vm-images Minor typos in the documentation (#1327) 2025-09-27 11:31:38 +03:00
.air.toml fix(build): scope git describe to release tags to avoid "manifest-*" version (#3770) 2026-07-02 15:14:37 +03:00
.dockerignore fix(ci): resolve date-fns import failure in Docker and CI builds (#2887) 2026-04-28 13:05:22 +03:00
.gitignore fix(profiling): tie off the batched follow-ups from the profiling epic (#4073) 2026-08-01 12:30:14 +03:00
.golangci.yaml ci(lint): re-enable goconst under golangci-lint v2.12.2 (#3789) 2026-07-04 21:47:50 +03:00
.mockery.yaml fix(api/v2): alerts return 409 (not 404) without v2 DB, 503 on engine init failure (#3724) 2026-06-27 16:02:32 +03:00
AGENTS.md docs: drop sunset Gemini Code Assist review references (#3962) 2026-07-18 11:04:58 +03:00
ARCHITECTURE.md feat(audio): make native go-opus the default Opus encoder (#4163) 2026-08-23 10:41:26 +03:00
AUTHORS docs: update AUTHORS file with current contributors (#4072) 2026-08-08 18:19:37 +03:00
CLAUDE.md docs: drop sunset Gemini Code Assist review references (#3962) 2026-07-18 11:04:58 +03:00
cliff.toml fix: correct comment formatting in cliff.toml 2025-07-28 22:06:43 +03:00
config.schema.json feat(privacy): stream per-source VAD on the upstream sequence model (#4183) 2026-08-25 00:24:39 +03:00
CONTRIBUTING.md docs: add cross-platform preflight quality gate skill (#3131) 2026-05-17 12:39:32 +03:00
Dockerfile build(docker): split runtime libraries into a versioned base image (#4178) 2026-08-24 15:16:55 +03:00
generate-multi-taxa feat(events): add app event entity, repository, and emitter (#3215) 2026-05-21 12:44:35 +03:00
go.mod chore(deps): bump go-flac and go-opus to v1.1.0 2026-08-23 21:47:39 +03:00
go.sum chore(deps): bump go-flac and go-opus to v1.1.0 2026-08-23 21:47:39 +03:00
install.sh fix(install): report which step a cross-host migration failed at (#3952) 2026-07-17 12:23:28 +03:00
LICENSE initial release 2023-10-20 15:49:08 +00:00
LICENSES.md docs: update dependency licenses (#1756) 2026-01-12 13:54:12 +02:00
main.go fix(profiling): decouple block and mutex sampling from debug: true (#4033) 2026-07-25 16:45:02 +03:00
podman-install.sh feat(podman): enable rootless sound card access in shipped artifacts (#4122) 2026-08-11 17:41:26 +03:00
PRIVACY.md docs: format MD files using prettier to meet markdown formatting requirements 2025-08-09 10:12:17 +03:00
README.md feat(wiki): auto-sync GitHub wiki from doc/wiki (#3676) 2026-06-24 14:58:47 +03:00
release-manifest feat(release): publish and maintain a machine-readable release manifest (#3631) 2026-06-22 18:51:32 +03:00
reset_auth.sh fix: cleanup, adding more security fixes 2024-11-07 21:31:46 +02:00
soundscape.wav initial release 2023-10-20 15:49:08 +00:00
support.sh feat: add log collection for BirdNET-Go application 2025-04-30 17:33:03 +03:00
Taskfile.yml feat(models): region coverage maps and localized country lists in the gallery (#4129) 2026-08-15 20:39:08 +03:00
tawnyowl.wav initial release 2023-10-20 15:49:08 +00:00
TESTING.md feat: database normalization with migration support (#1901) 2026-01-31 17:30:57 +02:00

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

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.

Core and extensions

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

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

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:

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.