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.jsoncaptures 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.jsoncontains 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.jsoncaptures working directory, systemd service file (with environment variable scrubbing), data directory listing, and/proc/1/mountinfofor 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
AutoMigratesilently 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 afterAutoMigratethat returnsErrV2SchemaCorruptedwith 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.,
LabelCountinAIModel,Sensitivity/ThresholdinDetection). 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 resultingspecies_name NOT NULLcrash. 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 acolumnListerstrategy now covers both. The MySQL manager gets a structured logger matching the SQLite manager (#3224). - MariaDB and
mysql_native_passwordrejected at startup - MySQL startup-check functions built DSNs withmysql.Config{}literals whereAllowNativePasswordsdefaulted tofalse. 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 sharedbuildMySQLStartupDSN()helper now setsAllowNativePasswords: trueconsistently. 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 whenreadOnly=falsebecause the first pragma was appended with&instead of?. NewreadOnlyDSN()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 -
GenerateFromFilenow stat-checks the file before calling FFmpeg/Sox and returns the underlyingos.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.RemoveAllcould 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/-acflags 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 -
AddSourcenow callsDeallocateSourcebeforeAllocateAnalysisso watchdog restarts that reuse the same source ID don't accumulate buffers (#3204). - Ignored species silently re-appeared after geomodel install -
toggleSpeciesInIgnoredListandaddSpeciesToIgnoredListmutated a stale settings pointer in place and calledSaveSettings(), 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.GetExcludedSpeciesalso fixed to read from the live snapshot (#3196 by @ModerateWinGuy). - macOS .dylib search paths missing for ONNX Runtime - Added Homebrew paths (
/opt/homebrew/libfor Apple Silicon,/usr/local/libfor Intel) and corrected the fallback string tolibonnxruntime.dylib(#3200).
TLS & MQTT
- TLS/MQTT settings save had no rollback or guard -
SaveSettings()was called without theisGlobalOwnerguard and without rollback on failure, leaving in-memory and on-disk state divergent if persistence failed. A newpublishAndSaveSettings()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 -
TLSModeis now included inwebserverSettingsChanged, 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 -
Statfsandfilepath.Walkwere 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
DetectionsListand thumbnail loading announcements inDetectionRoware now translated. Newdetections.aria.loadingResults,detections.aria.thumbnailLoading, anddetections.aria.thumbnailLoadedkeys with translations for all 15 locales (#3198). - PlayOverlay got stuck on permanently unsupported audio formats - Retries now skip on
MEDIA_ERR_SRC_NOT_SUPPORTEDandisLoadingis always reset in the catch block.audioElement.src = ''replaced withremoveAttribute('src')+.load()to prevent spurious network requests during cleanup (#3199, #3200). - Spectrogram canvas could OOM on invalid dimensions -
ResizeObservernow usesisFinitechecks and the RAF loop enforces aMAX_CANVAS_DIM(8192) bounds check beforecreateImageData(#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 -
BirdThumbnailPopupandDetectionDetailnow.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
NewSpeciesWindowDayswindow. 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-storeSuccessfulFetchandrefreshEntrydid not setScientificNameon the fetchedBirdImagebeforesaveToDB, triggering constraint violations for providers that returned partial metadata. Fix mirrors the existing pattern instoreNegativeCacheEntry(#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
dbCorruptedflag 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 cacheandcommon name not found in name mapsare 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 newdoc/ONNX-Runtime-Installation.mdcovers 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: