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
@@ -1,394 +1,383 @@
|
|
1
|
-
"""Extract segments from audio files based on BirdNET detections.
|
2
|
-
|
3
|
-
Can be used to save the segments of the audio files for each detection.
|
4
|
-
"""
|
5
|
-
|
6
|
-
import os
|
7
|
-
|
8
|
-
import numpy as np
|
9
|
-
|
10
|
-
import birdnet_analyzer.
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
""
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
afile =
|
234
|
-
|
235
|
-
elif rtype == "
|
236
|
-
d = line.split(",")
|
237
|
-
start = float(d[header_mapping["
|
238
|
-
end = float(d[header_mapping["
|
239
|
-
species = d[header_mapping["
|
240
|
-
confidence = float(d[header_mapping["
|
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
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
)
|
385
|
-
seg_path = os.path.join(outpath, seg_name)
|
386
|
-
audio.save_signal(seg_sig, seg_path, rate)
|
387
|
-
|
388
|
-
except Exception as ex:
|
389
|
-
# Write error log
|
390
|
-
print(f"Error: Cannot extract segments from {afile}.", flush=True)
|
391
|
-
utils.write_error_log(ex)
|
392
|
-
return False
|
393
|
-
|
394
|
-
return True
|
1
|
+
"""Extract segments from audio files based on BirdNET detections.
|
2
|
+
|
3
|
+
Can be used to save the segments of the audio files for each detection.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import os
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
|
10
|
+
import birdnet_analyzer.config as cfg
|
11
|
+
from birdnet_analyzer import audio, utils
|
12
|
+
|
13
|
+
# Set numpy random seed
|
14
|
+
RNG = np.random.default_rng(cfg.RANDOM_SEED)
|
15
|
+
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
|
16
|
+
|
17
|
+
|
18
|
+
def detect_rtype(line: str):
|
19
|
+
"""Detects the type of result file.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
line: First line of text.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
Either "table", "kaleidoscope", "csv" or "audacity".
|
26
|
+
"""
|
27
|
+
if line.lower().startswith("selection"):
|
28
|
+
return "table"
|
29
|
+
|
30
|
+
if line.lower().startswith("indir"):
|
31
|
+
return "kaleidoscope"
|
32
|
+
|
33
|
+
if line.lower().startswith("start (s)"):
|
34
|
+
return "csv"
|
35
|
+
|
36
|
+
return "audacity"
|
37
|
+
|
38
|
+
|
39
|
+
def get_header_mapping(line: str) -> dict:
|
40
|
+
"""
|
41
|
+
Parses a header line and returns a mapping of column names to their indices.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
line (str): A string representing the header line of a file.
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
dict: A dictionary where the keys are column names and the values are their respective indices.
|
48
|
+
"""
|
49
|
+
rtype = detect_rtype(line)
|
50
|
+
|
51
|
+
sep = "\t" if rtype in ("table", "audacity") else ","
|
52
|
+
|
53
|
+
cols = line.split(sep)
|
54
|
+
|
55
|
+
return {col: i for i, col in enumerate(cols)}
|
56
|
+
|
57
|
+
|
58
|
+
def parse_folders(apath: str, rpath: str, allowed_result_filetypes: tuple[str] = ("txt", "csv")) -> list[dict]:
|
59
|
+
"""Read audio and result files.
|
60
|
+
|
61
|
+
Reads all audio files and BirdNET output inside directory recursively.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
apath (str): Path to search for audio files.
|
65
|
+
rpath (str): Path to search for result files.
|
66
|
+
allowed_result_filetypes (tuple[str]): List of extensions for the result files.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
list[dict]: A list of {"audio": path_to_audio, "result": path_to_result }.
|
70
|
+
"""
|
71
|
+
data = {}
|
72
|
+
apath = apath.replace("/", os.sep).replace("\\", os.sep)
|
73
|
+
rpath = rpath.replace("/", os.sep).replace("\\", os.sep)
|
74
|
+
|
75
|
+
# Check if combined selection table is present and read that.
|
76
|
+
if os.path.exists(os.path.join(rpath, cfg.OUTPUT_RAVEN_FILENAME)):
|
77
|
+
# Read combined Raven selection table
|
78
|
+
rfile = os.path.join(rpath, cfg.OUTPUT_RAVEN_FILENAME)
|
79
|
+
data["combined"] = {"isCombinedFile": True, "result": rfile}
|
80
|
+
elif os.path.exists(os.path.join(rpath, cfg.OUTPUT_CSV_FILENAME)):
|
81
|
+
rfile = os.path.join(rpath, cfg.OUTPUT_CSV_FILENAME)
|
82
|
+
data["combined"] = {"isCombinedFile": True, "result": rfile}
|
83
|
+
elif os.path.exists(os.path.join(rpath, cfg.OUTPUT_KALEIDOSCOPE_FILENAME)):
|
84
|
+
rfile = os.path.join(rpath, cfg.OUTPUT_KALEIDOSCOPE_FILENAME)
|
85
|
+
data["combined"] = {"isCombinedFile": True, "result": rfile}
|
86
|
+
else:
|
87
|
+
# Get all audio files
|
88
|
+
for root, _, files in os.walk(apath):
|
89
|
+
for f in files:
|
90
|
+
if f.rsplit(".", 1)[-1].lower() in cfg.ALLOWED_FILETYPES and not f.startswith("."):
|
91
|
+
table_key = os.path.join(root.strip(apath), f.rsplit(".", 1)[0])
|
92
|
+
data[table_key] = {"audio": os.path.join(root, f), "result": ""}
|
93
|
+
|
94
|
+
# Get all result files
|
95
|
+
for root, _, files in os.walk(rpath):
|
96
|
+
for f in files:
|
97
|
+
if f.rsplit(".", 1)[-1] in allowed_result_filetypes and ".BirdNET." in f:
|
98
|
+
table_key = os.path.join(root.strip(rpath), f.split(".BirdNET.", 1)[0])
|
99
|
+
if table_key in data:
|
100
|
+
data[table_key]["result"] = os.path.join(root, f)
|
101
|
+
|
102
|
+
# Convert to list
|
103
|
+
flist = [f for f in data.values() if f["result"]]
|
104
|
+
|
105
|
+
print(f"Found {len(flist)} audio files with valid result file.")
|
106
|
+
|
107
|
+
return flist
|
108
|
+
|
109
|
+
|
110
|
+
def parse_files(flist: list[dict], max_segments=100):
|
111
|
+
"""
|
112
|
+
Parses a list of files to extract and organize bird call segments by species.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
flist (list[dict]): A list of dictionaries, each containing 'audio' and 'result' file paths.
|
116
|
+
Optionally, a dictionary can have 'isCombinedFile' set to True to indicate
|
117
|
+
that it is a combined result file.
|
118
|
+
max_segments (int, optional): The maximum number of segments to retain per species. Defaults to 100.
|
119
|
+
Returns:
|
120
|
+
list[tuple]: A list of tuples where each tuple contains an audio file path and a list of segments
|
121
|
+
associated with that audio file.
|
122
|
+
Raises:
|
123
|
+
KeyError: If the dictionaries in flist do not contain the required keys ('audio' and 'result').
|
124
|
+
Example:
|
125
|
+
flist = [
|
126
|
+
{"audio": "path/to/audio1.wav", "result": "path/to/result1.csv"},
|
127
|
+
{"audio": "path/to/audio2.wav", "result": "path/to/result2.csv"}
|
128
|
+
]
|
129
|
+
segments = parseFiles(flist, max_segments=50)
|
130
|
+
"""
|
131
|
+
species_segments: dict[str, list] = {}
|
132
|
+
|
133
|
+
is_combined_rfile = len(flist) == 1 and flist[0].get("isCombinedFile", False)
|
134
|
+
|
135
|
+
if is_combined_rfile:
|
136
|
+
rfile = flist[0]["result"]
|
137
|
+
segments = find_segments_from_combined(rfile)
|
138
|
+
|
139
|
+
# Parse segments by species
|
140
|
+
for s in segments:
|
141
|
+
if s["species"] not in species_segments:
|
142
|
+
species_segments[s["species"]] = []
|
143
|
+
|
144
|
+
species_segments[s["species"]].append(s)
|
145
|
+
else:
|
146
|
+
for f in flist:
|
147
|
+
# Paths
|
148
|
+
afile = f["audio"]
|
149
|
+
rfile = f["result"]
|
150
|
+
|
151
|
+
# Get all segments for result file
|
152
|
+
segments = find_segments(afile, rfile)
|
153
|
+
|
154
|
+
# Parse segments by species
|
155
|
+
for s in segments:
|
156
|
+
if s["species"] not in species_segments:
|
157
|
+
species_segments[s["species"]] = []
|
158
|
+
|
159
|
+
species_segments[s["species"]].append(s)
|
160
|
+
|
161
|
+
# Shuffle segments for each species and limit to max_segments
|
162
|
+
for s in species_segments:
|
163
|
+
RNG.shuffle(species_segments[s])
|
164
|
+
species_segments[s] = species_segments[s][:max_segments]
|
165
|
+
|
166
|
+
# Make dict of segments per audio file
|
167
|
+
segments: dict[str, list] = {}
|
168
|
+
seg_cnt = 0
|
169
|
+
|
170
|
+
for s in species_segments:
|
171
|
+
for seg in species_segments[s]:
|
172
|
+
if seg["audio"] not in segments:
|
173
|
+
segments[seg["audio"]] = []
|
174
|
+
|
175
|
+
segments[seg["audio"]].append(seg)
|
176
|
+
seg_cnt += 1
|
177
|
+
|
178
|
+
print(f"Found {seg_cnt} segments in {len(segments)} audio files.")
|
179
|
+
|
180
|
+
# Convert to list
|
181
|
+
return [tuple(e) for e in segments.items()]
|
182
|
+
|
183
|
+
|
184
|
+
def find_segments_from_combined(rfile: str) -> list[dict]:
|
185
|
+
"""Extracts the segments from a combined results file
|
186
|
+
|
187
|
+
Args:
|
188
|
+
rfile (str): Path to the result file.
|
189
|
+
|
190
|
+
Returns:
|
191
|
+
list[dict]: A list of dicts in the form of
|
192
|
+
{"audio": afile, "start": start, "end": end, "species": species, "confidence": confidence}
|
193
|
+
"""
|
194
|
+
segments: list[dict] = []
|
195
|
+
|
196
|
+
# Open and parse result file
|
197
|
+
lines = utils.read_lines(rfile)
|
198
|
+
|
199
|
+
# Auto-detect result type
|
200
|
+
rtype = detect_rtype(lines[0])
|
201
|
+
|
202
|
+
if rtype == "audacity":
|
203
|
+
raise Exception("Audacity files are not supported for combined results.")
|
204
|
+
|
205
|
+
# Get mapping from the header column
|
206
|
+
header_mapping = get_header_mapping(lines[0])
|
207
|
+
|
208
|
+
# Get start and end times based on rtype
|
209
|
+
confidence = 0
|
210
|
+
start = end = 0.0
|
211
|
+
species = ""
|
212
|
+
afile = ""
|
213
|
+
|
214
|
+
for i, line in enumerate(lines):
|
215
|
+
if rtype == "table" and i > 0:
|
216
|
+
d = line.split("\t")
|
217
|
+
file_offset = float(d[header_mapping["File Offset (s)"]])
|
218
|
+
start = file_offset
|
219
|
+
end = file_offset + (float(d[header_mapping["End Time (s)"]]) - float(d[header_mapping["Begin Time (s)"]]))
|
220
|
+
species = d[header_mapping["Common Name"]]
|
221
|
+
confidence = float(d[header_mapping["Confidence"]])
|
222
|
+
afile = d[header_mapping["Begin Path"]].replace("/", os.sep).replace("\\", os.sep)
|
223
|
+
|
224
|
+
elif rtype == "kaleidoscope" and i > 0:
|
225
|
+
d = line.split(",")
|
226
|
+
start = float(d[header_mapping["OFFSET"]])
|
227
|
+
end = float(d[header_mapping["DURATION"]]) + start
|
228
|
+
species = d[header_mapping["scientific_name"]]
|
229
|
+
confidence = float(d[header_mapping["confidence"]])
|
230
|
+
in_dir = d[header_mapping["INDIR"]]
|
231
|
+
folder = d[header_mapping["FOLDER"]]
|
232
|
+
in_file = d[header_mapping["IN FILE"]]
|
233
|
+
afile = os.path.join(in_dir, folder, in_file).replace("/", os.sep).replace("\\", os.sep)
|
234
|
+
|
235
|
+
elif rtype == "csv" and i > 0:
|
236
|
+
d = line.split(",")
|
237
|
+
start = float(d[header_mapping["Start (s)"]])
|
238
|
+
end = float(d[header_mapping["End (s)"]])
|
239
|
+
species = d[header_mapping["Common name"]]
|
240
|
+
confidence = float(d[header_mapping["Confidence"]])
|
241
|
+
afile = d[header_mapping["File"]].replace("/", os.sep).replace("\\", os.sep)
|
242
|
+
|
243
|
+
# Check if confidence is high enough and label is not "nocall"
|
244
|
+
if confidence >= cfg.MIN_CONFIDENCE and species.lower() != "nocall" and afile:
|
245
|
+
segments.append({"audio": afile, "start": start, "end": end, "species": species, "confidence": confidence})
|
246
|
+
|
247
|
+
return segments
|
248
|
+
|
249
|
+
|
250
|
+
def find_segments(afile: str, rfile: str):
|
251
|
+
"""Extracts the segments for an audio file from the results file
|
252
|
+
|
253
|
+
Args:
|
254
|
+
afile: Path to the audio file.
|
255
|
+
rfile: Path to the result file.
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
A list of dicts in the form of
|
259
|
+
{"audio": afile, "start": start, "end": end, "species": species, "confidence": confidence}
|
260
|
+
"""
|
261
|
+
segments: list[dict] = []
|
262
|
+
|
263
|
+
# Open and parse result file
|
264
|
+
lines = utils.read_lines(rfile)
|
265
|
+
|
266
|
+
# Auto-detect result type
|
267
|
+
rtype = detect_rtype(lines[0])
|
268
|
+
|
269
|
+
# Get mapping from the header column
|
270
|
+
header_mapping = get_header_mapping(lines[0])
|
271
|
+
|
272
|
+
# Get start and end times based on rtype
|
273
|
+
confidence = 0
|
274
|
+
start = end = 0.0
|
275
|
+
species = ""
|
276
|
+
|
277
|
+
for i, line in enumerate(lines):
|
278
|
+
if rtype == "table" and i > 0:
|
279
|
+
d = line.split("\t")
|
280
|
+
start = float(d[header_mapping["Begin Time (s)"]])
|
281
|
+
end = float(d[header_mapping["End Time (s)"]])
|
282
|
+
species = d[header_mapping["Common Name"]]
|
283
|
+
confidence = float(d[header_mapping["Confidence"]])
|
284
|
+
|
285
|
+
elif rtype == "audacity":
|
286
|
+
d = line.split("\t")
|
287
|
+
start = float(d[0])
|
288
|
+
end = float(d[1])
|
289
|
+
species = d[2].split(", ")[1]
|
290
|
+
confidence = float(d[-1])
|
291
|
+
|
292
|
+
elif rtype == "kaleidoscope" and i > 0:
|
293
|
+
d = line.split(",")
|
294
|
+
start = float(d[header_mapping["OFFSET"]])
|
295
|
+
end = float(d[header_mapping["DURATION"]]) + start
|
296
|
+
species = d[header_mapping["scientific_name"]]
|
297
|
+
confidence = float(d[header_mapping["confidence"]])
|
298
|
+
|
299
|
+
elif rtype == "csv" and i > 0:
|
300
|
+
d = line.split(",")
|
301
|
+
start = float(d[header_mapping["Start (s)"]])
|
302
|
+
end = float(d[header_mapping["End (s)"]])
|
303
|
+
species = d[header_mapping["Common name"]]
|
304
|
+
confidence = float(d[header_mapping["Confidence"]])
|
305
|
+
|
306
|
+
# Check if confidence is high enough and label is not "nocall"
|
307
|
+
if confidence >= cfg.MIN_CONFIDENCE and species.lower() != "nocall":
|
308
|
+
segments.append({"audio": afile, "start": start, "end": end, "species": species, "confidence": confidence})
|
309
|
+
|
310
|
+
return segments
|
311
|
+
|
312
|
+
|
313
|
+
def extract_segments(item: tuple[tuple[str, list[dict]], float, dict[str]]):
|
314
|
+
"""
|
315
|
+
Extracts audio segments from a given audio file based on provided segment information.
|
316
|
+
Args:
|
317
|
+
item (tuple): A tuple containing:
|
318
|
+
- A tuple with:
|
319
|
+
- A string representing the path to the audio file.
|
320
|
+
- A list of dictionaries, each containing segment information with keys "start", "end", "species", "confidence", and "audio".
|
321
|
+
- A float representing the segment length.
|
322
|
+
- A dictionary containing configuration settings.
|
323
|
+
Returns:
|
324
|
+
bool: True if segments were successfully extracted, False otherwise.
|
325
|
+
Raises:
|
326
|
+
Exception: If there is an error opening the audio file or extracting segments.
|
327
|
+
"""
|
328
|
+
# Paths and config
|
329
|
+
afile = item[0][0]
|
330
|
+
segments = item[0][1]
|
331
|
+
seg_length = item[1]
|
332
|
+
cfg.set_config(item[2])
|
333
|
+
|
334
|
+
# Status
|
335
|
+
print(f"Extracting segments from {afile}")
|
336
|
+
|
337
|
+
try:
|
338
|
+
# Open audio file
|
339
|
+
sig, rate = audio.open_audio_file(afile, cfg.SAMPLE_RATE, speed=cfg.AUDIO_SPEED)
|
340
|
+
except Exception as ex:
|
341
|
+
print(f"Error: Cannot open audio file {afile}", flush=True)
|
342
|
+
utils.write_error_log(ex)
|
343
|
+
|
344
|
+
return None
|
345
|
+
|
346
|
+
# Extract segments
|
347
|
+
for seg_cnt, seg in enumerate(segments, 1):
|
348
|
+
try:
|
349
|
+
# Get start and end times
|
350
|
+
start = int((seg["start"] * rate) / cfg.AUDIO_SPEED)
|
351
|
+
end = int((seg["end"] * rate) / cfg.AUDIO_SPEED)
|
352
|
+
|
353
|
+
offset = max(0, ((seg_length * rate) - (end - start)) // 2)
|
354
|
+
start = max(0, start - offset)
|
355
|
+
end = min(len(sig), end + offset)
|
356
|
+
|
357
|
+
# Make sure segment is long enough
|
358
|
+
if end > start:
|
359
|
+
# Get segment raw audio from signal
|
360
|
+
seg_sig = sig[int(start) : int(end)]
|
361
|
+
|
362
|
+
# Make output path
|
363
|
+
outpath = os.path.join(cfg.OUTPUT_PATH, seg["species"])
|
364
|
+
os.makedirs(outpath, exist_ok=True)
|
365
|
+
|
366
|
+
# Save segment
|
367
|
+
seg_name = "{:.3f}_{}_{}_{:.1f}s_{:.1f}s.wav".format(
|
368
|
+
seg["confidence"],
|
369
|
+
seg_cnt,
|
370
|
+
seg["audio"].rsplit(os.sep, 1)[-1].rsplit(".", 1)[0],
|
371
|
+
seg["start"],
|
372
|
+
seg["end"],
|
373
|
+
)
|
374
|
+
seg_path = os.path.join(outpath, seg_name)
|
375
|
+
audio.save_signal(seg_sig, seg_path, rate)
|
376
|
+
|
377
|
+
except Exception as ex:
|
378
|
+
# Write error log
|
379
|
+
print(f"Error: Cannot extract segments from {afile}.", flush=True)
|
380
|
+
utils.write_error_log(ex)
|
381
|
+
return False
|
382
|
+
|
383
|
+
return True
|