From 4786e096e1ccf65a8ac793e620d34e1df22d67c7 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Fri, 31 Jul 2026 15:54:16 +0200 Subject: [PATCH 1/3] Sync CLI - GUI params --- birdnet_analyzer/evaluation/__init__.py | 51 +++++++++++++++++++++---- birdnet_analyzer/gui/multi_file.py | 5 ++- birdnet_analyzer/lang/de.json | 3 +- birdnet_analyzer/lang/en.json | 3 +- birdnet_analyzer/lang/fi.json | 3 +- birdnet_analyzer/lang/fr.json | 3 +- birdnet_analyzer/lang/id.json | 3 +- birdnet_analyzer/lang/pt-br.json | 3 +- birdnet_analyzer/lang/ru.json | 3 +- birdnet_analyzer/lang/se.json | 3 +- birdnet_analyzer/lang/tlh.json | 3 +- birdnet_analyzer/lang/zh_TW.json | 3 +- 12 files changed, 66 insertions(+), 20 deletions(-) diff --git a/birdnet_analyzer/evaluation/__init__.py b/birdnet_analyzer/evaluation/__init__.py index 558296a99..9f34f6d77 100644 --- a/birdnet_analyzer/evaluation/__init__.py +++ b/birdnet_analyzer/evaluation/__init__.py @@ -33,7 +33,8 @@ class EvaluationResult(NamedTuple): Besides the metrics and the tensors the assessment ran on, it carries the context a caller needs to explain the numbers: which recordings had predictions but no annotations, and which classes were empty (and therefore excluded from any - aggregate score). + aggregate score). ``sample_data`` is the per-window matrix the metrics were derived + from -- the same table the GUI offers as the "data table" download. """ metrics_df: "pd.DataFrame" @@ -43,6 +44,7 @@ class EvaluationResult(NamedTuple): classes: tuple[str, ...] unmatched_recordings: tuple[str, ...] empty_classes: tuple[str, ...] + sample_data: "pd.DataFrame" def process_data( @@ -56,7 +58,7 @@ def process_data( columns_predictions: dict[str, str] | None = None, selected_classes: Sequence[str] | None = None, selected_recordings: list[str] | None = None, - metrics_list: tuple[str, ...] = ("accuracy", "precision", "recall"), + metrics_list: tuple[str, ...] = ("auroc", "precision", "recall", "f1", "ap"), threshold: float = 0.1, class_wise: bool = False, averaging: Literal["macro", "micro", "weighted"] = "macro", @@ -176,9 +178,28 @@ def process_data( classes=classes, unmatched_recordings=tuple(sorted(processor.unmatched_prediction_files)), empty_classes=pa.empty_classes(labels), + sample_data=processor.get_sample_data(), ) +# The metrics the PerformanceAssessor understands; also the CLI's allowed choices. +VALID_METRICS = ("accuracy", "recall", "precision", "f1", "ap", "auroc") + + +def _threshold_arg(value: str) -> float: + """argparse type for ``--threshold``: a float strictly between 0 and 1. + + Fails at the parser with a clear message instead of deep inside the + PerformanceAssessor, which requires ``0 < threshold < 1``. + """ + number = float(value) + if not 0 < number < 1: + raise argparse.ArgumentTypeError( + f"threshold must be between 0 and 1 (exclusive), got {value}" + ) + return number + + def main(): """ Entry point for the script. Parses command-line arguments and orchestrates the @@ -232,11 +253,16 @@ def main(): parser.add_argument( "--metrics", nargs="+", - default=["accuracy", "precision", "recall"], - help="List of metrics", + choices=VALID_METRICS, + default=["auroc", "precision", "recall", "f1", "ap"], + help="List of metrics (accuracy is excluded by default; in this sample-based " + "multilabel setting it is dominated by true negatives and misleadingly high)", ) parser.add_argument( - "--threshold", type=float, default=0.1, help="Threshold value (0-1)" + "--threshold", + type=_threshold_arg, + default=0.1, + help="Threshold value, strictly between 0 and 1", ) parser.add_argument( "--class_wise", action="store_true", help="Calculate class-wise metrics" @@ -301,9 +327,18 @@ def main(): ", ".join(result.empty_classes), ) - # Create output directory if needed - if args.output_dir and not os.path.exists(args.output_dir): - os.makedirs(args.output_dir) + # Write the result tables (and later the plots) when an output directory is given. + # These mirror the GUI's "results table" and "data table" downloads. + if args.output_dir: + os.makedirs(args.output_dir, exist_ok=True) + + results_table_path = os.path.join(args.output_dir, "results_table.csv") + result.metrics_df.to_csv(results_table_path, index=True) + logger.info("Saved results table to %s", results_table_path) + + data_table_path = os.path.join(args.output_dir, "data_table.csv") + result.sample_data.to_csv(data_table_path, index=False) + logger.info("Saved data table to %s", data_table_path) # Generate plots if specified if args.plot_metrics: diff --git a/birdnet_analyzer/gui/multi_file.py b/birdnet_analyzer/gui/multi_file.py index ef1a9b2e8..35bb136e3 100644 --- a/birdnet_analyzer/gui/multi_file.py +++ b/birdnet_analyzer/gui/multi_file.py @@ -12,6 +12,7 @@ def _output_type_map(): loc.localize("multi-tab-output-type-raven-label"): "table", loc.localize("multi-tab-output-type-audacity-label"): "audacity", loc.localize("multi-tab-output-type-csv-label"): "csv", + loc.localize("multi-tab-output-type-parquet-label"): "parquet", loc.localize("multi-tab-output-type-kaleidoscope-label"): "kaleidoscope", } @@ -249,7 +250,7 @@ def select_directory_wrapper(): gr.CheckboxGroup, choices=list(_additional_columns_map().items()), value=[], - visible="csv" in output_type_radio.value, + visible=bool({"csv", "parquet"} & set(output_type_radio.value)), label=loc.localize("multi-tab-additional-columns-checkbox-label"), info=loc.localize("multi-tab-additional-columns-checkbox-info"), ) @@ -297,7 +298,7 @@ def select_directory_wrapper(): ] def show_additional_columns(values): - return gr.update(visible="csv" in values) + return gr.update(visible=bool({"csv", "parquet"} & set(values))) start_batch_analysis_btn.click( run_batch_analysis, inputs=inputs, outputs=result_grid diff --git a/birdnet_analyzer/lang/de.json b/birdnet_analyzer/lang/de.json index 3a728e5c1..6c55f7d62 100644 --- a/birdnet_analyzer/lang/de.json +++ b/birdnet_analyzer/lang/de.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "Kein Eingabeverzeichnis ausgewählt", "embeddings-tab-start-button-label": "Embeddings extrahieren", "embeddings-tab-title": "Embeddings", - "eval-tab-accuracy-checkbox-info": "Accuracy misst den Prozentsatz der richtigen Vorhersagen des Modells.", + "eval-tab-accuracy-checkbox-info": "Accuracy ist der Anteil korrekter Vorhersagen. Da die meisten Fenster keinen Ruf enthalten, dominieren echte Negative und die Accuracy bleibt irreführend hoch – daher ist sie standardmäßig deaktiviert.", "eval-tab-annotation-col-accordion-label": "Annotationsspalten", "eval-tab-annotation-selection-button-label": "Verzeichnis mit Annotationen auswählen", "eval-tab-annotation-selection-textbox-placeholder": "Kein Annotationsverzeichnis ausgewählt", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Raven-Auswahltabelle", "multi-tab-result-dataframe-column-invalid-file-header": "Ungültige Audiodateien", "multi-tab-result-dataframe-column-success-header": "Alle Dateien wurden analysiert!", diff --git a/birdnet_analyzer/lang/en.json b/birdnet_analyzer/lang/en.json index 9d150d614..91fab7222 100644 --- a/birdnet_analyzer/lang/en.json +++ b/birdnet_analyzer/lang/en.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "No input directory selected", "embeddings-tab-start-button-label": "Extract embeddings", "embeddings-tab-title": "Embeddings", - "eval-tab-accuracy-checkbox-info": "Accuracy measures the percentage of correct predictions made by the model.", + "eval-tab-accuracy-checkbox-info": "Accuracy is the share of correct predictions. Since most sample windows contain no call, true negatives dominate and accuracy stays misleadingly high — so it is off by default.", "eval-tab-annotation-col-accordion-label": "Annotation columns", "eval-tab-annotation-selection-button-label": "Select annotation directory", "eval-tab-annotation-selection-textbox-placeholder": "No annotation directory selected", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Raven selection table", "multi-tab-result-dataframe-column-invalid-file-header": "Invalid audio files", "multi-tab-result-dataframe-column-success-header": "All files analyzed!", diff --git a/birdnet_analyzer/lang/fi.json b/birdnet_analyzer/lang/fi.json index 7263e41c9..c00a79a8e 100644 --- a/birdnet_analyzer/lang/fi.json +++ b/birdnet_analyzer/lang/fi.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "Syötehakemistoa ei valittu", "embeddings-tab-start-button-label": "Poimi upotukset", "embeddings-tab-title": "Upotukset", - "eval-tab-accuracy-checkbox-info": "Tarkkuus mittaa mallin oikeiden ennusteiden prosenttiosuutta.", + "eval-tab-accuracy-checkbox-info": "Accuracy on oikeiden ennusteiden osuus. Koska useimmissa ikkunoissa ei ole ääntelyä, todelliset negatiiviset hallitsevat ja accuracy pysyy harhaanjohtavan korkeana — siksi se on oletuksena pois päältä.", "eval-tab-annotation-col-accordion-label": "Merkintäsarakkeet", "eval-tab-annotation-selection-button-label": "Valitse merkintäkansio", "eval-tab-annotation-selection-textbox-placeholder": "Annotaatiohakemistoa ei valittu", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Raven-valintataulukko", "multi-tab-result-dataframe-column-invalid-file-header": "Virheelliset äänitiedostot", "multi-tab-result-dataframe-column-success-header": "Kaikki tiedostot analysoitu!", diff --git a/birdnet_analyzer/lang/fr.json b/birdnet_analyzer/lang/fr.json index 90565fe2e..915fb8de5 100644 --- a/birdnet_analyzer/lang/fr.json +++ b/birdnet_analyzer/lang/fr.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "Aucun répertoire d'entrée sélectionné", "embeddings-tab-start-button-label": "Extraire les incorporations", "embeddings-tab-title": "Incorporations", - "eval-tab-accuracy-checkbox-info": "La précision mesure le pourcentage de prédictions correctes faites par le modèle.", + "eval-tab-accuracy-checkbox-info": "L'accuracy est la proportion de prédictions correctes. Comme la plupart des fenêtres ne contiennent aucun cri, les vrais négatifs dominent et l'accuracy reste trompeusement élevée — elle est donc désactivée par défaut.", "eval-tab-annotation-col-accordion-label": "Colonnes d'annotation", "eval-tab-annotation-selection-button-label": "Sélectionner le répertoire d'annotations", "eval-tab-annotation-selection-textbox-placeholder": "Aucun répertoire d'annotations sélectionné", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Table de sélection Raven", "multi-tab-result-dataframe-column-invalid-file-header": "Fichiers audio non valides", "multi-tab-result-dataframe-column-success-header": "Tous les fichiers ont été analysés !", diff --git a/birdnet_analyzer/lang/id.json b/birdnet_analyzer/lang/id.json index 3befaf2fd..2c09e1eb1 100644 --- a/birdnet_analyzer/lang/id.json +++ b/birdnet_analyzer/lang/id.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "Tidak ada direktori input yang dipilih", "embeddings-tab-start-button-label": "Ekstrak embedding", "embeddings-tab-title": "Embedding", - "eval-tab-accuracy-checkbox-info": "Akurasi mengukur persentase prediksi benar yang dibuat oleh model.", + "eval-tab-accuracy-checkbox-info": "Akurasi adalah proporsi prediksi yang benar. Karena sebagian besar jendela tidak berisi suara, negatif benar mendominasi dan akurasi tetap tinggi secara menyesatkan — jadi dinonaktifkan secara bawaan.", "eval-tab-annotation-col-accordion-label": "Kolom anotasi", "eval-tab-annotation-selection-button-label": "Pilih direktori anotasi", "eval-tab-annotation-selection-textbox-placeholder": "Tidak ada direktori anotasi yang dipilih", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Tabel seleksi Raven", "multi-tab-result-dataframe-column-invalid-file-header": "File audio tidak valid", "multi-tab-result-dataframe-column-success-header": "Semua file telah dianalisis!", diff --git a/birdnet_analyzer/lang/pt-br.json b/birdnet_analyzer/lang/pt-br.json index da99a3afc..22fdeb609 100644 --- a/birdnet_analyzer/lang/pt-br.json +++ b/birdnet_analyzer/lang/pt-br.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "Nenhum diretório de entrada selecionado", "embeddings-tab-start-button-label": "Extrair embeddings", "embeddings-tab-title": "Embeddings", - "eval-tab-accuracy-checkbox-info": "A precisão mede a porcentagem de previsões corretas feitas pelo modelo.", + "eval-tab-accuracy-checkbox-info": "A acurácia é a proporção de previsões corretas. Como a maioria das janelas não contém nenhum som, os verdadeiros negativos dominam e a acurácia permanece enganosamente alta — por isso fica desativada por padrão.", "eval-tab-annotation-col-accordion-label": "Colunas de anotação", "eval-tab-annotation-selection-button-label": "Selecionar diretório de anotações", "eval-tab-annotation-selection-textbox-placeholder": "Nenhum diretório de anotações selecionado", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Tabela de seleção Raven", "multi-tab-result-dataframe-column-invalid-file-header": "Arquivos de áudio inválidos", "multi-tab-result-dataframe-column-success-header": "Todos os arquivos foram analisados!", diff --git a/birdnet_analyzer/lang/ru.json b/birdnet_analyzer/lang/ru.json index 6b075832b..c51350237 100644 --- a/birdnet_analyzer/lang/ru.json +++ b/birdnet_analyzer/lang/ru.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "Каталог входных данных не выбран", "embeddings-tab-start-button-label": "Извлечь эмбеддинги", "embeddings-tab-title": "Эмбеддинги", - "eval-tab-accuracy-checkbox-info": "Точность измеряет процент правильных прогнозов, сделанных моделью.", + "eval-tab-accuracy-checkbox-info": "Accuracy — доля правильных предсказаний. Поскольку в большинстве окон нет вокализаций, преобладают истинно отрицательные примеры, и accuracy остаётся обманчиво высокой — поэтому по умолчанию она отключена.", "eval-tab-annotation-col-accordion-label": "Колонки аннотаций", "eval-tab-annotation-selection-button-label": "Выбрать каталог аннотаций", "eval-tab-annotation-selection-textbox-placeholder": "Каталог аннотаций не выбран", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Таблица выбора Raven", "multi-tab-result-dataframe-column-invalid-file-header": "Недопустимые аудиофайлы", "multi-tab-result-dataframe-column-success-header": "Все файлы проанализированы!", diff --git a/birdnet_analyzer/lang/se.json b/birdnet_analyzer/lang/se.json index 72de74996..65018afd5 100644 --- a/birdnet_analyzer/lang/se.json +++ b/birdnet_analyzer/lang/se.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "Ingen inmatningskatalog vald", "embeddings-tab-start-button-label": "Extrahera inbäddningar", "embeddings-tab-title": "Inbäddningar", - "eval-tab-accuracy-checkbox-info": "Noggrannhet mäter procentandelen korrekta förutsägelser som modellen gör.", + "eval-tab-accuracy-checkbox-info": "Accuracy är andelen korrekta prediktioner. Eftersom de flesta fönster inte innehåller något läte dominerar sanna negativa och accuracy förblir vilseledande hög — därför är den avstängd som standard.", "eval-tab-annotation-col-accordion-label": "Anteckningskolumner", "eval-tab-annotation-selection-button-label": "Välj anteckningskatalog", "eval-tab-annotation-selection-textbox-placeholder": "Ingen anteckningskatalog vald", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Raven-urvalstabell", "multi-tab-result-dataframe-column-invalid-file-header": "Ogiltiga ljudfiler", "multi-tab-result-dataframe-column-success-header": "Alla filer har analyserats!", diff --git a/birdnet_analyzer/lang/tlh.json b/birdnet_analyzer/lang/tlh.json index 2d8ed7ac3..41f8f7628 100644 --- a/birdnet_analyzer/lang/tlh.json +++ b/birdnet_analyzer/lang/tlh.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "ngoq Daq wIvlu'be'", "embeddings-tab-start-button-label": "qelmey yInob", "embeddings-tab-title": "qelmey", - "eval-tab-accuracy-checkbox-info": "qel patlh vIt.", + "eval-tab-accuracy-checkbox-info": "Accuracy is the share of correct predictions. Since most sample windows contain no call, true negatives dominate and accuracy stays misleadingly high — so it is off by default.", "eval-tab-annotation-col-accordion-label": "QInmey", "eval-tab-annotation-selection-button-label": "QIn Daq yIwIv", "eval-tab-annotation-selection-textbox-placeholder": "QIj Daq wIvlu'be'", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Raven wIv ghItlh", "multi-tab-result-dataframe-column-invalid-file-header": "QoywI' De' qal", "multi-tab-result-dataframe-column-success-header": "Hoch tey' lunuDta'!", diff --git a/birdnet_analyzer/lang/zh_TW.json b/birdnet_analyzer/lang/zh_TW.json index 45f1ac5e0..863127d64 100644 --- a/birdnet_analyzer/lang/zh_TW.json +++ b/birdnet_analyzer/lang/zh_TW.json @@ -59,7 +59,7 @@ "embeddings-tab-select-input-directory-textbox-placeholder": "未選擇輸入目錄", "embeddings-tab-start-button-label": "提取嵌入", "embeddings-tab-title": "嵌入", - "eval-tab-accuracy-checkbox-info": "準確度測量模型做出正確預測的百分比。", + "eval-tab-accuracy-checkbox-info": "準確率是正確預測的比例。由於多數視窗不含叫聲,真陰性占主導,準確率會維持在誤導性的高值,因此預設為關閉。", "eval-tab-annotation-col-accordion-label": "註釋欄位", "eval-tab-annotation-selection-button-label": "選擇註釋目錄", "eval-tab-annotation-selection-textbox-placeholder": "未選擇標註目錄", @@ -191,6 +191,7 @@ "multi-tab-output-type-audacity-label": "Audacity", "multi-tab-output-type-csv-label": "CSV", "multi-tab-output-type-kaleidoscope-label": "Kaleidoscope", + "multi-tab-output-type-parquet-label": "Parquet", "multi-tab-output-type-raven-label": "Raven 選擇表", "multi-tab-result-dataframe-column-invalid-file-header": "無效的音訊檔案", "multi-tab-result-dataframe-column-success-header": "所有檔案皆已分析!", From 108d9fd2f07203fa0a91e764fc22ff735f2d32e3 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 3 Aug 2026 15:16:26 +0200 Subject: [PATCH 2/3] Streamline params --- birdnet_analyzer/gui/train.py | 74 ++++++++++++++++++++++++++------ birdnet_analyzer/lang/de.json | 2 + birdnet_analyzer/lang/en.json | 2 + birdnet_analyzer/lang/fi.json | 2 + birdnet_analyzer/lang/fr.json | 2 + birdnet_analyzer/lang/id.json | 2 + birdnet_analyzer/lang/pt-br.json | 2 + birdnet_analyzer/lang/ru.json | 2 + birdnet_analyzer/lang/se.json | 2 + birdnet_analyzer/lang/tlh.json | 2 + birdnet_analyzer/lang/zh_TW.json | 2 + 11 files changed, 81 insertions(+), 13 deletions(-) diff --git a/birdnet_analyzer/gui/train.py b/birdnet_analyzer/gui/train.py index a89b14f62..ca1a03147 100644 --- a/birdnet_analyzer/gui/train.py +++ b/birdnet_analyzer/gui/train.py @@ -70,6 +70,7 @@ def start_training( upsampling_mode, model_formats, audio_speed, + threads, progress=gr.Progress(), ): """Starts the training of a custom classifier. @@ -106,6 +107,8 @@ def start_training( upsampling_mode: Mode for upsampling (repeat, mean, smote). model_formats: Formats to save the trained model (tflite, raven, detached). audio_speed: Speed factor for audio playback. + threads: Number of parallel CPU threads for loading and decoding the training + audio. Ignored when training from a cache file. save_detached_classifier: Whether to save the detached classifier. Returns: Returns a matplotlib.pyplot figure. @@ -212,7 +215,9 @@ def trial_progression(trial): else None, dropout=max(0.0, min(1.0, float(dropout))), overlap=max(0.0, min(2.9, float(crop_overlap))), - threads=max(1, multiprocessing.cpu_count()), + threads=int(threads) + if threads and int(threads) > 0 + else max(1, multiprocessing.cpu_count()), on_epoch_end=epoch_progression, on_trial_result=trial_progression, on_data_load_end=data_load_progression, @@ -534,6 +539,7 @@ def on_cache_mode_change(value): gr.update(interactive=value != "load"), gr.update(interactive=value != "load"), gr.update(interactive=value != "load"), + gr.update(interactive=value != "load"), ) with ( @@ -620,6 +626,18 @@ def on_cache_mode_change(value): interactive=crops_segments and not uses_cache_file, ) + threads_number = state.persist( + "threads_number", + gr.Number, + value=multiprocessing.cpu_count(), + minimum=1, + step=1, + precision=0, + interactive=not uses_cache_file, + label=loc.localize("training-tab-threads-number-label"), + info=loc.localize("training-tab-threads-number-info"), + ) + def on_crop_select(new_crop_mode): # Make overlap slider visible for both "segments" and "smart" crop modes return gr.update( @@ -644,6 +662,7 @@ def on_crop_select(new_crop_mode): audio_speed_slider, crop_mode, crop_overlap, + threads_number, ], show_progress="hidden", ) @@ -684,8 +703,13 @@ def on_crop_select(new_crop_mode): info=loc.localize("training-tab-autotune-repeats-number-info"), ) + # The custom-parameters accordion stays visible while autotuning; every field + # the tuner searches is disabled instead (its value is only a seed and gets + # overridden), so users can still see the values but not waste time tuning them + # by hand. "epochs" is not autotuned and stays editable -- it applies to every + # trial. with ( - gr.Group(visible=not autotunes) as custom_params, + gr.Group(), gr.Accordion( open=False, label=loc.localize("training-tab-custom-params-accordion-label"), @@ -707,6 +731,7 @@ def on_crop_select(new_crop_mode): value=32, minimum=1, step=8, + interactive=not autotunes, label=loc.localize("training-tab-batchsize-number-label"), info=loc.localize("training-tab-batchsize-number-info"), ) @@ -716,6 +741,7 @@ def on_crop_select(new_crop_mode): value=0.0001, minimum=0.0001, step=0.0001, + interactive=not autotunes, label=loc.localize("training-tab-learningrate-number-label"), info=loc.localize("training-tab-learningrate-number-info"), ) @@ -727,6 +753,7 @@ def on_crop_select(new_crop_mode): value=0, minimum=0, step=64, + interactive=not autotunes, label=loc.localize("training-tab-hiddenunits-number-label"), info=loc.localize("training-tab-hiddenunits-number-info"), ) @@ -737,6 +764,7 @@ def on_crop_select(new_crop_mode): minimum=0.0, maximum=0.9, step=0.1, + interactive=not autotunes, label=loc.localize("training-tab-dropout-number-label"), info=loc.localize("training-tab-dropout-number-info"), ) @@ -744,6 +772,7 @@ def on_crop_select(new_crop_mode): "use_label_smoothing_checkbox", gr.Checkbox, value=False, + interactive=not autotunes, label=loc.localize( "training-tab-use-labelsmoothing-checkbox-label" ), @@ -774,6 +803,7 @@ def on_crop_select(new_crop_mode): ), ], value="repeat", + interactive=not autotunes, label=loc.localize("training-tab-upsampling-radio-label"), info=loc.localize("training-tab-upsampling-radio-info"), ) @@ -784,6 +814,7 @@ def on_crop_select(new_crop_mode): maximum=1.0, value=0.0, step=0.05, + interactive=not autotunes, label=loc.localize("training-tab-upsampling-ratio-slider-label"), info=loc.localize("training-tab-upsampling-ratio-slider-info"), ) @@ -793,6 +824,7 @@ def on_crop_select(new_crop_mode): "use_mixup_checkbox", gr.Checkbox, value=False, + interactive=not autotunes, label=loc.localize("training-tab-use-mixup-checkbox-label"), info=loc.localize("training-tab-use-mixup-checkbox-info"), show_label=True, @@ -801,14 +833,13 @@ def on_crop_select(new_crop_mode): "use_focal_loss_checkbox", gr.Checkbox, value=False, + interactive=not autotunes, label=loc.localize("training-tab-use-focal-loss-checkbox-label"), info=loc.localize("training-tab-use-focal-loss-checkbox-info"), show_label=True, ) - with gr.Row( - visible=bool(use_focal_loss.value) and not autotunes - ) as focal_loss_params: + with gr.Row(visible=bool(use_focal_loss.value)) as focal_loss_params: focal_loss_gamma = state.persist( "focal_loss_gamma_slider", gr.Slider, @@ -816,9 +847,9 @@ def on_crop_select(new_crop_mode): maximum=5.0, value=2.0, step=0.1, + interactive=not autotunes, label=loc.localize("training-tab-focal-loss-gamma-slider-label"), info=loc.localize("training-tab-focal-loss-gamma-slider-info"), - interactive=True, ) focal_loss_alpha = state.persist( "focal_loss_alpha_slider", @@ -827,9 +858,9 @@ def on_crop_select(new_crop_mode): maximum=0.9, value=0.25, step=0.05, + interactive=not autotunes, label=loc.localize("training-tab-focal-loss-alpha-slider-label"), info=loc.localize("training-tab-focal-loss-alpha-slider-info"), - interactive=True, ) def on_focal_loss_change(value): @@ -842,17 +873,33 @@ def on_focal_loss_change(value): show_progress="hidden", ) + # Every hyperparameter the tuner searches. When autotuning is on these are + # disabled (their values are only seeds and get overridden); "epochs" is not + # tuned and stays editable. + autotuned_params = [ + batch_size_number, + learning_rate_number, + hidden_units_number, + dropout_number, + use_label_smoothing, + upsampling_mode, + upsampling_ratio, + use_mixup, + use_focal_loss, + focal_loss_gamma, + focal_loss_alpha, + ] + def on_autotune_change(value): - return ( - gr.update(visible=not value), - gr.update(visible=value), - gr.update(visible=not value and use_focal_loss.value), - ) + return [ + gr.update(visible=value), # autotune_params + *[gr.update(interactive=not value) for _ in autotuned_params], + ] autotune_cb.change( on_autotune_change, inputs=autotune_cb, - outputs=[custom_params, autotune_params, focal_loss_params], + outputs=[autotune_params, *autotuned_params], show_progress="hidden", ) @@ -966,6 +1013,7 @@ def train_and_show_metrics(*args): upsampling_mode, output_formats, audio_speed_slider, + threads_number, ], outputs=[train_history_plot, metrics_table], ) diff --git a/birdnet_analyzer/lang/de.json b/birdnet_analyzer/lang/de.json index 6c55f7d62..0068c16d9 100644 --- a/birdnet_analyzer/lang/de.json +++ b/birdnet_analyzer/lang/de.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "Training starten", "training-tab-test-data-selection-button-label": "Testdaten auswählen (optional)", "training-tab-test-data-selection-textbox-placeholder": "Kein Testdatenverzeichnis ausgewählt", + "training-tab-threads-number-info": "Anzahl paralleler CPU-Threads zum Laden und Dekodieren der Trainingsaudios. Mehr Threads können das Laden beschleunigen, brauchen aber mehr CPU und Speicher. Wird beim Training aus einer Cache-Datei ignoriert.", + "training-tab-threads-number-label": "Threads zum Laden der Daten", "training-tab-title": "Trainieren", "training-tab-upsampling-radio-info": "Balancieren Sie die Trainingsdaten durch Upsampling von Minderheitenklassen aus.", "training-tab-upsampling-radio-label": "Upsampling-Modus", diff --git a/birdnet_analyzer/lang/en.json b/birdnet_analyzer/lang/en.json index 91fab7222..f493c14f0 100644 --- a/birdnet_analyzer/lang/en.json +++ b/birdnet_analyzer/lang/en.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "Start training", "training-tab-test-data-selection-button-label": "Select test data (optional)", "training-tab-test-data-selection-textbox-placeholder": "No test data directory selected", + "training-tab-threads-number-info": "Number of parallel CPU threads used to load and decode the training audio. More threads can speed up data loading but use more CPU and memory. Ignored when training from a cache file.", + "training-tab-threads-number-label": "Data loading threads", "training-tab-title": "Train", "training-tab-upsampling-radio-info": "Balance train data by upsampling minority classes.", "training-tab-upsampling-radio-label": "Upsampling mode", diff --git a/birdnet_analyzer/lang/fi.json b/birdnet_analyzer/lang/fi.json index c00a79a8e..df359b26e 100644 --- a/birdnet_analyzer/lang/fi.json +++ b/birdnet_analyzer/lang/fi.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "Aloita koulutus", "training-tab-test-data-selection-button-label": "Valitse testidataa (Valinnainen)", "training-tab-test-data-selection-textbox-placeholder": "Testidatahakemistoa ei valittu", + "training-tab-threads-number-info": "Rinnakkaisten suoritinsäikeiden määrä koulutusäänen lataamiseen ja purkamiseen. Useammat säikeet voivat nopeuttaa datan latausta, mutta käyttävät enemmän suoritinta ja muistia. Ei käytössä, kun koulutus tehdään välimuistitiedostosta.", + "training-tab-threads-number-label": "Datan latauksen säikeet", "training-tab-title": "Kouluta", "training-tab-upsampling-radio-info": "Tasapainota koulutusdataa ylösnäytteistämällä vähemmistöluokkia.", "training-tab-upsampling-radio-label": "Ylösnäytteistystila", diff --git a/birdnet_analyzer/lang/fr.json b/birdnet_analyzer/lang/fr.json index 915fb8de5..ad7a19906 100644 --- a/birdnet_analyzer/lang/fr.json +++ b/birdnet_analyzer/lang/fr.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "Commencer l’apprentissage", "training-tab-test-data-selection-button-label": "Sélectionner les données de test (optionnel)", "training-tab-test-data-selection-textbox-placeholder": "Aucun répertoire de données de test sélectionné", + "training-tab-threads-number-info": "Nombre de threads CPU parallèles utilisés pour charger et décoder l'audio d'entraînement. Plus de threads peut accélérer le chargement des données mais utilise plus de CPU et de mémoire. Ignoré lors de l'entraînement à partir d'un fichier cache.", + "training-tab-threads-number-label": "Threads de chargement des données", "training-tab-title": "Apprentissage", "training-tab-upsampling-radio-info": "Équilibrer les données de formation en suréchantillonnant les classes minoritaires.", "training-tab-upsampling-radio-label": "Mode de suréchantillonnage", diff --git a/birdnet_analyzer/lang/id.json b/birdnet_analyzer/lang/id.json index 2c09e1eb1..15ae5433f 100644 --- a/birdnet_analyzer/lang/id.json +++ b/birdnet_analyzer/lang/id.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "Mulai pelatihan", "training-tab-test-data-selection-button-label": "Pilih data pengujian (opsional)", "training-tab-test-data-selection-textbox-placeholder": "Tidak ada direktori data uji yang dipilih", + "training-tab-threads-number-info": "Jumlah utas CPU paralel yang digunakan untuk memuat dan mendekode audio pelatihan. Lebih banyak utas dapat mempercepat pemuatan data tetapi menggunakan lebih banyak CPU dan memori. Diabaikan saat melatih dari berkas cache.", + "training-tab-threads-number-label": "Utas pemuatan data", "training-tab-title": "Latih", "training-tab-upsampling-radio-info": "Menyeimbangkan data pelatihan dengan peningkatan kelas minoritas.", "training-tab-upsampling-radio-label": "Mode peningkatan sampel", diff --git a/birdnet_analyzer/lang/pt-br.json b/birdnet_analyzer/lang/pt-br.json index 22fdeb609..f0fa3cfdf 100644 --- a/birdnet_analyzer/lang/pt-br.json +++ b/birdnet_analyzer/lang/pt-br.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "Comece o treinamento", "training-tab-test-data-selection-button-label": "Selecionar dados de teste (opcional)", "training-tab-test-data-selection-textbox-placeholder": "Nenhum diretório de dados de teste selecionado", + "training-tab-threads-number-info": "Número de threads de CPU paralelas usadas para carregar e decodificar o áudio de treinamento. Mais threads podem acelerar o carregamento dos dados, mas usam mais CPU e memória. Ignorado ao treinar a partir de um arquivo de cache.", + "training-tab-threads-number-label": "Threads de carregamento de dados", "training-tab-title": "Treinamento", "training-tab-upsampling-radio-info": "Balanceia os dados de treinamento por meio de aumento de amostras nas classes minoritárias.", "training-tab-upsampling-radio-label": "Modo de upsampling", diff --git a/birdnet_analyzer/lang/ru.json b/birdnet_analyzer/lang/ru.json index c51350237..38a455669 100644 --- a/birdnet_analyzer/lang/ru.json +++ b/birdnet_analyzer/lang/ru.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "Начало обучения", "training-tab-test-data-selection-button-label": "Выбор данных для тестирования (опционально)", "training-tab-test-data-selection-textbox-placeholder": "Каталог тестовых данных не выбран", + "training-tab-threads-number-info": "Количество параллельных потоков ЦП для загрузки и декодирования обучающего аудио. Больше потоков может ускорить загрузку данных, но требует больше ресурсов ЦП и памяти. Игнорируется при обучении из файла кэша.", + "training-tab-threads-number-label": "Потоки загрузки данных", "training-tab-title": "Обучить", "training-tab-upsampling-radio-info": "Балансировка тренировочных данных с помощью повышающей дискретизации меньших классов", "training-tab-upsampling-radio-label": "Режим повышающей дискретизации", diff --git a/birdnet_analyzer/lang/se.json b/birdnet_analyzer/lang/se.json index 65018afd5..de947dfab 100644 --- a/birdnet_analyzer/lang/se.json +++ b/birdnet_analyzer/lang/se.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "Starta träning", "training-tab-test-data-selection-button-label": "Välj testdata (valfritt)", "training-tab-test-data-selection-textbox-placeholder": "Ingen testdatakatalog vald", + "training-tab-threads-number-info": "Antal parallella CPU-trådar som används för att läsa in och avkoda träningsljudet. Fler trådar kan snabba upp datainläsningen men använder mer CPU och minne. Ignoreras vid träning från en cachefil.", + "training-tab-threads-number-label": "Trådar för datainläsning", "training-tab-title": "Träna", "training-tab-upsampling-radio-info": "Balansera träningsdata genom upsampling av minoritetsklasser.", "training-tab-upsampling-radio-label": "Upsamplingsläge", diff --git a/birdnet_analyzer/lang/tlh.json b/birdnet_analyzer/lang/tlh.json index 41f8f7628..ef055487f 100644 --- a/birdnet_analyzer/lang/tlh.json +++ b/birdnet_analyzer/lang/tlh.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "qeq yItagh", "training-tab-test-data-selection-button-label": "wav qeq (rap)", "training-tab-test-data-selection-textbox-placeholder": "nuD De' Daq wIvlu'be'", + "training-tab-threads-number-info": "Number of parallel CPU threads used to load and decode the training audio. More threads can speed up data loading but use more CPU and memory. Ignored when training from a cache file.", + "training-tab-threads-number-label": "Data loading threads", "training-tab-title": "qeq", "training-tab-upsampling-radio-info": "Seghmey qochHa'.", "training-tab-upsampling-radio-label": "qechmey choH", diff --git a/birdnet_analyzer/lang/zh_TW.json b/birdnet_analyzer/lang/zh_TW.json index 863127d64..15c240265 100644 --- a/birdnet_analyzer/lang/zh_TW.json +++ b/birdnet_analyzer/lang/zh_TW.json @@ -424,6 +424,8 @@ "training-tab-start-training-button-label": "開始訓練", "training-tab-test-data-selection-button-label": "選擇測試資料 (可選)", "training-tab-test-data-selection-textbox-placeholder": "未選擇測試資料目錄", + "training-tab-threads-number-info": "用於載入和解碼訓練音訊的平行 CPU 執行緒數量。更多執行緒可加快資料載入速度,但會使用更多 CPU 和記憶體。從快取檔案訓練時會忽略此設定。", + "training-tab-threads-number-label": "資料載入執行緒", "training-tab-title": "訓練客製化分類器", "training-tab-upsampling-radio-info": "上取樣樣本較少的類別已達到平衡的訓練資料", "training-tab-upsampling-radio-label": "上取樣模式", From 61d37c89941284a152998202c11f864908af5b19 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 3 Aug 2026 15:45:23 +0200 Subject: [PATCH 3/3] .\CLAUDE.md --- birdnet_analyzer/evaluation/__init__.py | 7 ++++++- birdnet_analyzer/gui/train.py | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/birdnet_analyzer/evaluation/__init__.py b/birdnet_analyzer/evaluation/__init__.py index 9f34f6d77..0beb159fa 100644 --- a/birdnet_analyzer/evaluation/__init__.py +++ b/birdnet_analyzer/evaluation/__init__.py @@ -192,7 +192,12 @@ def _threshold_arg(value: str) -> float: Fails at the parser with a clear message instead of deep inside the PerformanceAssessor, which requires ``0 < threshold < 1``. """ - number = float(value) + try: + number = float(value) + except ValueError: + raise argparse.ArgumentTypeError( + f"threshold must be a number between 0 and 1 (exclusive), got {value!r}" + ) from None if not 0 < number < 1: raise argparse.ArgumentTypeError( f"threshold must be between 0 and 1 (exclusive), got {value}" diff --git a/birdnet_analyzer/gui/train.py b/birdnet_analyzer/gui/train.py index ca1a03147..b488437f0 100644 --- a/birdnet_analyzer/gui/train.py +++ b/birdnet_analyzer/gui/train.py @@ -109,7 +109,6 @@ def start_training( audio_speed: Speed factor for audio playback. threads: Number of parallel CPU threads for loading and decoding the training audio. Ignored when training from a cache file. - save_detached_classifier: Whether to save the detached classifier. Returns: Returns a matplotlib.pyplot figure. """