Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
486f374
custom classifier are read again, more testing needed to ensure old m…
max-mauermann Jul 28, 2025
af79aeb
added test for custom classifier training and prediction, changed beh…
max-mauermann Jul 29, 2025
81b6508
ruff fix
max-mauermann Jul 29, 2025
6edde52
timeout increase for test
max-mauermann Jul 29, 2025
2f52188
.
max-mauermann Jul 29, 2025
266af4d
.
max-mauermann Jul 29, 2025
e597a62
.
max-mauermann Jul 30, 2025
8defe2c
.
max-mauermann Jul 30, 2025
111e13d
.
max-mauermann Jul 30, 2025
a1951c7
.
max-mauermann Jul 30, 2025
7816a7d
.
max-mauermann Jul 30, 2025
5115c9f
.
max-mauermann Jul 30, 2025
edaa70b
.
max-mauermann Jul 30, 2025
c65dad1
.
max-mauermann Jul 30, 2025
abbf601
removed logging
max-mauermann Jul 30, 2025
e1b5641
Merge branch 'main' into custom-classifier-label-fix
max-mauermann Jul 30, 2025
e8cf558
added real training data from test-data submodule
max-mauermann Jul 30, 2025
672d79d
ruff fixes
max-mauermann Jul 30, 2025
bec7510
simple embeddings test
max-mauermann Jul 30, 2025
c0fffda
minmizing for testing
Josef-Haupt Jul 31, 2025
cc5a916
enabling more tests
Josef-Haupt Jul 31, 2025
e16be51
so far so good, more tests enabled
Josef-Haupt Jul 31, 2025
d898354
so far so good, more tests enabled
Josef-Haupt Jul 31, 2025
703edc9
so far so good, more tests enabled
Josef-Haupt Jul 31, 2025
7416e63
so far so good, more tests enabled
Josef-Haupt Jul 31, 2025
1ce159e
so far so good, more tests enabled
Josef-Haupt Jul 31, 2025
840099f
so far so good, more tests enabled
Josef-Haupt Jul 31, 2025
c53455e
so far so good, all tests enabled
Josef-Haupt Jul 31, 2025
a0b7242
so far so good, all tests enabled
Josef-Haupt Jul 31, 2025
7cc96fb
i hate this
Josef-Haupt Jul 31, 2025
84eb30c
Moved failing test to back, see if that triggers it
Josef-Haupt Jul 31, 2025
68366a3
The ordering triggers it, checking interactions
Josef-Haupt Jul 31, 2025
6444e90
analyze Yes
Josef-Haupt Jul 31, 2025
97cc0a9
embeddings No
Josef-Haupt Jul 31, 2025
6f3f4da
evaluation No
Josef-Haupt Jul 31, 2025
6d97842
gui No
Josef-Haupt Jul 31, 2025
c4582ec
segments No
Josef-Haupt Jul 31, 2025
5f29fcb
species No
Josef-Haupt Jul 31, 2025
10d5876
Merge remote-tracking branch 'origin/main' into custom-classifier-lab…
Josef-Haupt Aug 18, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ jobs:
run: |
python -m pip install .[tests]
python -m birdnet_analyzer.utils
python -m pytest
python -m pytest -s tests/test_utils.py tests/train/
1 change: 1 addition & 0 deletions birdnet_analyzer/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def open_audio_file(path: str, sample_rate=48000, offset=0.0, duration=None, fmi
Returns:
Returns the audio time series and the sampling rate.
"""

# Open file with librosa (uses ffmpeg or libav)
if speed == 1.0:
sig, rate = librosa.load(
Expand Down
1 change: 0 additions & 1 deletion birdnet_analyzer/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,6 @@ def embeddings(sample):
Returns:
The embeddings.
"""

load_model(False)

sample = np.array(sample, dtype="float32")
Expand Down
1 change: 1 addition & 0 deletions birdnet_analyzer/train/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def _load_audio_file(f, label_vector, config):
else:
sig_splits = audio.split_signal(sig, rate, cfg.SIG_LENGTH, cfg.SIG_OVERLAP, cfg.SIG_MINLEN)


# Get feature embeddings
batch_size = 1 # turns out that batch size 1 is the fastest, probably because of having to resize the model input when the number of samples in a batch changes
for i in range(0, len(sig_splits), batch_size):
Expand Down
15 changes: 15 additions & 0 deletions tests/embeddings/test_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import tempfile
from unittest.mock import MagicMock, patch

import numpy as np
import pytest

import birdnet_analyzer.config as cfg
from birdnet_analyzer import model
from birdnet_analyzer.cli import embeddings_parser
from birdnet_analyzer.embeddings.core import embeddings

Expand Down Expand Up @@ -53,3 +55,16 @@ def test_embeddings_cli(mock_run_embeddings: MagicMock, mock_ensure_model: Magic
mock_ensure_model.assert_called_once()
threads = min(8, max(1, multiprocessing.cpu_count() // 2))
mock_run_embeddings.assert_called_once_with(env["input_dir"], env["output_dir"], 0, 1.0, 0, 15000, threads, 1, None)


def test_model_embeddings_function_returns_expected_shape():
# Create a dummy sample (e.g., 1D numpy array of audio data)
sample = np.zeros(144000).astype(np.float32)
# Reshape the sample to (1, 144000) as expected by the model
sample = sample.reshape(1, 144000)
# Call the embeddings function
result = model.embeddings(sample)

# Check that result is a numpy array and has expected shape (depends on model, e.g., (1, embedding_dim))
assert isinstance(result, np.ndarray)
assert result.ndim == 2
37 changes: 34 additions & 3 deletions tests/train/test_train.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

import birdnet_analyzer.config as cfg
from birdnet_analyzer.analyze.core import analyze
from birdnet_analyzer.cli import train_parser
from birdnet_analyzer.train.core import train

Expand All @@ -17,10 +18,11 @@ def setup_test_environment():
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)
# Directory should not exist, so no exist_ok=True
os.makedirs(input_dir)
os.makedirs(output_dir)

classifier_output = os.path.join(output_dir, "classifier_output")
classifier_output = os.path.join(output_dir, "classifier_output", "custom_classifier.tflite")

# Store original config values
original_config = {
Expand Down Expand Up @@ -55,3 +57,32 @@ def test_train_cli(mock_train_model, mock_ensure_model, setup_test_environment):

mock_ensure_model.assert_called_once()
mock_train_model.assert_called_once_with()

@pytest.mark.timeout(400) # Increase timeout for training, 400s should be sufficient, win is by far the slowest
def test_training(setup_test_environment):
"""Test the training process and prediction with dummy data."""
env = setup_test_environment
training_data_input = "tests/data/training"

# Read class names from subfolders in the input directory, filtering out background classes
dummy_classes = [
d for d in os.listdir(training_data_input)
if os.path.isdir(os.path.join(training_data_input, d)) and d.lower() not in cfg.NON_EVENT_CLASSES
]

train(training_data_input, env["classifier_output"])

assert os.path.isfile(env["classifier_output"]), "Classifier output file was not created."
assert os.path.exists(env["classifier_output"].replace(".tflite", "_Labels.txt")), "Labels file was not created."
assert os.path.exists(env["classifier_output"].replace(".tflite", "_Params.csv")), "Params file was not created."
assert os.path.exists(env["classifier_output"].replace(".tflite", ".tflite_sample_counts.csv")), "Params file was not created."

soundscape_path = "birdnet_analyzer/example/soundscape.wav"
analyze(soundscape_path, env["output_dir"], top_n=1, classifier=env["classifier_output"])

output_file = os.path.join(env["output_dir"], "soundscape.BirdNET.selection.table.txt")
with open(output_file) as f:
lines = f.readlines()[1:]
for line in lines:
parts = line.strip().split("\t")
assert parts[7] in dummy_classes, f"Detected class {parts[7]} not in expected classes {dummy_classes}"
Loading