github tphakala/birdnet-go nightly-20260523
Nightly Build nightly-20260523

9 hours ago

The headline change in this release is that bat detection now works on network audio streams. Previously, bat models only ran against locally attached sound cards because the streaming pipeline forced every stream to 48 kHz, destroying the ultrasonic content bats are recorded at. Streams now discover their actual sample rate at connect time, so a 192 kHz PCM RTSP feed from an AudioMoth or USB ultrasonic mic on a remote Raspberry Pi can drive a bat model directly. Support dumps now bundle a database schema snapshot, application event log, and deployment context for much better remote troubleshooting. A series of database integrity fixes resolves upgrade failures where startup silently fell back to legacy mode, lost detections, or rejected MariaDB authentication. Non-bird species in the Perch v2 label set now show up under the correct genus, family, and taxonomic class in the species tree.

New Features

Bat Detection Over Network Audio Streams

Bat detection previously required a sound card attached directly to the BirdNET-Go host with ALSA exclusive mode; RTSP and other network streams were unusable because the audio pipeline hardcoded streams to 48 kHz, which is far below the 256 kHz capture rate bat models expect and which throws away all ultrasonic content above ~24 kHz.

Streams now probe their actual sample rate, codec, and channel count with ffprobe at connect time, and the buffer pipeline carries that rate end-to-end. A working RTSP feed from a remote AudioMoth or a USB ultrasonic microphone on a separate Raspberry Pi (the community example used a 384 kHz feed) can now drive a bat model directly with no sound card on the BirdNET-Go host.

The settings UI gains a Test Stream button next to the URL field on both the add-stream form and existing stream edit cards. It calls the new POST /api/v2/streams/test endpoint and shows the probed sample rate, codec name, and a bat-compatibility badge inline. When a stream uses a lossy codec (AAC, Opus, MP3) a dedicated warning explains that lossy compression destroys ultrasonic content even when the sample rate is high enough; only raw PCM or FLAC over RTSP can preserve the frequencies bat detection needs. Save and Add are gated on a successful test, with specific localized errors for unsupported URL schemes, blocked destinations (SSRF protection blocks loopback, link-local, metadata, and unspecified IPs), connection failures, and timeouts. The model checkbox list shows a bat-stream-requirements warning whenever a bat model is selected on a stream source so users see the hardware/codec constraints before they save (#3229, #3230, #3236, #3237).

Genus, Family, and Class Metadata for Perch v2 Non-Bird Species

Perch v2's label set has always included insects, amphibians, mammals, reptiles, arachnids, mollusks, and other non-bird taxa, but the embedded genus_taxonomy.json only carried bird taxonomy. Non-bird detections lacked genus, family, and class metadata, so the species tree, classifier metadata, and grouping in the UI fell back to hardcoded Class: "Aves" / Phylum: "Chordata" and produced incorrect hierarchies for everything outside birds. The taxonomy database is now extended via GBIF Backbone Taxonomy (4,110 additional species; 93% match rate), growing from 2,375 to 3,720 genera, 254 to 495 families, and 11,324 to 15,434 catalogued species. GetSpeciesTree() resolves the correct Class and Phylum dynamically (Arthropoda for insects/arachnids, Mollusca for snails/clams/squid, Cnidaria for hydrozoans, Platyhelminthes for trematodes, Chordata for vertebrates) so non-bird Perch v2 detections show up under the right branch of the tree. This is metadata only: Perch v2 itself still identifies the same species it always did, but they now display with correct taxonomic context. README and detection-pipeline docs updated to drop the inaccurate "bird species only" framing of Perch v2 (#3209).

Enhanced Support Dumps

Support dumps now include three new diagnostic artifacts to make remote troubleshooting practical without back-and-forth requests for additional logs:

  • database_info.json captures the complete database schema for SQLite and MySQL: table schemas, column definitions, indexes, row counts, integrity check, foreign key violations, migration state, app metadata, WAL/SHM sizes, and SQLite PRAGMA diagnostics. Each sub-collection has its own timeout (2-30s) so partial failures never kill the dump (#3213).
  • app_events.json contains the last 30 days of application events (max 1,000 entries): startup, shutdown, version changes, hot-reloads, model loads, settings saves with per-key diffs, migration state transitions, range filter updates, schema repair, and notification delivery attempts. Settings save events scrub sensitive values (passwords, tokens, API keys, certs) before persisting. Events are backed by a new persistent app event repository with a 90-day retention cutoff and 10k row cap (#3215, #3216).
  • deployment_info.json captures working directory, systemd service file (with environment variable scrubbing), data directory listing, and /proc/1/mountinfo for container bind mounts. Detects Docker, Podman, LXC, and systemd-nspawn. Useful for diagnosing fresh-install detection issues where a database exists but isn't visible to the new install (#3217).

Database startup also gains structured decision logs at every code path: fresh_install, v2_restart, legacy_mode, stale_sidecar_fallback, v2_corrupted, and migration state outcomes, with a safety scan that warns about nearby .db files when a fresh install is detected.

A new standalone tools/db-doctor/db-doctor.py script (zero dependencies, Python 3) diagnoses and repairs BirdNET-Go SQLite databases offline. The read-only mode runs eight checks (integrity, schema version, schema contamination across ai_models/labels/detections/daily_events, foreign keys, migration state, clip path extensions, and a database fingerprint); --fix recreates contaminated tables, recovers orphaned label references, resets stuck migration states, REINDEXes corrupted indexes, and repairs clip extensions against the filesystem. All repairs run in transactions on top of an atomic SQLite backup; lock detection prevents running while BirdNET-Go is active (#3212, #3223).

Periodic Pipeline Stats Logging

Two always-on info-level log summaries print every 5 minutes to make the detection pipeline diagnosable from regular logs. Pipeline stats report per source/model: inference count, raw classifier results, results that passed filter, max confidence seen, and the effective threshold. Audio level stats report per source: average, minimum, and maximum levels, zero percentage, and clipping percentage. Both suppress zero-activity periods to avoid log spam. Motivated by GitHub Discussion #3220 where users reported zero detections after model changes with no log evidence (#3226).

Model Selection Recommendation Banners

The model checkbox list on audio source configuration (sound cards and streams) shows contextual notification banners. When both BirdNET and Perch are installed but only BirdNET is enabled, an info banner recommends enabling both models for best species coverage. When only Perch is selected, a warning banner advises lowering the confidence threshold to 0.50 for meaningful results. Banners only appear when both model families are available; if only one is installed, no banner is shown. All 15 locales include native translations (#3228).

Automated Root-to-User Installation Migration

Users who previously ran sudo ./install.sh and got blocked by the root guard now get an automated migration path. When a root install is detected, the installer offers a 3-way prompt: migrate (recommended), fresh install, or cancel. The migration stops the existing service, copies data to the user's install location, fixes file ownership, validates the config and SQLite integrity, and cleans up the old systemd files. Pre-flight checks cover source validity, destination emptiness, and disk space with a 10% safety margin. On failure, the partial copy is removed so retry attempts aren't blocked. Silent mode (--silent) auto-migrates without prompting. Triggered by user reports of being unable to update after a Proxmox root install (#3191).

A separate change converts the hard root-check block to a soft block with a --force-root flag for users who understand the tradeoffs (containers, minimal setups). With --force-root, a one-line warning is shown and the install proceeds (#3206).

Health Diagnostics: FFmpeg and Sox Tool Checks

A new tool availability check in the Config category of the System Health page reports whether FFmpeg and Sox binaries are present at runtime, including the FFmpeg version string. The check reads paths from the live audio settings, so it stays accurate across hot-reload (#3203).

Security

  • TLS certificate writes are now transactional with atomic temp-file rename, backup/restore for delete handlers, and cert file cleanup on settings save failure. Prevents partial writes from leaving corrupted certs on disk and orphaned deletions when settings save fails. Delete handlers are serialized under the settings mutex to eliminate the race between backup and remove (#3208).
  • SoxPath validation added before exec at all five Sox call sites in the spectrogram generator, rejecting empty, relative, and proxy-contaminated paths. Mirrors the existing FFmpeg path validation as defense-in-depth against command injection via settings (#3200).

Bug Fixes

Database & Schema Integrity

  • Silent legacy fallback masked missing v2 columns - When GORM AutoMigrate silently failed to add a column, every insert produced "table has no column named X" errors, and the legacy fallback hid the failure. The schema validator now runs a missing-column check after AutoMigrate that returns ErrV2SchemaCorrupted with table and column names; the v2 init path refuses to fall back to legacy mode on schema corruption errors. Closes #3211 (#3233).
  • Schema evolution blocked v2 init on populated tables - V2 entity structs went through several iterations (e.g., LabelCount in AIModel, Sensitivity/Threshold in Detection). GORM never drops the corresponding columns when fields are removed, and the overly strict schema validator treated these harmless leftovers as fatal corruption, triggering the silent legacy fallback and the resulting species_name NOT NULL crash. The validator now warns and continues for extra columns on populated tables while still rejecting missing columns. Telemetry and app events record the evolution event for support dumps (#3222).
  • MySQL had no schema evolution validation - Schema integrity validation was SQLite-only, leaving MySQL deployments to silently carry extra columns. A shared validateSchemaIntegrity() with a columnLister strategy now covers both. The MySQL manager gets a structured logger matching the SQLite manager (#3224).
  • MariaDB and mysql_native_password rejected at startup - MySQL startup-check functions built DSNs with mysql.Config{} literals where AllowNativePasswords defaulted to false. After migration, MariaDB users (and anyone using native password auth) hit auth failures, fell back to legacy mode, and saw "Restart Required" on every restart. A shared buildMySQLStartupDSN() helper now sets AllowNativePasswords: true consistently. Closes #3165 (#3207).
  • SQLite DSN built with wrong separator - Four call sites concatenated + "?mode=ro" to DSNs that might already contain query parameters. The backup package also produced an unparseable DSN when readOnly=false because the first pragma was appended with & instead of ?. New readOnlyDSN() helper centralizes the logic. isV2DatabaseSafeToDelete() and the MySQL manager get the same fix (#3224, #3227).
  • AppEvent repository not wired during fresh install - InitializeFreshInstall() didn't register the AppEvent repository, so fresh installs silently dropped all app events. Now matches the post-migration init path (#3227).

Audio & Streams

  • Spectrogram returned 500 for missing audio files - GenerateFromFile now stat-checks the file before calling FFmpeg/Sox and returns the underlying os.ErrNotExist, which the HTTP error mapper translates to 404 instead of 500. Skips the Sox->FFmpeg fallback when the file genuinely does not exist (#3197).
  • HLS cleanup failed on "directory not empty" race - os.RemoveAll could race with a concurrent HLS writer creating a file between the internal traversal and the final parent unlink. A single 100ms retry handles the race (#3197).
  • FFmpeg rejected RTSPS streams with Option sample_rate not found - -ar/-ac flags are now conditional on source type. Protocol-based streams (RTSP, HTTP, HLS, RTMP, UDP) negotiate audio parameters via SDP/headers; only local sources (audio card, file) need explicit sample rate and channel count (#3197).
  • Species count on settings test endpoint only counted BirdNET species - Multi-model setups now combine range-filtered primary species with the full label sets from additional models (Perch v2, etc.). Follows the existing locking pattern (#3200).
  • Watchdog restart leaked analysis buffer - AddSource now calls DeallocateSource before AllocateAnalysis so watchdog restarts that reuse the same source ID don't accumulate buffers (#3204).
  • Ignored species silently re-appeared after geomodel install - toggleSpeciesInIgnoredList and addSpeciesToIgnoredList mutated a stale settings pointer in place and called SaveSettings(), which reads from the live atomic pointer that already contained the pre-mutation snapshot. The result was a silent discard of every ignore action on both disk and in-memory state. Now follows the clone-mutate-publish pattern used by the main settings save path. GetExcludedSpecies also fixed to read from the live snapshot (#3196 by @ModerateWinGuy).
  • macOS .dylib search paths missing for ONNX Runtime - Added Homebrew paths (/opt/homebrew/lib for Apple Silicon, /usr/local/lib for Intel) and corrected the fallback string to libonnxruntime.dylib (#3200).

TLS & MQTT

  • TLS/MQTT settings save had no rollback or guard - SaveSettings() was called without the isGlobalOwner guard and without rollback on failure, leaving in-memory and on-disk state divergent if persistence failed. A new publishAndSaveSettings() helper consolidates the clone-mutate-publish-save-rollback pattern across 7 call sites in detections, MQTT TLS, and TLS handlers. Persistence failures now return HTTP 500 with the in-memory snapshot rolled back to the pre-save state (#3205).
  • TLS mode change didn't trigger restart prompt - TLSMode is now included in webserverSettingsChanged, so toggling TLS mode produces the "Restart required" toast (#3208).

Notifications, MQTT, and Storage

  • MarkAsRead and Delete returned 404 when notification was already cleaned up - These operations are now idempotent: returning nil when the notification was removed by the cleanup loop prevents Sentry noise from the TOCTOU race between cleanup and user interaction. ~44 events suppressed (#3204).
  • MQTT HA discovery published after disconnect - The OnConnect handler now guards on IsConnected() before publishing Home Assistant discovery, matching the existing guard in the manual trigger endpoint (#3204).
  • Diskmanager looped on empty export path - Statfs and filepath.Walk were called every cleanup tick on containers where the export path was never configured, producing ~340 events. The cleanup loop now skips entirely when the path is empty (#3204).

Frontend & Dashboard

  • ErrorPage TypeError on quick navigation - createFeather() lacked a null guard for the zero element when the component unmounted before the setTimeout callback fired. Initial feather timeout is also tracked and cleared on cleanup to prevent resource leaks (#3198).
  • Detection list and row had hardcoded English ARIA strings - ARIA live region announcements in DetectionsList and thumbnail loading announcements in DetectionRow are now translated. New detections.aria.loadingResults, detections.aria.thumbnailLoading, and detections.aria.thumbnailLoaded keys with translations for all 15 locales (#3198).
  • PlayOverlay got stuck on permanently unsupported audio formats - Retries now skip on MEDIA_ERR_SRC_NOT_SUPPORTED and isLoading is always reset in the catch block. audioElement.src = '' replaced with removeAttribute('src') + .load() to prevent spurious network requests during cleanup (#3199, #3200).
  • Spectrogram canvas could OOM on invalid dimensions - ResizeObserver now uses isFinite checks and the RAF loop enforces a MAX_CANVAS_DIM (8192) bounds check before createImageData (#3199).
  • AudioLevelIndicator was hardcoded English - 22 new media.audio.* i18n keys with translations for all 15 locales cover ARIA announcements, dropdown labels, status messages, media session metadata, and screen reader text (#3199).
  • Empty species name reached the API - BirdThumbnailPopup and DetectionDetail now .trim() scientific names before calling the API, preventing whitespace-only API calls (#3204).
  • New species star disappeared at midnight - The daily summary endpoint compared first-seen against exact dates instead of the configured NewSpeciesWindowDays window. The endpoint now uses the tracker's pre-computed window-based flags (IsNew, IsNewThisYear, IsNewThisSeason), so the star persists for the configured window. Per-detection and SSE endpoints are intentionally unchanged. Fixes #3218 (#3219).

Image Cache

  • NOT NULL constraint violation on image_caches.scientific_name - storeSuccessfulFetch and refreshEntry did not set ScientificName on the fetched BirdImage before saveToDB, triggering constraint violations for providers that returned partial metadata. Fix mirrors the existing pattern in storeNegativeCacheEntry (#3231).
  • Image cache hammered a corrupted SQLite file forever - When the cache file became malformed, every read, save, batch lookup, hourly refresh, and fallback chain kept retrying the dead database, generating thousands of Sentry events with no recovery. The cache now latches a dbCorrupted flag on first corruption error, logs the failure once, and short-circuits all subsequent DB operations until restart. Fresh fetches still served from providers (#3231).
  • Excessive warnings tripped the health diagnostics error signal - Failed to download image to file cache and common name not found in name maps are both expected fallback behavior, not errors. Lowered to info-level so they stop inflating the elevated-error-count signal (#3231).

Health Diagnostics

  • Overall health status showed Skipped even when actionable checks were healthy - WorstStatus() treated skipped/unknown checks as worse than healthy. It now prioritizes actionable results (healthy/warning/critical) and only returns unknown when everything is unknown, or skipped when everything is skipped (#3235).

Install & UX

  • ONNX Runtime errors had no install guide URL - Not-found, version-mismatch, and init-failure errors now include the wiki install guide URL via the {installGuideURL} i18n parameter. A new doc/ONNX-Runtime-Installation.md covers Linux, macOS, and Windows (#3202).

Internationalization

  • 22 new audio player i18n keys across all 15 locales for AudioLevelIndicator ARIA, dropdown labels, status messages, and media session metadata (#3199).
  • Detection list ARIA i18n - new keys for loading announcements and thumbnail lifecycle states with translations for all 15 locales (#3198).
  • ONNX Runtime install guide URL parameterized in error messages with localized "install guide" preambles for all 15 locales (#3202).
  • Model selection recommendation banners translated to 15 locales (#3228).
  • Stream Test button and probe results translated to 15 locales, with renaming from "Probe" to "Test" reflected in i18n keys (#3230, #3236, #3237).

🛡️ VirusTotal Results:

Don't miss a new birdnet-go release

NewReleases is sending notifications on new releases.