Skip to content

Commit 66ca4f1

Browse files
Josef-Hauptmax-mauermannJosef HauptCopilot
authored
Perch v2 (#788)
* initial perch * perch model, GPU only * runs with GPU * . * . * failing test * update Readme + tests * ruff * use perch2 cpu + apply softmax instead of our sigmoid * tests * . * updated deps + tests * . * there is a problem on macOS where tf import hangs forever due to magic locks * . * . * add Python 3.12 to ci workflow * . * there seems to be a version mismatch in tf 2.20 with pyarrow and protobuf ....................... * . * ....... * ruff * comment on why tf versions differ on win+linux and mac * Update tests/test_utils.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * move tf+keras import back to top * . * added tests for model download + updated workflows * filter pytest warnings * disable sensitivity, change max overlap for PERCH in GUI + disable warnings * basic validation for cli * basic perch tests * new tf api in model.py * Update birdnet_analyzer/gui/train.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * more verbose * download perch in installed version * remove old benchmark script * maxs comments * lint --------- Co-authored-by: Max Mauermann <max-mauermann@web.de> Co-authored-by: Josef Haupt <josefhaupt@Josefs-MacBook-Air.local> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 78b2561 commit 66ca4f1

53 files changed

Lines changed: 728 additions & 556 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ concurrency:
1616
jobs:
1717
running-tests:
1818
runs-on: ${{ matrix.os }}
19+
env:
20+
IS_GITHUB_RUNNER: "true"
1921
strategy:
2022
matrix:
2123
os: [ubuntu-latest, macos-latest, windows-latest]
22-
python-version: ["3.11"]
24+
python-version: ["3.11", "3.12"]
2325
steps:
2426
- uses: actions/checkout@v4
2527
with:
@@ -32,9 +34,7 @@ jobs:
3234
- name: Install dependencies
3335
run: |
3436
python -m pip install --upgrade pip
35-
python -m pip install .[embeddings,train]
37+
python -m pip install .[embeddings,train,tests]
3638
- name: Run tests
3739
run: |
38-
python -m pip install .[tests]
39-
python -m birdnet_analyzer.utils
4040
python -m pytest

.github/workflows/documentation.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- uses: actions/checkout@v4
2323
- uses: actions/setup-python@v5
2424
with:
25-
python-version: '3.11'
25+
python-version: '3.12'
2626
- name: Install dependencies
2727
run: |
2828
pip install .[docs]

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up Python
1818
uses: actions/setup-python@v4
1919
with:
20-
python-version: '3.11'
20+
python-version: '3.12'
2121
- name: Install dependencies
2222
run: |
2323
python -m pip install --upgrade pip

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Set up Python
2121
uses: actions/setup-python@v4
2222
with:
23-
python-version: "3.11"
23+
python-version: "3.12"
2424

2525
- name: Install dependencies
2626
run: |

.github/workflows/test-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- name: Set up Python
2222
uses: actions/setup-python@v4
2323
with:
24-
python-version: "3.11"
24+
python-version: "3.12"
2525

2626
- name: Install dependencies
2727
run: |

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ celerybeat.pid
145145
*.sage.py
146146

147147
# Environments
148-
.env
149-
.venv
148+
.env*
149+
.venv*
150150
env/
151151
venv/
152152
ENV/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
![License](https://img.shields.io/github/license/birdnet-team/BirdNET-Analyzer)
1111
![OS](https://badgen.net/badge/OS/Linux%2C%20Windows%2C%20macOS/blue)
12-
[![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110/)
12+
[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-3120/)
1313
![Species](https://badgen.net/badge/Species/6512/blue)
1414
![Downloads](https://www-user.tu-chemnitz.de/~johau/birdnet_total_downloads_badge.php)
1515

benchmark.sh

Lines changed: 0 additions & 73 deletions
This file was deleted.

birdnet_analyzer/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
import os
2+
import warnings
3+
4+
from absl import logging
5+
16
from birdnet_analyzer.analyze import analyze
27
from birdnet_analyzer.embeddings import embeddings
38
from birdnet_analyzer.search import search
49
from birdnet_analyzer.segments import segments
510
from birdnet_analyzer.species import species
611
from birdnet_analyzer.train import train
712

8-
__version__ = "2.0.0"
13+
__version__ = "2.2.0"
914
__all__ = ["analyze", "embeddings", "search", "segments", "species", "train"]
15+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
16+
17+
logging.set_verbosity(logging.ERROR)
18+
warnings.filterwarnings("ignore")

birdnet_analyzer/analyze/core.py

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def analyze(
2727
threads: int = 8,
2828
locale: str = "en",
2929
additional_columns: list[str] | None = None,
30+
use_perch: bool = False,
3031
):
3132
"""
3233
Analyzes audio files for bird species detection using the BirdNET-Analyzer.
@@ -55,6 +56,7 @@ def analyze(
5556
threads (int, optional): Number of CPU threads to use for analysis. Defaults to 8.
5657
locale (str, optional): Locale for species names and output. Defaults to "en".
5758
additional_columns (list[str] | None, optional): Additional columns to include in the output. Defaults to None.
59+
use_perch (bool, optional): Whether to use the Perch model for analysis. Defaults to False.
5860
Returns:
5961
None
6062
Raises:
@@ -69,9 +71,6 @@ def analyze(
6971
import birdnet_analyzer.config as cfg
7072
from birdnet_analyzer.analyze.utils import analyze_file, save_analysis_params
7173
from birdnet_analyzer.analyze.utils import combine_results as combine
72-
from birdnet_analyzer.utils import ensure_model_exists
73-
74-
ensure_model_exists()
7574

7675
flist = _set_params(
7776
audio_input=audio_input,
@@ -98,6 +97,7 @@ def analyze(
9897
threads=threads,
9998
labels_file=cfg.LABELS_FILE,
10099
additional_columns=additional_columns,
100+
use_perch=use_perch,
101101
)
102102

103103
print(f"Found {len(cfg.FILE_LIST)} files to analyze")
@@ -154,29 +154,37 @@ def _set_params(
154154
threads,
155155
labels_file=None,
156156
additional_columns=None,
157+
use_perch=False,
157158
):
158159
import birdnet_analyzer.config as cfg
159160
from birdnet_analyzer.analyze.utils import load_codes
160161
from birdnet_analyzer.species.utils import get_species_list
161-
from birdnet_analyzer.utils import collect_audio_files, read_lines
162+
from birdnet_analyzer.utils import collect_audio_files, ensure_model_exists, read_lines
163+
164+
ensure_model_exists(check_perch=use_perch)
162165

163166
if not isinstance(overlap, int | float):
164167
raise ValueError("Overlap must be a numeric value.")
165168

166169
if overlap < 0:
167170
raise ValueError("Overlap must be a non-negative value.")
168171

169-
if overlap >= cfg.SIG_LENGTH:
170-
raise ValueError(f"Overlap must be less than {cfg.SIG_LENGTH} seconds.")
172+
if not use_perch and overlap > 2.9:
173+
raise ValueError("Overlap must be less than or equal to 2.9 seconds for BirdNET model.")
174+
175+
if use_perch and overlap > 4.9:
176+
raise ValueError("Overlap must be less than or equal to 4.9 seconds for Perch model.")
171177

172178
if not isinstance(audio_speed, int | float):
173179
raise ValueError("Audio speed must be a numeric value.")
174180

175181
if audio_speed <= 0:
176182
raise ValueError("Audio speed must be a positive value.")
177183

184+
if use_perch and sensitivity != 1.0:
185+
print("Warning: Sensitivity setting is ignored when using the Perch model.")
186+
178187
cfg.CODES = load_codes()
179-
cfg.LABELS = read_lines(labels_file if labels_file else cfg.LABELS_FILE)
180188
cfg.SKIP_EXISTING_RESULTS = skip_existing_results
181189
cfg.LOCATION_FILTER_THRESHOLD = sf_thresh
182190
cfg.TOP_N = top_n
@@ -192,6 +200,10 @@ def _set_params(
192200
cfg.COMBINE_RESULTS = combine_results
193201
cfg.BATCH_SIZE = bs
194202
cfg.ADDITIONAL_COLUMNS = additional_columns
203+
cfg.USE_PERCH = use_perch
204+
205+
if cfg.USE_PERCH and custom_classifier:
206+
raise ValueError("Selected custom classifier and Perch model, please select only one.")
195207

196208
if not output:
197209
if os.path.isfile(cfg.INPUT_PATH):
@@ -213,16 +225,52 @@ def _set_params(
213225
cfg.CPU_THREADS = 1
214226
cfg.TFLITE_THREADS = threads
215227

216-
if custom_classifier is not None:
228+
if cfg.USE_PERCH:
229+
cfg.MODEL_PATH = cfg.PERCH_V2_MODEL_PATH
230+
cfg.LABELS_FILE = cfg.PERCH_LABELS_FILE
231+
cfg.SAMPLE_RATE = cfg.PERCH_SAMPLE_RATE
232+
cfg.SIG_LENGTH = cfg.PERCH_SIG_LENGTH
233+
cfg.LABELS = read_lines(cfg.PERCH_LABELS_FILE)
234+
cfg.LABELS = cfg.LABELS[1:] # it's a csv with header
235+
else:
236+
cfg.MODEL_PATH = cfg.BIRDNET_MODEL_PATH
237+
cfg.LABELS_FILE = cfg.BIRDNET_LABELS_FILE
238+
cfg.SAMPLE_RATE = cfg.BIRDNET_SAMPLE_RATE
239+
cfg.SIG_LENGTH = cfg.BIRDNET_SIG_LENGTH
240+
cfg.LABELS = read_lines(labels_file if labels_file else cfg.LABELS_FILE)
241+
242+
if overlap >= cfg.SIG_LENGTH:
243+
raise ValueError(f"Overlap must be less than {cfg.SIG_LENGTH} seconds.")
244+
245+
# Custom classifier trained with the Analyzer, not arbitrary models, meaning; A a tflite model or B a raven model
246+
if custom_classifier is None:
247+
# TODO: does species list even make sense with Perch?
248+
cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK = lat, lon, week
249+
cfg.CUSTOM_CLASSIFIER = None
250+
251+
if cfg.LATITUDE == -1 and cfg.LONGITUDE == -1:
252+
if not slist:
253+
cfg.SPECIES_LIST_FILE = None
254+
else:
255+
cfg.SPECIES_LIST_FILE = slist
256+
257+
if os.path.isdir(cfg.SPECIES_LIST_FILE):
258+
cfg.SPECIES_LIST_FILE = os.path.join(cfg.SPECIES_LIST_FILE, "species_list.txt")
259+
260+
cfg.SPECIES_LIST = read_lines(cfg.SPECIES_LIST_FILE)
261+
else:
262+
cfg.SPECIES_LIST_FILE = None
263+
cfg.SPECIES_LIST = get_species_list(cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK, cfg.LOCATION_FILTER_THRESHOLD)
264+
else:
217265
cfg.CUSTOM_CLASSIFIER = custom_classifier # we treat this as absolute path, so no need to join with dirname
218266

219267
if custom_classifier.endswith(".tflite"):
220268
cfg.LABELS_FILE = custom_classifier.replace(".tflite", "_Labels.txt") # same for labels file
221269

222-
if not os.path.isfile(cfg.LABELS_FILE): # if the label file is not found, an old birdnet model might be used
270+
if not os.path.isfile(cfg.LABELS_FILE): # if the label file is not found, an old birdnet model might be used
223271
cfg.LABELS_FILE = custom_classifier.replace("Model_FP32.tflite", "Labels.txt")
224272

225-
if not os.path.isfile(cfg.LABELS_FILE): # if the label file is still not found, dont use labels
273+
if not os.path.isfile(cfg.LABELS_FILE): # if the label file is still not found, dont use labels
226274
cfg.LABELS_FILE = None
227275
cfg.LABELS = None
228276
else:
@@ -237,23 +285,6 @@ def _set_params(
237285
cfg.LABELS = read_lines(cfg.LABELS_FILE)
238286
else:
239287
cfg.LABELS = [line.split(",")[1] for line in read_lines(cfg.LABELS_FILE)]
240-
else:
241-
cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK = lat, lon, week
242-
cfg.CUSTOM_CLASSIFIER = None
243-
244-
if cfg.LATITUDE == -1 and cfg.LONGITUDE == -1:
245-
if not slist:
246-
cfg.SPECIES_LIST_FILE = None
247-
else:
248-
cfg.SPECIES_LIST_FILE = slist
249-
250-
if os.path.isdir(cfg.SPECIES_LIST_FILE):
251-
cfg.SPECIES_LIST_FILE = os.path.join(cfg.SPECIES_LIST_FILE, "species_list.txt")
252-
253-
cfg.SPECIES_LIST = read_lines(cfg.SPECIES_LIST_FILE)
254-
else:
255-
cfg.SPECIES_LIST_FILE = None
256-
cfg.SPECIES_LIST = get_species_list(cfg.LATITUDE, cfg.LONGITUDE, cfg.WEEK, cfg.LOCATION_FILTER_THRESHOLD)
257288

258289
if cfg.LABELS_FILE:
259290
lfile = os.path.join(cfg.TRANSLATED_LABELS_PATH, os.path.basename(cfg.LABELS_FILE).replace(".txt", f"_{locale}.txt"))

0 commit comments

Comments
 (0)