diff --git a/birdnet_analyzer/analyze/core.py b/birdnet_analyzer/analyze/core.py index d591064df..908635be6 100644 --- a/birdnet_analyzer/analyze/core.py +++ b/birdnet_analyzer/analyze/core.py @@ -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. @@ -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 "" diff --git a/birdnet_analyzer/gui/analysis.py b/birdnet_analyzer/gui/analysis.py index 2183993c7..5728a9852 100644 --- a/birdnet_analyzer/gui/analysis.py +++ b/birdnet_analyzer/gui/analysis.py @@ -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 + ) diff --git a/birdnet_analyzer/gui/species.py b/birdnet_analyzer/gui/species.py index 02c308bbe..94169cf7b 100644 --- a/birdnet_analyzer/gui/species.py +++ b/birdnet_analyzer/gui/species.py @@ -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}") diff --git a/birdnet_analyzer/gui/utils.py b/birdnet_analyzer/gui/utils.py index 6d2472807..2836aac49 100644 --- a/birdnet_analyzer/gui/utils.py +++ b/birdnet_analyzer/gui/utils.py @@ -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 from html import escape from typing import Literal, cast, get_args @@ -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. @@ -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 = ( @@ -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. @@ -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( diff --git a/birdnet_analyzer/lang/de.json b/birdnet_analyzer/lang/de.json index 3a40b29e9..066c33adb 100644 --- a/birdnet_analyzer/lang/de.json +++ b/birdnet_analyzer/lang/de.json @@ -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", diff --git a/birdnet_analyzer/lang/en.json b/birdnet_analyzer/lang/en.json index 20f653277..a29483125 100644 --- a/birdnet_analyzer/lang/en.json +++ b/birdnet_analyzer/lang/en.json @@ -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", diff --git a/birdnet_analyzer/lang/fi.json b/birdnet_analyzer/lang/fi.json index 57c89206e..32e206301 100644 --- a/birdnet_analyzer/lang/fi.json +++ b/birdnet_analyzer/lang/fi.json @@ -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", diff --git a/birdnet_analyzer/lang/fr.json b/birdnet_analyzer/lang/fr.json index 52d4f02c0..9ad145c6e 100644 --- a/birdnet_analyzer/lang/fr.json +++ b/birdnet_analyzer/lang/fr.json @@ -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 à", diff --git a/birdnet_analyzer/lang/id.json b/birdnet_analyzer/lang/id.json index a56b1988c..3419eb2bf 100644 --- a/birdnet_analyzer/lang/id.json +++ b/birdnet_analyzer/lang/id.json @@ -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", diff --git a/birdnet_analyzer/lang/pt-br.json b/birdnet_analyzer/lang/pt-br.json index 925291491..2a75cf37a 100644 --- a/birdnet_analyzer/lang/pt-br.json +++ b/birdnet_analyzer/lang/pt-br.json @@ -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", diff --git a/birdnet_analyzer/lang/ru.json b/birdnet_analyzer/lang/ru.json index 8b5ca3c93..0800e83e3 100644 --- a/birdnet_analyzer/lang/ru.json +++ b/birdnet_analyzer/lang/ru.json @@ -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": "Сохранение в", diff --git a/birdnet_analyzer/lang/se.json b/birdnet_analyzer/lang/se.json index 0ebfc3864..6d2360270 100644 --- a/birdnet_analyzer/lang/se.json +++ b/birdnet_analyzer/lang/se.json @@ -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", diff --git a/birdnet_analyzer/lang/tlh.json b/birdnet_analyzer/lang/tlh.json index 1ef7dd347..8f765a8b3 100644 --- a/birdnet_analyzer/lang/tlh.json +++ b/birdnet_analyzer/lang/tlh.json @@ -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", diff --git a/birdnet_analyzer/lang/zh_CN.json b/birdnet_analyzer/lang/zh_CN.json index a6b75e03e..8c472a7fb 100644 --- a/birdnet_analyzer/lang/zh_CN.json +++ b/birdnet_analyzer/lang/zh_CN.json @@ -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": "正在保存至", diff --git a/birdnet_analyzer/lang/zh_TW.json b/birdnet_analyzer/lang/zh_TW.json index bbf6d0c52..32a308a68 100644 --- a/birdnet_analyzer/lang/zh_TW.json +++ b/birdnet_analyzer/lang/zh_TW.json @@ -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": "儲存中", diff --git a/tests/gui/test_download_progress.py b/tests/gui/test_download_progress.py new file mode 100644 index 000000000..a13c257d0 --- /dev/null +++ b/tests/gui/test_download_progress.py @@ -0,0 +1,147 @@ +"""The GUI shows the birdnet library's model downloads.""" + +import sys +from unittest.mock import MagicMock + +import pytest + +gr = pytest.importorskip("gradio") + +sys.modules.setdefault("webview", MagicMock(settings={})) + + +def test_download_progress_routes_library_updates_to_gradio(monkeypatch): + import birdnet + import gradio as gr + + from birdnet_analyzer.gui import utils as gu + + calls = [] + infos = [] + warnings = [] + monkeypatch.setattr(gr, "Info", lambda msg, **kw: infos.append(msg)) + monkeypatch.setattr(gr, "Warning", lambda msg, **kw: warnings.append(msg)) + + def fake_progress(value, desc=None, **kwargs): + calls.append((value, desc)) + + def registered(): + return birdnet.get_download_progress_callback() + + def report(**kwargs): + registered()( + birdnet.DownloadProgress( + description="Downloading acoustic model v3.0", + url="https://example.invalid/model", + attempt=1, + max_attempts=3, + **kwargs, + ) + ) + + with gu.download_progress(fake_progress): + report(status="started", bytes_done=0, bytes_total=1000) + report(status="progress", bytes_done=250, bytes_total=1000) + report(status="progress", bytes_done=5_000_000, bytes_total=None) + report(status="finished", bytes_done=1000, bytes_total=1000) + + assert calls[0][0] == 0.25 + assert "acoustic model v3.0" in calls[0][1] + assert "Downloading Downloading" not in calls[0][1] + assert calls[1][0] == 0.0 + assert "5 MB" in calls[1][1] + assert calls[2][0] == 1.0, "finished shows the bar full" + assert not infos, "no toasts while a progress bar is available" + + with gu.download_progress(None): + report(status="started", bytes_done=0, bytes_total=1000) + report(status="progress", bytes_done=250, bytes_total=1000) + + assert len(infos) == 1 + assert "acoustic model v3.0" in infos[0] + + assert registered() is None + + +def test_download_progress_survives_ui_failures(monkeypatch): + import birdnet + import gradio as gr + + from birdnet_analyzer.gui import utils as gu + + warnings = [] + monkeypatch.setattr(gr, "Warning", lambda msg, **kw: warnings.append(msg)) + + def broken_progress(*args, **kwargs): + raise RuntimeError("no request context") + + def registered(): + return birdnet.get_download_progress_callback() + + def update(status, **extra): + return birdnet.DownloadProgress( + description="Downloading acoustic model v3.0", + url="https://example.invalid/model", + bytes_done=0, + bytes_total=1000, + attempt=2, + max_attempts=3, + status=status, + **extra, + ) + + with gu.download_progress(broken_progress): + registered()(update("progress")) # must not raise + registered()(update("retrying", error="connection reset")) + + assert len(warnings) == 1 + assert "connection reset" in warnings[0] + assert "(2/3)" in warnings[0] + + +def test_overlapping_gui_events_keep_their_own_sink_and_unregister_cleanly(): + import threading + + import birdnet + + from birdnet_analyzer.gui import utils as gu + + def update(desc): + return birdnet.DownloadProgress( + description=desc, + url="u", + bytes_done=1, + bytes_total=2, + attempt=1, + max_attempts=1, + status="progress", + ) + + seen_a, seen_b = [], [] + a_entered, b_entered, a_left = (threading.Event() for _ in range(3)) + + def event_a(): + with gu.download_progress(lambda v, desc=None, **k: seen_a.append(desc)): + a_entered.set() + b_entered.wait(5) + birdnet.get_download_progress_callback()(update("Downloading alpha-model")) + a_left.set() + + def event_b(): + a_entered.wait(5) + with gu.download_progress(lambda v, desc=None, **k: seen_b.append(desc)): + b_entered.set() + a_left.wait(5) + birdnet.get_download_progress_callback()(update("Downloading beta-model")) + + ta, tb = threading.Thread(target=event_a), threading.Thread(target=event_b) + ta.start() + tb.start() + ta.join(10) + tb.join(10) + + assert any("alpha-model" in d for d in seen_a) + assert not any("beta-model" in d for d in seen_a) + assert any("beta-model" in d for d in seen_b) + assert not any("alpha-model" in d for d in seen_b) + assert birdnet.get_download_progress_callback() is None diff --git a/tests/gui/test_sensitivity_slider.py b/tests/gui/test_sensitivity_slider.py index 85c29371b..e7cf2b747 100644 --- a/tests/gui/test_sensitivity_slider.py +++ b/tests/gui/test_sensitivity_slider.py @@ -12,7 +12,6 @@ gr = pytest.importorskip("gradio") -# gui.utils imports pywebview at module level, which the gui-tests extra lacks. sys.modules.setdefault("webview", MagicMock(settings={})) from birdnet_analyzer import settings # noqa: E402 @@ -22,8 +21,6 @@ @pytest.fixture(autouse=True) def no_map_figure(monkeypatch): - # The species-list block draws a plotly map while building; plotly is a gui-only - # extra and the map is irrelevant here. monkeypatch.setattr(gu, "plot_map_scatter_mapbox", lambda *a, **k: None) @@ -41,8 +38,6 @@ def build(): sample, _, model = gu.sample_species_model_settings(gs.TabState("multi")) radio = model["model_selection_radio"] slider = sample["sensitivity_slider"] - # The radio has several change handlers; the one that drives the slider is the one - # listing it among its outputs. handler = next( event.fn for event in demo.fns.values() @@ -54,7 +49,6 @@ def build(): def test_slider_disabled_for_3_0_and_restored_for_2_4(appdir): - # The user set 1.25 while on 2.4 (persisted on slider release). settings.set_tab_setting("multi", "sensitivity_slider", 1.25) settings.set_tab_setting("multi", "model_selection_radio", gu._USE_BIRDNET_2_4) @@ -76,10 +70,29 @@ def test_slider_disabled_for_3_0_and_restored_for_2_4(appdir): def test_slider_built_disabled_at_1_0_when_3_0_is_persisted(appdir): - # A 1.25 persisted from a 2.4 session must not sit visibly on the disabled slider. settings.set_tab_setting("multi", "sensitivity_slider", 1.25) settings.set_tab_setting("multi", "model_selection_radio", gu._USE_BIRDNET_3_0) slider, _ = build() assert not slider.interactive assert slider.value == 1.0 + + +def test_programmatic_value_on_disabled_slider_snaps_back_to_1_0(appdir): + settings.set_tab_setting("multi", "model_selection_radio", gu._USE_BIRDNET_3_0) + + with gr.Blocks() as demo: + sample, _, _ = gu.sample_species_model_settings(gs.TabState("multi")) + slider = sample["sensitivity_slider"] + on_slider_change = next( + event.fn + for event in demo.fns.values() + if event.targets + and event.targets[0] == (slider._id, "change") + and slider in event.outputs + ) + + assert on_slider_change(1.3, gu._USE_BIRDNET_3_0) == gr.update(value=1.0) + assert on_slider_change(1.0, gu._USE_BIRDNET_3_0) == gr.update() + assert on_slider_change(1.3, gu._USE_BIRDNET_2_4) == gr.update() + assert on_slider_change(1.3, gu._USE_PERCH) == gr.update(value=1.0) diff --git a/tests/test_model_utils.py b/tests/test_model_utils.py index 9e773f1ef..9dd98fd24 100644 --- a/tests/test_model_utils.py +++ b/tests/test_model_utils.py @@ -65,10 +65,6 @@ def test_language_for_version_keeps_supported_and_falls_back_otherwise(): def test_supports_sensitivity_only_for_2_4_based_models(): - # Sensitivity scales the sigmoid the analyzer applies to logits: BirdNET 2.4 and - # custom classifiers (2.4 base). BirdNET 3.0 applies its sigmoid inside the model - # (the library raises for a sensitivity other than 1.0); Perch is run on raw logits - # without a sigmoid. Unknown future versions are treated like 3.0. assert model_utils.supports_sensitivity("birdnet", "2.4") assert not model_utils.supports_sensitivity("birdnet", "3.1") assert model_utils.supports_sensitivity("birdnet", "3.0", classifier="cc.tflite") @@ -77,9 +73,6 @@ def test_supports_sensitivity_only_for_2_4_based_models(): def test_run_inference_drops_sensitivity_for_3_0(monkeypatch, tmp_path): - # A non-default sensitivity is coerced to 1.0 before it reaches the library, which - # would otherwise reject it for 3.0 and crash the analysis (GUI state can carry the - # slider value over from a 2.4 run). from contextlib import contextmanager from unittest.mock import MagicMock