Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions birdnet_analyzer/analyze/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ def analyze(
week (int, optional): Week of the year for seasonal filtering. Defaults to -1.
slist (str | None, optional): Path to a species list file for filtering.
Defaults to None.
sensitivity (float, optional): Sensitivity of the detection algorithm.
Defaults to 1.0.
sensitivity (float, optional): Sensitivity of the detection algorithm; only
applies to BirdNET 2.4 and custom classifiers, ignored (with a warning)
for BirdNET 3.0 and Perch. Defaults to 1.0.
overlap (float, optional): Overlap between analysis windows in seconds.
Defaults to 0.
fmin (int, optional): Minimum frequency for analysis in Hz. Defaults to 0.
Expand Down Expand Up @@ -122,8 +123,7 @@ def analyze(
)
from birdnet_analyzer.utils import save_params_file

# Settle the sensitivity here so the params file, result columns and the resume
# fingerprint all record the value the analysis actually used.
# Settled before the params file, result columns and resume fingerprint see it.
sensitivity = effective_sensitivity(sensitivity, model, birdnet, classifier)

species_list_file = slist if isinstance(slist, (str, Path)) else ""
Expand Down
65 changes: 33 additions & 32 deletions birdnet_analyzer/gui/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,35 +130,36 @@ def run_analysis(
if progress is not None:
progress(0, desc=f"{loc.localize('progress-starting')} ...")

return analyze(
audio_input=input_dir or input_path, # type: ignore
# TODO: workaround while lib is not ignoring confidence with top_n
min_conf=confidence if not use_top_n else 0,
sensitivity=sensitivity,
locale=locale,
overlap=overlap,
audio_speed=audio_speed,
fmin=fmin,
fmax=fmax,
batch_size=batch_size,
rtype=output_types,
sf_thresh=sf_thresh,
lat=lat,
lon=lon,
week=week,
slist=slist,
top_n=top_n if use_top_n else None,
output=output_path,
merge_consecutive=merge_consecutive,
additional_columns=additional_columns,
model="perch" if use_perch else "birdnet",
birdnet=birdnet_version,
classifier=custom_classifier,
cc_species_list=None, # always default search path in GUI currently
on_update=on_update,
save_params=save_params,
n_producers=n_producers,
n_workers=n_workers,
split_tables=split_tables,
_return_only=bool(input_path), # only for single file tab
)
with gu.download_progress(progress if callable(progress) else None):
return analyze(
audio_input=input_dir or input_path, # type: ignore
# TODO: workaround while lib is not ignoring confidence with top_n
min_conf=confidence if not use_top_n else 0,
sensitivity=sensitivity,
locale=locale,
overlap=overlap,
audio_speed=audio_speed,
fmin=fmin,
fmax=fmax,
batch_size=batch_size,
rtype=output_types,
sf_thresh=sf_thresh,
lat=lat,
lon=lon,
week=week,
slist=slist,
top_n=top_n if use_top_n else None,
output=output_path,
merge_consecutive=merge_consecutive,
additional_columns=additional_columns,
model="perch" if use_perch else "birdnet",
birdnet=birdnet_version,
classifier=custom_classifier,
cc_species_list=None, # always default search path in GUI currently
on_update=on_update,
save_params=save_params,
n_producers=n_producers,
n_workers=n_workers,
split_tables=split_tables,
_return_only=bool(input_path), # only for single file tab
)
17 changes: 9 additions & 8 deletions birdnet_analyzer/gui/species.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ def run_species_list(

gu.validate(out_path, loc.localize("validation-no-directory-selected"))

species(
output=os.path.join(out_path, filename or "species_list.txt"),
lat=lat,
lon=lon,
week=None if use_yearlong else week,
sf_thresh=sf_thresh,
locale=locale,
)
with gu.download_progress():
species(
output=os.path.join(out_path, filename or "species_list.txt"),
lat=lat,
lon=lon,
week=None if use_yearlong else week,
sf_thresh=sf_thresh,
locale=locale,
)

gr.Info(f"{loc.localize('species-tab-finish-info')} {out_path}")

Expand Down
106 changes: 100 additions & 6 deletions birdnet_analyzer/gui/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
import os
import platform
import sys
import threading
import warnings
from collections.abc import Callable
from contextlib import suppress
from contextlib import contextmanager, suppress

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the diagnosis, but this is pre-existing and out of scope here: gui.utils has imported webview at module level since long before this PR, and every test that reaches it stubs sys.modules['webview'] first (documented in AGENTS.md; both new test modules in this PR do so). Making gui.utils importable without pywebview is a real improvement (open_window, _WINDOW typing, the folder/file dialogs) but a separate refactor — filing it as a follow-up rather than widening this PR.

from html import escape
from typing import Literal, cast, get_args

Expand Down Expand Up @@ -199,6 +200,85 @@ def wrapper(*args, **kwargs):
return wrapper


def _format_bytes(n: int) -> str:
return f"{n / 1e6:.0f} MB" if n < 1e9 else f"{n / 1e9:.1f} GB"


# Download sinks by the thread that runs birdnet.load; the library's callback fires
# synchronously on that thread. One dispatcher is registered with the library while
# any sink is active, so overlapping GUI events neither misroute nor clobber each
# other's registration (the library's own scoped registration is a plain set/restore).
_DOWNLOAD_SINKS: dict[int, "gr.Progress | None"] = {}
_DOWNLOAD_SINKS_LOCK = threading.Lock()


def _show_download_update(update, progress: "gr.Progress | None") -> None:
name = update.description.removeprefix("Downloading ")
if update.attempt > 1:
name = f"{name} ({update.attempt}/{update.max_attempts})"

label = f"{loc.localize('progress-downloading-model')}: {name}"

if update.status == "started" and progress is None:
gr.Info(label)
elif update.status in ("progress", "finished") and progress is not None:
# "progress" is throttled, so only "finished" reliably shows the bar full.
if update.status == "finished":
progress(1.0, desc=f"{label} ({_format_bytes(update.bytes_done)})")
elif update.bytes_total:
done = _format_bytes(update.bytes_done)
total = _format_bytes(update.bytes_total)
progress(
min(update.bytes_done / update.bytes_total, 1.0),
desc=f"{label} ({done} / {total})",
)
else:
progress(0.0, desc=f"{label} ({_format_bytes(update.bytes_done)})")
elif update.status == "retrying":
gr.Warning(
f"{loc.localize('progress-download-retrying')}: {name} - {update.error}"
)
# "failed": the library raises right after; the operation reports it.


def _dispatch_download_update(update) -> None:
with _DOWNLOAD_SINKS_LOCK:
if threading.get_ident() not in _DOWNLOAD_SINKS:
return
progress = _DOWNLOAD_SINKS[threading.get_ident()]

# An exception escaping the callback aborts the download in the library.
try:
_show_download_update(update, progress)
except Exception:
logging.getLogger(__name__).exception("Download progress UI update failed")


@contextmanager
def download_progress(progress: "gr.Progress | None" = None):
"""Shows the birdnet library's model downloads while the ``with`` block runs.

Models are fetched on first use inside ``birdnet.load``; the library's own tqdm
bar goes to stderr, which the frozen GUI diverts to the log file. This routes the
updates to ``progress`` when a bar is available and to toasts otherwise, so a
first-run download of several hundred MB never looks like a hang.
"""
import birdnet

thread = threading.get_ident()
with _DOWNLOAD_SINKS_LOCK:
if not _DOWNLOAD_SINKS:
birdnet.set_download_progress_callback(_dispatch_download_update)
_DOWNLOAD_SINKS[thread] = progress
try:
yield
finally:
with _DOWNLOAD_SINKS_LOCK:
_DOWNLOAD_SINKS.pop(thread, None)
if not _DOWNLOAD_SINKS:
birdnet.set_download_progress_callback(None)


def select_folder(state_key=None):
"""Opens a folder selection dialog and returns the selected folder path.

Expand Down Expand Up @@ -633,9 +713,7 @@ def on_species_list_change(value, species_choice):
else [_CUSTOM_SPECIES, _PREDICT_SPECIES, _ALL_SPECIES]
)

# A disabled slider shows 1.0 so it never displays a value the analysis will
# not use; re-enabling brings back the value the user last set (persisted
# on release, so read live rather than from the state snapshot).
# Slider release persists the value; read it live, the state snapshot is stale.
if model_supports_sensitivity(value):
persisted = settings.get_tab_settings(state.tab).get("sensitivity_slider")
restored = (
Expand Down Expand Up @@ -672,6 +750,24 @@ def on_species_list_change(value, species_choice):
show_progress="hidden",
)

def keep_disabled_slider_at_default(sensitivity, model_choice):
# A preset or params file can set the slider while the model stays 3.0/Perch,
# which fires no model change to reset it.
if model_supports_sensitivity(model_choice) or sensitivity == 1.0:
return gr.update()

return gr.update(value=1.0)

sample_settings["sensitivity_slider"].change(
keep_disabled_slider_at_default,
inputs=[
sample_settings["sensitivity_slider"],
model_settings["model_selection_radio"],
],
outputs=sample_settings["sensitivity_slider"],
show_progress="hidden",
)

def warn_unmatched_species(file, model_choice, locale):
"""Heads-up when a chosen custom list has species the selected model lacks.

Expand Down Expand Up @@ -790,8 +886,6 @@ def sample_sliders(
label=loc.localize("inference-settings-sensitivity-slider-label"),
info=loc.localize("inference-settings-sensitivity-slider-info"),
)
# Show what the analysis will use: a value persisted from a 2.4 run
# would otherwise sit visibly on the disabled slider.
if not sensitivity_enabled:
sensitivity_slider.value = 1.0
overlap_slider = state.persist(
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "Analysiere",
"progress-autotune": "Autotune läuft",
"progress-build-classifier": "Daten laden & Klassifikator erstellen",
"progress-download-retrying": "Modell-Download unterbrochen, neuer Versuch",
"progress-downloading-model": "Modell wird heruntergeladen",
"progress-extracting-segments": "Segmente extrahieren",
"progress-loading-data": "Daten für",
"progress-saving": "Gespeichert unter",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "Analyzing",
"progress-autotune": "Autotune in progress",
"progress-build-classifier": "Loading data & building classifier",
"progress-download-retrying": "Model download interrupted, retrying",
"progress-downloading-model": "Downloading model",
"progress-extracting-segments": "Extracting segments",
"progress-loading-data": "Loading data for",
"progress-saving": "Saving at",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "Analysoidaan",
"progress-autotune": "Autoviritys käynnissä",
"progress-build-classifier": "Ladataan dataa & rakennetaan luokittelijaa",
"progress-download-retrying": "Mallin lataus keskeytyi, yritetään uudelleen",
"progress-downloading-model": "Ladataan mallia",
"progress-extracting-segments": "Segmenttejä puretaan",
"progress-loading-data": "Ladataan dataa kohteelle",
"progress-saving": "Tallennetaan kohteeseen",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "Analyse en cours",
"progress-autotune": "Autotune en progression",
"progress-build-classifier": "Chargement des données et construction d'un classificateur",
"progress-download-retrying": "Téléchargement du modèle interrompu, nouvelle tentative",
"progress-downloading-model": "Téléchargement du modèle",
"progress-extracting-segments": "Extraction des segments",
"progress-loading-data": "Chargement des données pour",
"progress-saving": "Enregistrement à",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "Menganalisis",
"progress-autotune": "Autotune dalam progres",
"progress-build-classifier": "Memuat data & membangun klasifikator",
"progress-download-retrying": "Pengunduhan model terputus, mencoba lagi",
"progress-downloading-model": "Mengunduh model",
"progress-extracting-segments": "Mengekstrak segmen",
"progress-loading-data": "Memuat data untuk",
"progress-saving": "Menyimpan di",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "Analisando",
"progress-autotune": "Autotune em progresss",
"progress-build-classifier": "Carregando dados e construindo classificador",
"progress-download-retrying": "Download do modelo interrompido, tentando novamente",
"progress-downloading-model": "Baixando modelo",
"progress-extracting-segments": "Extraindo segmentos",
"progress-loading-data": "Carregando os dados para",
"progress-saving": "slvando em",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "Анализ",
"progress-autotune": "Выполняется автонастройка",
"progress-build-classifier": "Загрузка данных и создание классификатора",
"progress-download-retrying": "Загрузка модели прервана, повторная попытка",
"progress-downloading-model": "Загрузка модели",
"progress-extracting-segments": "Извлечение сегментов",
"progress-loading-data": "Загрузка данных для",
"progress-saving": "Сохранение в",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/se.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "Analyserar",
"progress-autotune": "Autojustering pågår",
"progress-build-classifier": "Laddar data och bygger klassificerare",
"progress-download-retrying": "Nedladdningen av modellen avbröts, försöker igen",
"progress-downloading-model": "Laddar ner modell",
"progress-extracting-segments": "Extraherar segment",
"progress-loading-data": "Laddar data för",
"progress-saving": "Sparar i",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/tlh.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "poj",
"progress-autotune": "autotune Qap",
"progress-build-classifier": "De' poQ & tu'law' rurbogh",
"progress-download-retrying": "model Download mev, nIDqa'",
"progress-downloading-model": "model'e' Downloadlu'",
"progress-extracting-segments": "'ay'mey lInglu'",
"progress-loading-data": "De' poQ",
"progress-saving": "pol",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "分析中",
"progress-autotune": "自动调优进行中",
"progress-build-classifier": "正在加载数据并构建分类器",
"progress-download-retrying": "模型下载中断,正在重试",
"progress-downloading-model": "正在下载模型",
"progress-extracting-segments": "正在提取片段",
"progress-loading-data": "正在为以下内容加载数据",
"progress-saving": "正在保存至",
Expand Down
2 changes: 2 additions & 0 deletions birdnet_analyzer/lang/zh_TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"progress-analyzing": "分析中",
"progress-autotune": "自動調諧進行中",
"progress-build-classifier": "載入資料中、建立分類器",
"progress-download-retrying": "模型下載中斷,正在重試",
"progress-downloading-model": "正在下載模型",
"progress-extracting-segments": "正在擷取片段",
"progress-loading-data": "載入資料中",
"progress-saving": "儲存中",
Expand Down
Loading
Loading