birdnet-analyzer 2.0.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- birdnet_analyzer/__init__.py +9 -8
- birdnet_analyzer/analyze/__init__.py +5 -5
- birdnet_analyzer/analyze/__main__.py +3 -4
- birdnet_analyzer/analyze/cli.py +25 -25
- birdnet_analyzer/analyze/core.py +241 -245
- birdnet_analyzer/analyze/utils.py +692 -701
- birdnet_analyzer/audio.py +368 -372
- birdnet_analyzer/cli.py +709 -707
- birdnet_analyzer/config.py +242 -242
- birdnet_analyzer/eBird_taxonomy_codes_2021E.json +25279 -25279
- birdnet_analyzer/embeddings/__init__.py +3 -4
- birdnet_analyzer/embeddings/__main__.py +3 -3
- birdnet_analyzer/embeddings/cli.py +12 -13
- birdnet_analyzer/embeddings/core.py +69 -70
- birdnet_analyzer/embeddings/utils.py +179 -193
- birdnet_analyzer/evaluation/__init__.py +196 -195
- birdnet_analyzer/evaluation/__main__.py +3 -3
- birdnet_analyzer/evaluation/assessment/__init__.py +0 -0
- birdnet_analyzer/evaluation/assessment/metrics.py +388 -0
- birdnet_analyzer/evaluation/assessment/performance_assessor.py +409 -0
- birdnet_analyzer/evaluation/assessment/plotting.py +379 -0
- birdnet_analyzer/evaluation/preprocessing/__init__.py +0 -0
- birdnet_analyzer/evaluation/preprocessing/data_processor.py +631 -0
- birdnet_analyzer/evaluation/preprocessing/utils.py +98 -0
- birdnet_analyzer/gui/__init__.py +19 -23
- birdnet_analyzer/gui/__main__.py +3 -3
- birdnet_analyzer/gui/analysis.py +175 -174
- birdnet_analyzer/gui/assets/arrow_down.svg +4 -4
- birdnet_analyzer/gui/assets/arrow_left.svg +4 -4
- birdnet_analyzer/gui/assets/arrow_right.svg +4 -4
- birdnet_analyzer/gui/assets/arrow_up.svg +4 -4
- birdnet_analyzer/gui/assets/gui.css +28 -28
- birdnet_analyzer/gui/assets/gui.js +93 -93
- birdnet_analyzer/gui/embeddings.py +619 -620
- birdnet_analyzer/gui/evaluation.py +795 -813
- birdnet_analyzer/gui/localization.py +75 -68
- birdnet_analyzer/gui/multi_file.py +245 -246
- birdnet_analyzer/gui/review.py +519 -527
- birdnet_analyzer/gui/segments.py +191 -191
- birdnet_analyzer/gui/settings.py +128 -129
- birdnet_analyzer/gui/single_file.py +267 -269
- birdnet_analyzer/gui/species.py +95 -95
- birdnet_analyzer/gui/train.py +696 -698
- birdnet_analyzer/gui/utils.py +810 -808
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_af.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ar.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_bg.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ca.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_cs.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_da.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_de.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_el.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_en_uk.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_es.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_fi.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_fr.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_he.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_hr.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_hu.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_in.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_is.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_it.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ja.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ko.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_lt.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ml.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_nl.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_no.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pl.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pt_BR.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pt_PT.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ro.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ru.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sk.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sl.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sr.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sv.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_th.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_tr.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_uk.txt +6522 -6522
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_zh.txt +6522 -6522
- birdnet_analyzer/lang/de.json +334 -334
- birdnet_analyzer/lang/en.json +334 -334
- birdnet_analyzer/lang/fi.json +334 -334
- birdnet_analyzer/lang/fr.json +334 -334
- birdnet_analyzer/lang/id.json +334 -334
- birdnet_analyzer/lang/pt-br.json +334 -334
- birdnet_analyzer/lang/ru.json +334 -334
- birdnet_analyzer/lang/se.json +334 -334
- birdnet_analyzer/lang/tlh.json +334 -334
- birdnet_analyzer/lang/zh_TW.json +334 -334
- birdnet_analyzer/model.py +1212 -1243
- birdnet_analyzer/playground.py +5 -0
- birdnet_analyzer/search/__init__.py +3 -3
- birdnet_analyzer/search/__main__.py +3 -3
- birdnet_analyzer/search/cli.py +11 -12
- birdnet_analyzer/search/core.py +78 -78
- birdnet_analyzer/search/utils.py +107 -111
- birdnet_analyzer/segments/__init__.py +3 -3
- birdnet_analyzer/segments/__main__.py +3 -3
- birdnet_analyzer/segments/cli.py +13 -14
- birdnet_analyzer/segments/core.py +81 -78
- birdnet_analyzer/segments/utils.py +383 -394
- birdnet_analyzer/species/__init__.py +3 -3
- birdnet_analyzer/species/__main__.py +3 -3
- birdnet_analyzer/species/cli.py +13 -14
- birdnet_analyzer/species/core.py +35 -35
- birdnet_analyzer/species/utils.py +74 -75
- birdnet_analyzer/train/__init__.py +3 -3
- birdnet_analyzer/train/__main__.py +3 -3
- birdnet_analyzer/train/cli.py +13 -14
- birdnet_analyzer/train/core.py +113 -113
- birdnet_analyzer/train/utils.py +877 -847
- birdnet_analyzer/translate.py +133 -104
- birdnet_analyzer/utils.py +426 -419
- {birdnet_analyzer-2.0.0.dist-info → birdnet_analyzer-2.0.1.dist-info}/METADATA +137 -129
- birdnet_analyzer-2.0.1.dist-info/RECORD +125 -0
- {birdnet_analyzer-2.0.0.dist-info → birdnet_analyzer-2.0.1.dist-info}/WHEEL +1 -1
- {birdnet_analyzer-2.0.0.dist-info → birdnet_analyzer-2.0.1.dist-info}/licenses/LICENSE +18 -18
- birdnet_analyzer-2.0.0.dist-info/RECORD +0 -117
- {birdnet_analyzer-2.0.0.dist-info → birdnet_analyzer-2.0.1.dist-info}/entry_points.txt +0 -0
- {birdnet_analyzer-2.0.0.dist-info → birdnet_analyzer-2.0.1.dist-info}/top_level.txt +0 -0
birdnet_analyzer/utils.py
CHANGED
@@ -1,419 +1,426 @@
|
|
1
|
-
"""Module containing common function."""
|
2
|
-
|
3
|
-
import
|
4
|
-
import
|
5
|
-
import
|
6
|
-
import traceback
|
7
|
-
from pathlib import Path
|
8
|
-
|
9
|
-
import birdnet_analyzer.config as cfg
|
10
|
-
|
11
|
-
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
|
12
|
-
FROZEN = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
|
13
|
-
|
14
|
-
|
15
|
-
def runtime_error_handler(f: callable):
|
16
|
-
"""Decorator to catch runtime errors and write them to the error log.
|
17
|
-
|
18
|
-
Args:
|
19
|
-
f: The function to be decorated.
|
20
|
-
|
21
|
-
Returns:
|
22
|
-
The decorated function.
|
23
|
-
"""
|
24
|
-
|
25
|
-
def wrapper(*args, **kwargs):
|
26
|
-
try:
|
27
|
-
return f(*args, **kwargs)
|
28
|
-
except Exception as ex:
|
29
|
-
write_error_log(ex)
|
30
|
-
raise
|
31
|
-
|
32
|
-
return wrapper
|
33
|
-
|
34
|
-
|
35
|
-
def batched(iterable, n, *, strict=False):
|
36
|
-
# TODO: Remove this function when Python 3.12 is the minimum version
|
37
|
-
# batched('ABCDEFG', 3) → ABC DEF G
|
38
|
-
if n < 1:
|
39
|
-
raise ValueError("n must be at least one")
|
40
|
-
iterator = iter(iterable)
|
41
|
-
while batch := tuple(itertools.islice(iterator, n)):
|
42
|
-
if strict and len(batch) != n:
|
43
|
-
raise ValueError("batched(): incomplete batch")
|
44
|
-
yield batch
|
45
|
-
|
46
|
-
|
47
|
-
def spectrogram_from_file(path, fig_num=None, fig_size=None, offset=0, duration=None, fmin=None, fmax=None, speed=1.0):
|
48
|
-
"""
|
49
|
-
Generate a spectrogram from an audio file.
|
50
|
-
|
51
|
-
Parameters:
|
52
|
-
path (str): The path to the audio file.
|
53
|
-
|
54
|
-
Returns:
|
55
|
-
matplotlib.figure.Figure: The generated spectrogram figure.
|
56
|
-
"""
|
57
|
-
|
58
|
-
|
59
|
-
# s, sr = librosa.load(path, offset=offset, duration=duration)
|
60
|
-
s, sr = audio.open_audio_file(path, offset=offset, duration=duration, fmin=fmin, fmax=fmax, speed=speed)
|
61
|
-
|
62
|
-
return spectrogram_from_audio(s, sr, fig_num, fig_size)
|
63
|
-
|
64
|
-
|
65
|
-
def spectrogram_from_audio(s, sr, fig_num=None, fig_size=None):
|
66
|
-
"""
|
67
|
-
Generate a spectrogram from an audio signal.
|
68
|
-
|
69
|
-
Parameters:
|
70
|
-
s: The signal
|
71
|
-
sr: The sample rate
|
72
|
-
|
73
|
-
Returns:
|
74
|
-
matplotlib.figure.Figure: The generated spectrogram figure.
|
75
|
-
"""
|
76
|
-
import librosa
|
77
|
-
import librosa.display
|
78
|
-
import matplotlib
|
79
|
-
import matplotlib.pyplot as plt
|
80
|
-
import numpy as np
|
81
|
-
|
82
|
-
matplotlib.use("agg")
|
83
|
-
|
84
|
-
if isinstance(fig_size, tuple):
|
85
|
-
f = plt.figure(fig_num, figsize=fig_size)
|
86
|
-
elif fig_size == "auto":
|
87
|
-
duration = librosa.get_duration(y=s, sr=sr)
|
88
|
-
width = min(12, max(3, duration / 10))
|
89
|
-
f = plt.figure(fig_num, figsize=(width, 3))
|
90
|
-
else:
|
91
|
-
f = plt.figure(fig_num)
|
92
|
-
|
93
|
-
f.clf()
|
94
|
-
|
95
|
-
ax = f.add_subplot(111)
|
96
|
-
|
97
|
-
ax.set_axis_off()
|
98
|
-
f.tight_layout(pad=0)
|
99
|
-
|
100
|
-
D = librosa.stft(s, n_fft=1024, hop_length=512) # STFT of y
|
101
|
-
S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
|
102
|
-
|
103
|
-
return librosa.display.specshow(S_db, ax=ax, n_fft=1024, hop_length=512).figure
|
104
|
-
|
105
|
-
|
106
|
-
def collect_audio_files(path: str, max_files: int = None):
|
107
|
-
"""Collects all audio files in the given directory.
|
108
|
-
|
109
|
-
Args:
|
110
|
-
path: The directory to be searched.
|
111
|
-
|
112
|
-
Returns:
|
113
|
-
A sorted list of all audio files in the directory.
|
114
|
-
"""
|
115
|
-
# Get all files in directory with os.walk
|
116
|
-
files = []
|
117
|
-
|
118
|
-
for root, _, flist in os.walk(path):
|
119
|
-
for f in flist:
|
120
|
-
if not f.startswith(".") and f.rsplit(".", 1)[-1].lower() in cfg.ALLOWED_FILETYPES:
|
121
|
-
files.append(os.path.join(root, f))
|
122
|
-
|
123
|
-
if max_files and len(files) >= max_files:
|
124
|
-
return sorted(files)
|
125
|
-
|
126
|
-
return sorted(files)
|
127
|
-
|
128
|
-
|
129
|
-
def collect_all_files(path: str, filetypes: list[str], pattern: str = ""):
|
130
|
-
"""Collects all files of the given filetypes in the given directory.
|
131
|
-
|
132
|
-
Args:
|
133
|
-
path: The directory to be searched.
|
134
|
-
filetypes: A list of filetypes to be collected.
|
135
|
-
|
136
|
-
Returns:
|
137
|
-
A sorted list of all files in the directory.
|
138
|
-
"""
|
139
|
-
|
140
|
-
files = []
|
141
|
-
|
142
|
-
for root, _, flist in os.walk(path):
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
""
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
"""
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
"BirdNET_GLOBAL_6K_V2.
|
340
|
-
"BirdNET_GLOBAL_6K_V2.
|
341
|
-
"BirdNET_GLOBAL_6K_V2.
|
342
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-
|
343
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-
|
344
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-
|
345
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/
|
346
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
347
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
348
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
349
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
350
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/
|
351
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
352
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
353
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
354
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
355
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
356
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
357
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
358
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-
|
359
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/
|
360
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/
|
361
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/
|
362
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/
|
363
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/
|
364
|
-
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/
|
365
|
-
"BirdNET_GLOBAL_6K_V2.
|
366
|
-
"BirdNET_GLOBAL_6K_V2.
|
367
|
-
"BirdNET_GLOBAL_6K_V2.
|
368
|
-
"BirdNET_GLOBAL_6K_V2.
|
369
|
-
"BirdNET_GLOBAL_6K_V2.
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
1
|
+
"""Module containing common function."""
|
2
|
+
|
3
|
+
import itertools
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
import traceback
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
import birdnet_analyzer.config as cfg
|
10
|
+
|
11
|
+
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
|
12
|
+
FROZEN = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
|
13
|
+
|
14
|
+
|
15
|
+
def runtime_error_handler(f: callable):
|
16
|
+
"""Decorator to catch runtime errors and write them to the error log.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
f: The function to be decorated.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
The decorated function.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def wrapper(*args, **kwargs):
|
26
|
+
try:
|
27
|
+
return f(*args, **kwargs)
|
28
|
+
except Exception as ex:
|
29
|
+
write_error_log(ex)
|
30
|
+
raise
|
31
|
+
|
32
|
+
return wrapper
|
33
|
+
|
34
|
+
|
35
|
+
def batched(iterable, n, *, strict=False):
|
36
|
+
# TODO: Remove this function when Python 3.12 is the minimum version
|
37
|
+
# batched('ABCDEFG', 3) → ABC DEF G
|
38
|
+
if n < 1:
|
39
|
+
raise ValueError("n must be at least one")
|
40
|
+
iterator = iter(iterable)
|
41
|
+
while batch := tuple(itertools.islice(iterator, n)):
|
42
|
+
if strict and len(batch) != n:
|
43
|
+
raise ValueError("batched(): incomplete batch")
|
44
|
+
yield batch
|
45
|
+
|
46
|
+
|
47
|
+
def spectrogram_from_file(path, fig_num=None, fig_size=None, offset=0, duration=None, fmin=None, fmax=None, speed=1.0):
|
48
|
+
"""
|
49
|
+
Generate a spectrogram from an audio file.
|
50
|
+
|
51
|
+
Parameters:
|
52
|
+
path (str): The path to the audio file.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
matplotlib.figure.Figure: The generated spectrogram figure.
|
56
|
+
"""
|
57
|
+
from birdnet_analyzer import audio
|
58
|
+
|
59
|
+
# s, sr = librosa.load(path, offset=offset, duration=duration)
|
60
|
+
s, sr = audio.open_audio_file(path, offset=offset, duration=duration, fmin=fmin, fmax=fmax, speed=speed)
|
61
|
+
|
62
|
+
return spectrogram_from_audio(s, sr, fig_num, fig_size)
|
63
|
+
|
64
|
+
|
65
|
+
def spectrogram_from_audio(s, sr, fig_num=None, fig_size=None):
|
66
|
+
"""
|
67
|
+
Generate a spectrogram from an audio signal.
|
68
|
+
|
69
|
+
Parameters:
|
70
|
+
s: The signal
|
71
|
+
sr: The sample rate
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
matplotlib.figure.Figure: The generated spectrogram figure.
|
75
|
+
"""
|
76
|
+
import librosa
|
77
|
+
import librosa.display
|
78
|
+
import matplotlib
|
79
|
+
import matplotlib.pyplot as plt
|
80
|
+
import numpy as np
|
81
|
+
|
82
|
+
matplotlib.use("agg")
|
83
|
+
|
84
|
+
if isinstance(fig_size, tuple):
|
85
|
+
f = plt.figure(fig_num, figsize=fig_size)
|
86
|
+
elif fig_size == "auto":
|
87
|
+
duration = librosa.get_duration(y=s, sr=sr)
|
88
|
+
width = min(12, max(3, duration / 10))
|
89
|
+
f = plt.figure(fig_num, figsize=(width, 3))
|
90
|
+
else:
|
91
|
+
f = plt.figure(fig_num)
|
92
|
+
|
93
|
+
f.clf()
|
94
|
+
|
95
|
+
ax = f.add_subplot(111)
|
96
|
+
|
97
|
+
ax.set_axis_off()
|
98
|
+
f.tight_layout(pad=0)
|
99
|
+
|
100
|
+
D = librosa.stft(s, n_fft=1024, hop_length=512) # STFT of y
|
101
|
+
S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
|
102
|
+
|
103
|
+
return librosa.display.specshow(S_db, ax=ax, n_fft=1024, hop_length=512).figure
|
104
|
+
|
105
|
+
|
106
|
+
def collect_audio_files(path: str, max_files: int | None = None):
|
107
|
+
"""Collects all audio files in the given directory.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
path: The directory to be searched.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
A sorted list of all audio files in the directory.
|
114
|
+
"""
|
115
|
+
# Get all files in directory with os.walk
|
116
|
+
files = []
|
117
|
+
|
118
|
+
for root, _, flist in os.walk(path):
|
119
|
+
for f in flist:
|
120
|
+
if not f.startswith(".") and f.rsplit(".", 1)[-1].lower() in cfg.ALLOWED_FILETYPES:
|
121
|
+
files.append(os.path.join(root, f))
|
122
|
+
|
123
|
+
if max_files and len(files) >= max_files:
|
124
|
+
return sorted(files)
|
125
|
+
|
126
|
+
return sorted(files)
|
127
|
+
|
128
|
+
|
129
|
+
def collect_all_files(path: str, filetypes: list[str], pattern: str = ""):
|
130
|
+
"""Collects all files of the given filetypes in the given directory.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
path: The directory to be searched.
|
134
|
+
filetypes: A list of filetypes to be collected.
|
135
|
+
|
136
|
+
Returns:
|
137
|
+
A sorted list of all files in the directory.
|
138
|
+
"""
|
139
|
+
|
140
|
+
files = []
|
141
|
+
|
142
|
+
for root, _, flist in os.walk(path):
|
143
|
+
files.extend(
|
144
|
+
os.path.join(root, f)
|
145
|
+
for f in flist
|
146
|
+
if not f.startswith(".") and f.rsplit(".", 1)[-1].lower() in filetypes and (pattern in f or not pattern)
|
147
|
+
)
|
148
|
+
|
149
|
+
return sorted(files)
|
150
|
+
|
151
|
+
|
152
|
+
def read_lines(path: str):
|
153
|
+
"""Reads the lines into a list.
|
154
|
+
|
155
|
+
Opens the file and reads its contents into a list.
|
156
|
+
It is expected to have one line for each species or label.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
path: Absolute path to the species file.
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
A list of all species inside the file.
|
163
|
+
"""
|
164
|
+
return Path(path).read_text(encoding="utf-8").splitlines() if path else []
|
165
|
+
|
166
|
+
|
167
|
+
def list_subdirectories(path: str):
|
168
|
+
"""Lists all directories inside a path.
|
169
|
+
|
170
|
+
Retrieves all the subdirectories in a given path without recursion.
|
171
|
+
|
172
|
+
Args:
|
173
|
+
path: Directory to be searched.
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
A filter sequence containing the absolute paths to all directories.
|
177
|
+
"""
|
178
|
+
return filter(lambda el: os.path.isdir(os.path.join(path, el)), os.listdir(path))
|
179
|
+
|
180
|
+
|
181
|
+
def save_to_cache(path, x_train, y_train, x_test, y_test, labels):
|
182
|
+
"""Saves training data to cache.
|
183
|
+
|
184
|
+
Args:
|
185
|
+
path: Path to the cache file.
|
186
|
+
x_train: Training samples.
|
187
|
+
y_train: Training labels.
|
188
|
+
x_test: Test samples.
|
189
|
+
y_test: Test labels.
|
190
|
+
labels: Labels.
|
191
|
+
"""
|
192
|
+
import numpy as np
|
193
|
+
|
194
|
+
# Make directory if needed
|
195
|
+
directory = os.path.dirname(path)
|
196
|
+
if directory and not os.path.exists(directory):
|
197
|
+
os.makedirs(directory)
|
198
|
+
|
199
|
+
# Save cache file with training data, test data, labels and configuration
|
200
|
+
np.savez(
|
201
|
+
path,
|
202
|
+
x_train=x_train,
|
203
|
+
y_train=y_train,
|
204
|
+
x_test=x_test,
|
205
|
+
y_test=y_test,
|
206
|
+
labels=np.array(labels, dtype=object),
|
207
|
+
binary_classification=cfg.BINARY_CLASSIFICATION,
|
208
|
+
multi_label=cfg.MULTI_LABEL,
|
209
|
+
fmin=cfg.BANDPASS_FMIN,
|
210
|
+
fmax=cfg.BANDPASS_FMAX,
|
211
|
+
audio_speed=cfg.AUDIO_SPEED,
|
212
|
+
crop_mode=cfg.SAMPLE_CROP_MODE,
|
213
|
+
overlap=cfg.SIG_OVERLAP,
|
214
|
+
)
|
215
|
+
|
216
|
+
|
217
|
+
def load_from_cache(path):
|
218
|
+
"""Loads training data from cache.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
path: Path to the cache file.
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
A tuple of (x_train, y_train, labels, binary_classification, multi_label).
|
225
|
+
"""
|
226
|
+
import numpy as np
|
227
|
+
|
228
|
+
# Load cache file
|
229
|
+
data = np.load(path, allow_pickle=True)
|
230
|
+
|
231
|
+
# Check if cache contains needed preprocessing parameters
|
232
|
+
if (
|
233
|
+
"fmin" in data
|
234
|
+
and "fmax" in data
|
235
|
+
and "audio_speed" in data
|
236
|
+
and "crop_mode" in data
|
237
|
+
and "overlap" in data
|
238
|
+
and ( # Check if preprocessing parameters match current settings
|
239
|
+
data["fmin"] != cfg.BANDPASS_FMIN
|
240
|
+
or data["fmax"] != cfg.BANDPASS_FMAX
|
241
|
+
or data["audio_speed"] != cfg.AUDIO_SPEED
|
242
|
+
or data["crop_mode"] != cfg.SAMPLE_CROP_MODE
|
243
|
+
or data["overlap"] != cfg.SIG_OVERLAP
|
244
|
+
)
|
245
|
+
):
|
246
|
+
print("\t...WARNING: Cache preprocessing parameters don't match current settings!", flush=True)
|
247
|
+
print(f"\t Cache: fmin={data['fmin']}, fmax={data['fmax']}, speed={data['audio_speed']}", flush=True)
|
248
|
+
print(f"\t Cache: crop_mode={data['crop_mode']}, overlap={data['overlap']}", flush=True)
|
249
|
+
print(f"\t Current: fmin={cfg.BANDPASS_FMIN}, fmax={cfg.BANDPASS_FMAX}, speed={cfg.AUDIO_SPEED}", flush=True)
|
250
|
+
print(f"\t Current: crop_mode={cfg.SAMPLE_CROP_MODE}, overlap={cfg.SIG_OVERLAP}", flush=True)
|
251
|
+
|
252
|
+
# Extract and return data
|
253
|
+
x_train = data["x_train"]
|
254
|
+
y_train = data["y_train"]
|
255
|
+
|
256
|
+
# Handle test data which might not be in older cache files
|
257
|
+
x_test = data.get("x_test", np.array([]))
|
258
|
+
y_test = data.get("y_test", np.array([]))
|
259
|
+
|
260
|
+
labels = data["labels"]
|
261
|
+
binary_classification = bool(data.get("binary_classification", False))
|
262
|
+
multi_label = bool(data.get("multi_label", False))
|
263
|
+
|
264
|
+
return x_train, y_train, x_test, y_test, labels, binary_classification, multi_label
|
265
|
+
|
266
|
+
|
267
|
+
def clear_error_log():
|
268
|
+
"""Clears the error log file.
|
269
|
+
|
270
|
+
For debugging purposes.
|
271
|
+
"""
|
272
|
+
if os.path.isfile(cfg.ERROR_LOG_FILE):
|
273
|
+
os.remove(cfg.ERROR_LOG_FILE)
|
274
|
+
|
275
|
+
|
276
|
+
def write_error_log(ex: Exception):
|
277
|
+
"""Writes an exception to the error log.
|
278
|
+
|
279
|
+
Formats the stacktrace and writes it in the error log file configured in the config.
|
280
|
+
|
281
|
+
Args:
|
282
|
+
ex: An exception that occurred.
|
283
|
+
"""
|
284
|
+
import datetime
|
285
|
+
|
286
|
+
with open(cfg.ERROR_LOG_FILE, "a") as elog:
|
287
|
+
elog.write(
|
288
|
+
datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
|
289
|
+
+ "\n"
|
290
|
+
+ "".join(traceback.TracebackException.from_exception(ex).format())
|
291
|
+
+ "\n"
|
292
|
+
)
|
293
|
+
|
294
|
+
|
295
|
+
def img2base64(path):
|
296
|
+
import base64
|
297
|
+
|
298
|
+
with open(path, "rb") as img_file:
|
299
|
+
return base64.b64encode(img_file.read()).decode("utf-8")
|
300
|
+
|
301
|
+
|
302
|
+
def save_params(file_path, headers, values):
|
303
|
+
"""Saves the params used to train the custom classifier.
|
304
|
+
|
305
|
+
The hyperparams will be saved to disk in a file named 'model_params.csv'.
|
306
|
+
|
307
|
+
Args:
|
308
|
+
file_path: The path to the file.
|
309
|
+
headers: The headers of the csv file.
|
310
|
+
values: The values of the csv file.
|
311
|
+
"""
|
312
|
+
import csv
|
313
|
+
|
314
|
+
with open(file_path, "w", newline="") as paramsfile:
|
315
|
+
paramswriter = csv.writer(paramsfile)
|
316
|
+
paramswriter.writerow(headers)
|
317
|
+
paramswriter.writerow(values)
|
318
|
+
|
319
|
+
|
320
|
+
def save_result_file(result_path: str, out_string: str):
|
321
|
+
"""Saves the result to a file.
|
322
|
+
|
323
|
+
Args:
|
324
|
+
result_path: The path to the result file.
|
325
|
+
out_string: The string to be written to the file.
|
326
|
+
"""
|
327
|
+
|
328
|
+
# Make directory if it doesn't exist
|
329
|
+
os.makedirs(os.path.dirname(result_path), exist_ok=True)
|
330
|
+
|
331
|
+
# Write the result to the file
|
332
|
+
with open(result_path, "w", encoding="utf-8") as rfile:
|
333
|
+
rfile.write(out_string)
|
334
|
+
|
335
|
+
|
336
|
+
def check_model_files():
|
337
|
+
checkpoint_dir = os.path.join(SCRIPT_DIR, "checkpoints", "V2.4")
|
338
|
+
required_files = [
|
339
|
+
"BirdNET_GLOBAL_6K_V2.4_Model/variables/variables.data-00000-of-00001",
|
340
|
+
"BirdNET_GLOBAL_6K_V2.4_Model/variables/variables.index",
|
341
|
+
"BirdNET_GLOBAL_6K_V2.4_Model/saved_model.pb",
|
342
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-shard1of8.bin",
|
343
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-shard2of8.bin",
|
344
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-shard3of8.bin",
|
345
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-shard4of8.bin",
|
346
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-shard5of8.bin",
|
347
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-shard6of8.bin",
|
348
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-shard7of8.bin",
|
349
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/group1-shard8of8.bin",
|
350
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/mdata/model.json",
|
351
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard1of13.bin",
|
352
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard2of13.bin",
|
353
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard3of13.bin",
|
354
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard4of13.bin",
|
355
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard5of13.bin",
|
356
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard6of13.bin",
|
357
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard7of13.bin",
|
358
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard8of13.bin",
|
359
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard9of13.bin",
|
360
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard10of13.bin",
|
361
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard11of13.bin",
|
362
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard12of13.bin",
|
363
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/group1-shard13of13.bin",
|
364
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/model.json",
|
365
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/model/labels.json",
|
366
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/main.js",
|
367
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/static/sample.wav",
|
368
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/templates/index.html",
|
369
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_TFJS/app.py",
|
370
|
+
"BirdNET_GLOBAL_6K_V2.4_Labels.txt",
|
371
|
+
"BirdNET_GLOBAL_6K_V2.4_MData_Model_V2_FP16.tflite",
|
372
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_FP16.tflite",
|
373
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_FP32.tflite",
|
374
|
+
"BirdNET_GLOBAL_6K_V2.4_Model_INT8.tflite",
|
375
|
+
]
|
376
|
+
|
377
|
+
for file in required_files:
|
378
|
+
if not os.path.exists(os.path.join(checkpoint_dir, file)):
|
379
|
+
print(f"Missing {file}")
|
380
|
+
|
381
|
+
return False
|
382
|
+
|
383
|
+
print("Model found!")
|
384
|
+
|
385
|
+
return True
|
386
|
+
|
387
|
+
|
388
|
+
def ensure_model_exists():
|
389
|
+
import zipfile
|
390
|
+
|
391
|
+
import requests
|
392
|
+
from tqdm import tqdm
|
393
|
+
|
394
|
+
if FROZEN or check_model_files():
|
395
|
+
return
|
396
|
+
|
397
|
+
checkpoint_dir = os.path.join(SCRIPT_DIR, "checkpoints")
|
398
|
+
|
399
|
+
os.makedirs(checkpoint_dir, exist_ok=True)
|
400
|
+
|
401
|
+
url = "https://tuc.cloud/index.php/s/3BsizWy5M7CtQ5w/download/V2.4.zip"
|
402
|
+
download_path = os.path.join(checkpoint_dir, "V2.4.zip")
|
403
|
+
|
404
|
+
response = requests.get(url, stream=True, timeout=30)
|
405
|
+
total_size = int(response.headers.get("content-length", 0))
|
406
|
+
block_size = 1024
|
407
|
+
|
408
|
+
with (
|
409
|
+
tqdm(total=total_size, unit="iB", unit_scale=True, desc="Downloading model") as tqdm_bar,
|
410
|
+
open(download_path, "wb") as file,
|
411
|
+
):
|
412
|
+
for data in response.iter_content(block_size):
|
413
|
+
tqdm_bar.update(len(data))
|
414
|
+
file.write(data)
|
415
|
+
|
416
|
+
if response.status_code != 200 or (total_size not in (0, tqdm_bar.n)):
|
417
|
+
raise ValueError(f"Failed to download the file. Status code: {response.status_code}")
|
418
|
+
|
419
|
+
with zipfile.ZipFile(download_path, "r") as zip_ref:
|
420
|
+
zip_ref.extractall(os.path.dirname(download_path))
|
421
|
+
|
422
|
+
os.remove(download_path)
|
423
|
+
|
424
|
+
|
425
|
+
if __name__ == "__main__":
|
426
|
+
ensure_model_exists()
|