Skip to content
Open
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
5 changes: 0 additions & 5 deletions birdnet_analyzer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,10 +932,5 @@ def train_parser():
choices=get_args(AUTOTUNE_METRICS),
help="Metric to optimize during hyperparameter tuning. This can be any metric that is returned by the training process and is included in the training history object. Common choices are 'val_loss', 'val_AUPRC' or 'val_AUROC'.",
)
parser.add_argument(
"--save_detached_classifier",
action="store_true",
help="Whether to additionally save a detached version of the trained classifier. This can be useful if you want to use the trained classifier in another model or application without having to load the entire model.",
)

return parser
2 changes: 0 additions & 2 deletions birdnet_analyzer/train/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def train(
autotune_n_repeats: int = 1,
autotune_n_splits: int = 3,
autotune_metric: AUTOTUNE_METRICS = "val_AUPRC",
save_detached_classifier: bool = False,
):
"""
Trains a custom classifier model using the BirdNET-Analyzer framework.
Expand Down Expand Up @@ -141,5 +140,4 @@ def train(
autotune_n_repeats=autotune_n_repeats,
autotune_n_splits=autotune_n_splits,
autotune_metric=autotune_metric,
save_detached_classifier=save_detached_classifier,
)
3 changes: 1 addition & 2 deletions birdnet_analyzer/train/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,6 @@ def train_model(
on_epoch_end=None,
on_trial_result=None,
on_data_load_end=None,
save_detached_classifier: bool = False,
):
"""Trains a custom classifier.

Expand Down Expand Up @@ -805,7 +804,7 @@ def generate_splits(
model.save_detached_classifier(
classifier,
output,
labels=labels if model_save_mode == "append" else None,
labels=labels,
)
except Exception as e:
raise Exception("Error saving model") from e
Expand Down
85 changes: 85 additions & 0 deletions tests/analyze/test_analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import platform
import shutil
import tempfile
from unittest.mock import patch

import birdnet
import numpy as np
import pandas as pd
import pytest

from birdnet_analyzer.analyze.core import analyze
from birdnet_analyzer.cli import analyzer_parser


@pytest.fixture
Expand All @@ -31,6 +33,89 @@ def setup_test_environment():
shutil.rmtree(test_dir)


@patch("birdnet_analyzer.model_utils.run_inference")
def test_analyze_cli_accepts_full_parser_surface(
mock_run_inference, setup_test_environment
):
env = setup_test_environment

mock_run_inference.return_value = object()

parser = analyzer_parser()
species_list_path = os.path.join(env["test_dir"], "species_list.txt")
classifier_path = os.path.join(env["test_dir"], "classifier.tflite")
cc_species_list_path = os.path.join(env["test_dir"], "classifier_labels.txt")
args = parser.parse_args(
[
env["input_dir"],
"--output",
env["output_dir"],
"--birdnet",
"2.4",
"--min_conf",
"0.1",
"--classifier",
classifier_path,
"--cc_species_list",
cc_species_list_path,
"--slist",
species_list_path,
"--sensitivity",
"1.2",
"--overlap",
"0.5",
"--fmin",
"100",
"--fmax",
"10000",
"--audio_speed",
"1.1",
"-b",
"4",
"--n_workers",
"2",
"--n_producers",
"3",
"--rtype",
"csv",
"parquet",
"--additional_columns",
"lat",
"lon",
"week",
"model",
"overlap",
"sensitivity",
"species_list",
"min_conf",
"--top_n",
"5",
"--merge_consecutive",
"2",
"--locale",
"de",
"--use_perch",
"--split_tables",
]
)

kwargs = vars(args)
assert kwargs["use_perch"] is True
kwargs.pop("use_perch", None)

analyze(**kwargs, _return_only=True)

mock_run_inference.assert_called_once()
call_kwargs = mock_run_inference.call_args.kwargs
assert call_kwargs["top_k"] == 5
assert call_kwargs["batch_size"] == 4
assert call_kwargs["n_workers"] == 2
assert call_kwargs["n_producers"] == 3
assert call_kwargs["bandpass_fmin"] == 100
assert call_kwargs["bandpass_fmax"] == 10000
assert call_kwargs["sigmoid_sensitivity"] == 1.2


def test_analyze_with_real_custom_classifier(setup_test_environment):
"""Test analyzing with a real custom classifier."""
env = setup_test_environment
Expand Down
58 changes: 58 additions & 0 deletions tests/embeddings/test_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,64 @@ def test_embeddings_cli(
assert call_kwargs[1]["version"] == "2.4"


@patch("birdnet_analyzer.embeddings.core.create_csv_output")
@patch("birdnet_analyzer.embeddings.core._check_database_settings")
@patch("birdnet_analyzer.embeddings.core.get_or_create_database")
@patch("birdnet_analyzer.model_utils.get_embeddings")
def test_embeddings_cli_accepts_full_parser_surface(
mock_get_embeddings: MagicMock,
mock_get_db: MagicMock,
mock_check_settings: MagicMock,
mock_csv_output: MagicMock,
setup_test_environment,
):
env = setup_test_environment

mock_get_embeddings.return_value = _make_empty_encoding_result()
mock_db = MagicMock()
mock_get_db.return_value = mock_db

parser = embeddings_parser()
file_output = os.path.join(env["output_dir"], "embeddings.csv")
args = parser.parse_args(
[
"--input",
env["input_dir"],
"-db",
env["output_dir"],
"--overlap",
"0.5",
"--audio_speed",
"1.1",
"--fmin",
"100",
"--fmax",
"10000",
"-b",
"4",
"--n_workers",
"2",
"--n_producers",
"3",
"--file_output",
file_output,
]
)

embeddings(**vars(args))

mock_get_embeddings.assert_called_once()
call_kwargs = mock_get_embeddings.call_args.kwargs
assert call_kwargs["batch_size"] == 4
assert call_kwargs["overlap_duration_s"] == 0.5
assert call_kwargs["bandpass_fmin"] == 100
assert call_kwargs["bandpass_fmax"] == 10000
assert call_kwargs["speed"] == 1.1
assert call_kwargs["n_workers"] == 2
assert call_kwargs["n_producers"] == 3
assert mock_csv_output.called


@patch("birdnet_analyzer.embeddings.core._ensure_recording")
@patch("birdnet_analyzer.embeddings.core._ensure_deployment")
@patch("birdnet_analyzer.embeddings.core._check_database_settings")
Expand Down
88 changes: 88 additions & 0 deletions tests/search/test_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import os
import shutil
import tempfile
from unittest.mock import MagicMock, patch

from birdnet_analyzer.cli import search_parser
from birdnet_analyzer.search.core import search


def _make_test_environment():
test_dir = tempfile.mkdtemp()
input_dir = os.path.join(test_dir, "input")
output_dir = os.path.join(test_dir, "output")

os.makedirs(input_dir, exist_ok=True)
os.makedirs(output_dir, exist_ok=True)

return {
"test_dir": test_dir,
"input_dir": input_dir,
"output_dir": output_dir,
}


@patch("birdnet_analyzer.audio.save_signal")
@patch("birdnet_analyzer.audio.open_audio_file")
@patch("birdnet_analyzer.search.utils.get_search_results")
@patch("birdnet_analyzer.search.core.get_database")
def test_search_cli_accepts_full_parser_surface(
mock_get_database,
mock_get_search_results,
mock_open_audio_file,
mock_save_signal,
):
env = _make_test_environment()
try:
parser = search_parser()
args = parser.parse_args(
[
"-q",
os.path.join(env["test_dir"], "query.wav"),
"-o",
env["output_dir"],
"--n_results",
"5",
"--score_function",
"dot",
"--crop_mode",
"segments",
"--audio_root",
env["input_dir"],
"-db",
os.path.join(env["test_dir"], "database.sqlite"),
"--overlap",
"0.5",
]
)

mock_db = MagicMock()
mock_db.get_metadata.return_value = {
"BANDPASS_FMIN": 100,
"BANDPASS_FMAX": 10000,
"AUDIO_SPEED": 1.1,
"SIG_LENGTH": 3.0,
}
mock_db.get_window.return_value = MagicMock(recording_id=7, offsets=[1.0, 2.0])
mock_db.get_recording.return_value = MagicMock(filename="query_source.wav")
mock_db.db = MagicMock()
mock_get_database.return_value = mock_db

mock_get_search_results.return_value = [
MagicMock(window_id=11, sort_score=0.12345)
]
mock_open_audio_file.return_value = (b"signal", 48000)

search(**vars(args))

mock_get_search_results.assert_called_once()
search_kwargs = mock_get_search_results.call_args.args
assert search_kwargs[0] == args.queryfile
assert search_kwargs[2] == 5
assert search_kwargs[6] == "dot"
assert search_kwargs[7] == "segments"
assert search_kwargs[8] == 0.5
mock_open_audio_file.assert_called_once()
mock_save_signal.assert_called_once()
finally:
shutil.rmtree(env["test_dir"])
58 changes: 58 additions & 0 deletions tests/segments/test_segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,61 @@ def test_segments_cli(
mock_parse_folders.assert_called_once()
mock_parse_files.assert_called_once()
mock_extract_segments.assert_called()


@patch("birdnet_analyzer.segments.utils.extract_segments")
@patch("birdnet_analyzer.segments.utils.parse_files")
@patch("birdnet_analyzer.segments.utils.parse_folders")
def test_segments_cli_accepts_full_parser_surface(
mock_parse_folders: MagicMock,
mock_parse_files: MagicMock,
mock_extract_segments: MagicMock,
setup_test_environment,
):
env = setup_test_environment

parser = segments_parser()
args = parser.parse_args(
[
env["input_dir"],
"--results",
env["results_dir"],
"--output",
env["output_dir"],
"--max_segments",
"5",
"--seg_length",
"4.5",
"--max_conf",
"0.9",
"--collection_mode",
"confidence",
"--n_bins",
"12",
"--audio_speed",
"1.25",
"--threads",
"1",
"--min_conf",
"0.15",
]
)

mock_parse_files.return_value = [
(
env["file_list"][0]["audio"],
[{"start": 0, "end": 3, "species": "sp1", "confidence": 0.9}],
),
]

segments(**vars(args))

mock_parse_folders.assert_called_once_with(env["input_dir"], env["results_dir"])
mock_parse_files.assert_called_once()
parse_files_kwargs = mock_parse_files.call_args.kwargs
assert parse_files_kwargs["max_segments"] == 5
assert parse_files_kwargs["collection_mode"] == "confidence"
assert parse_files_kwargs["n_bins"] == 12
assert parse_files_kwargs["min_conf"] == 0.15
assert parse_files_kwargs["max_conf"] == 0.9
mock_extract_segments.assert_called_once()
32 changes: 32 additions & 0 deletions tests/species/test_species.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,35 @@ def test_species_cli(mock_get_species_list, setup_test_environment):
mock_get_species_list.assert_called_once_with(
lat=-1, lon=-1, week=None, threshold=0.03, lang="en_us"
)


@patch("birdnet_analyzer.species.utils.get_species_list")
def test_species_cli_accepts_full_parser_surface(
mock_get_species_list, setup_test_environment
):
env = setup_test_environment

mock_get_species_list.return_value = ["Species1", "Species2"]

parser = species_parser()
args = parser.parse_args(
[
"--lat",
"42.5",
"--lon",
"-76.45",
"--week",
"20",
"--sf_thresh",
"0.12",
"--locale",
"de",
env["output_dir"],
]
)

species(**vars(args))

mock_get_species_list.assert_called_once_with(
lat=42.5, lon=-76.45, week=20, threshold=0.12, lang="de"
)
Loading
Loading