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