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