birdnet-analyzer 2.1.0__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 (120) hide show
  1. birdnet_analyzer/__init__.py +9 -9
  2. birdnet_analyzer/analyze/__init__.py +19 -19
  3. birdnet_analyzer/analyze/__main__.py +3 -3
  4. birdnet_analyzer/analyze/cli.py +30 -30
  5. birdnet_analyzer/analyze/core.py +268 -246
  6. birdnet_analyzer/analyze/utils.py +700 -694
  7. birdnet_analyzer/audio.py +368 -368
  8. birdnet_analyzer/cli.py +732 -732
  9. birdnet_analyzer/config.py +243 -243
  10. birdnet_analyzer/eBird_taxonomy_codes_2024E.json +13045 -13045
  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 -70
  15. birdnet_analyzer/embeddings/utils.py +173 -220
  16. birdnet_analyzer/evaluation/__init__.py +189 -189
  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 -364
  20. birdnet_analyzer/evaluation/assessment/plotting.py +378 -378
  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 -179
  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 -36
  31. birdnet_analyzer/gui/assets/gui.js +89 -93
  32. birdnet_analyzer/gui/embeddings.py +638 -638
  33. birdnet_analyzer/gui/evaluation.py +801 -801
  34. birdnet_analyzer/gui/localization.py +75 -75
  35. birdnet_analyzer/gui/multi_file.py +265 -265
  36. birdnet_analyzer/gui/review.py +472 -472
  37. birdnet_analyzer/gui/segments.py +191 -191
  38. birdnet_analyzer/gui/settings.py +149 -149
  39. birdnet_analyzer/gui/single_file.py +264 -264
  40. birdnet_analyzer/gui/species.py +95 -95
  41. birdnet_analyzer/gui/train.py +687 -687
  42. birdnet_analyzer/gui/utils.py +803 -797
  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 -341
  81. birdnet_analyzer/lang/en.json +342 -341
  82. birdnet_analyzer/lang/fi.json +342 -341
  83. birdnet_analyzer/lang/fr.json +342 -341
  84. birdnet_analyzer/lang/id.json +342 -341
  85. birdnet_analyzer/lang/pt-br.json +342 -341
  86. birdnet_analyzer/lang/ru.json +342 -341
  87. birdnet_analyzer/lang/se.json +342 -341
  88. birdnet_analyzer/lang/tlh.json +342 -341
  89. birdnet_analyzer/lang/zh_TW.json +342 -341
  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 -425
  113. {birdnet_analyzer-2.1.0.dist-info → birdnet_analyzer-2.1.1.dist-info}/METADATA +147 -146
  114. birdnet_analyzer-2.1.1.dist-info/RECORD +124 -0
  115. {birdnet_analyzer-2.1.0.dist-info → birdnet_analyzer-2.1.1.dist-info}/licenses/LICENSE +18 -18
  116. birdnet_analyzer/playground.py +0 -5
  117. birdnet_analyzer-2.1.0.dist-info/RECORD +0 -125
  118. {birdnet_analyzer-2.1.0.dist-info → birdnet_analyzer-2.1.1.dist-info}/WHEEL +0 -0
  119. {birdnet_analyzer-2.1.0.dist-info → birdnet_analyzer-2.1.1.dist-info}/entry_points.txt +0 -0
  120. {birdnet_analyzer-2.1.0.dist-info → birdnet_analyzer-2.1.1.dist-info}/top_level.txt +0 -0
@@ -1,472 +1,472 @@
1
- import os
2
- import random
3
- from functools import partial
4
-
5
- import gradio as gr
6
-
7
- import birdnet_analyzer.config as cfg
8
- import birdnet_analyzer.gui.localization as loc
9
- import birdnet_analyzer.gui.utils as gu
10
- from birdnet_analyzer import utils
11
-
12
- POSITIVE_LABEL_DIR = "Positive"
13
- NEGATIVE_LABEL_DIR = "Negative"
14
- MATPLOTLIB_FIGURE_ID = "review-tab-spectrogram-plot"
15
-
16
- SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
17
-
18
-
19
- def build_review_tab():
20
- def collect_segments(directory, shuffle=False):
21
- segments = (
22
- [
23
- entry.path
24
- for entry in os.scandir(directory)
25
- if (entry.is_file() and not entry.name.startswith(".") and entry.name.rsplit(".", 1)[-1] in cfg.ALLOWED_FILETYPES)
26
- ]
27
- if os.path.isdir(directory)
28
- else []
29
- )
30
-
31
- if shuffle:
32
- random.shuffle(segments)
33
-
34
- return segments
35
-
36
- def collect_files(directory):
37
- return (
38
- collect_segments(directory),
39
- collect_segments(os.path.join(directory, POSITIVE_LABEL_DIR)),
40
- collect_segments(os.path.join(directory, NEGATIVE_LABEL_DIR)),
41
- )
42
-
43
- def create_log_plot(positives, negatives, fig_num=None):
44
- import matplotlib
45
- import matplotlib.pyplot as plt
46
- import numpy as np
47
- from scipy.special import expit
48
- from sklearn import linear_model
49
-
50
- matplotlib.use("agg")
51
-
52
- f = plt.figure(fig_num, figsize=(12, 6))
53
- f.clear()
54
- f.tight_layout(pad=0)
55
- f.set_dpi(300)
56
-
57
- ax = f.add_subplot(111)
58
- ax.set_xlim(0, 1)
59
- ax.set_yticks([0, 1])
60
- ax.set_ylabel(f"{loc.localize('review-tab-regression-plot-y-label-false')}/{loc.localize('review-tab-regression-plot-y-label-true')}")
61
- ax.set_xlabel(loc.localize("review-tab-regression-plot-x-label"))
62
-
63
- x_vals = []
64
- y_val = []
65
-
66
- for fl in positives + negatives:
67
- try:
68
- x_val = float(os.path.basename(fl).split("_", 1)[0])
69
-
70
- if 0 > x_val > 1:
71
- continue
72
-
73
- x_vals.append([x_val])
74
- y_val.append(1 if fl in positives else 0)
75
- except ValueError:
76
- pass
77
-
78
- if (len(positives) + len(negatives)) >= 2 and len(set(y_val)) > 1:
79
- log_model = linear_model.LogisticRegression(C=55)
80
- log_model.fit(x_vals, y_val)
81
- Xs = np.linspace(0, 10, 200)
82
- Ys = expit(Xs * log_model.coef_ + log_model.intercept_).ravel()
83
- target_ps = [0.85, 0.9, 0.95, 0.99]
84
- thresholds = [(np.log(target_p / (1 - target_p)) - log_model.intercept_[0]) / log_model.coef_[0][0] for target_p in target_ps]
85
- p_colors = ["blue", "purple", "orange", "green"]
86
-
87
- for target_p, p_color, threshold in zip(target_ps, p_colors, thresholds, strict=True):
88
- if threshold <= 1:
89
- ax.vlines(
90
- threshold,
91
- 0,
92
- target_p,
93
- color=p_color,
94
- linestyle="--",
95
- linewidth=0.5,
96
- label=f"p={target_p:.2f} threshold>={threshold:.2f}",
97
- )
98
- ax.hlines(target_p, 0, threshold, color=p_color, linestyle="--", linewidth=0.5)
99
-
100
- ax.plot(Xs, Ys, color="red")
101
- ax.scatter(thresholds, target_ps, color=p_colors, marker="x")
102
-
103
- box = ax.get_position()
104
- ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
105
-
106
- if any(threshold <= 1 for threshold in thresholds):
107
- ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
108
-
109
- if len(y_val) > 0:
110
- ax.scatter(x_vals, y_val, 2)
111
-
112
- return gr.Plot(value=f, visible=bool(y_val))
113
-
114
- with gr.Tab(loc.localize("review-tab-title"), elem_id="review-tab"):
115
- review_state = gr.State(
116
- {
117
- "input_directory": "",
118
- "species_list": [],
119
- "current_species": "",
120
- "files": [],
121
- POSITIVE_LABEL_DIR: [],
122
- NEGATIVE_LABEL_DIR: [],
123
- "skipped": [],
124
- "history": [],
125
- }
126
- )
127
-
128
- select_directory_btn = gr.Button(loc.localize("review-tab-input-directory-button-label"))
129
-
130
- with gr.Column(visible=False) as review_col:
131
- with gr.Row():
132
- species_dropdown = gr.Dropdown(label=loc.localize("review-tab-species-dropdown-label"))
133
- file_count_matrix = gr.Matrix(
134
- headers=[
135
- loc.localize("review-tab-file-matrix-todo-header"),
136
- loc.localize("review-tab-file-matrix-pos-header"),
137
- loc.localize("review-tab-file-matrix-neg-header"),
138
- ],
139
- interactive=False,
140
- elem_id="segments-results-grid",
141
- )
142
-
143
- with gr.Column() as review_item_col, gr.Row():
144
- with gr.Column(), gr.Group():
145
- spectrogram_image = gr.Plot(label=loc.localize("review-tab-spectrogram-plot-label"), show_label=False)
146
- spectrogram_dl_btn = gr.Button("Download spectrogram", size="sm")
147
-
148
- with gr.Column():
149
- positive_btn = gr.Button(
150
- loc.localize("review-tab-pos-button-label"),
151
- elem_id="positive-button",
152
- variant="huggingface",
153
- icon=os.path.join(SCRIPT_DIR, "assets/arrow_up.svg"),
154
- )
155
- negative_btn = gr.Button(
156
- loc.localize("review-tab-neg-button-label"),
157
- elem_id="negative-button",
158
- variant="huggingface",
159
- icon=os.path.join(SCRIPT_DIR, "assets/arrow_down.svg"),
160
- )
161
-
162
- with gr.Row():
163
- undo_btn = gr.Button(
164
- loc.localize("review-tab-undo-button-label"),
165
- elem_id="undo-button",
166
- icon=os.path.join(SCRIPT_DIR, "assets/arrow_left.svg"),
167
- )
168
- skip_btn = gr.Button(
169
- loc.localize("review-tab-skip-button-label"),
170
- elem_id="skip-button",
171
- icon=os.path.join(SCRIPT_DIR, "assets/arrow_right.svg"),
172
- )
173
-
174
- with gr.Group():
175
- review_audio = gr.Audio(type="filepath", sources=[], show_download_button=False, autoplay=True)
176
- autoplay_checkbox = gr.Checkbox(True, label=loc.localize("review-tab-autoplay-checkbox-label"))
177
-
178
- no_samles_label = gr.Label(loc.localize("review-tab-no-files-label"), visible=False, show_label=False)
179
- with gr.Group():
180
- species_regression_plot = gr.Plot(label=loc.localize("review-tab-regression-plot-label"))
181
- regression_dl_btn = gr.Button("Download regression", size="sm")
182
-
183
- def update_values(next_review_state, skip_plot=False):
184
- update_dict = {review_state: next_review_state}
185
-
186
- if not skip_plot:
187
- update_dict |= {
188
- species_regression_plot: create_log_plot(next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2),
189
- }
190
-
191
- if next_review_state["files"]:
192
- next_file = next_review_state["files"][0]
193
- update_dict |= {
194
- review_audio: gr.Audio(next_file, label=os.path.basename(next_file)),
195
- spectrogram_image: utils.spectrogram_from_file(next_file, fig_num=MATPLOTLIB_FIGURE_ID, fig_size=(8, 4)),
196
- }
197
-
198
- update_dict |= {
199
- file_count_matrix: [
200
- [
201
- len(next_review_state["files"]) + len(next_review_state["skipped"]),
202
- len(next_review_state[POSITIVE_LABEL_DIR]),
203
- len(next_review_state[NEGATIVE_LABEL_DIR]),
204
- ],
205
- ],
206
- undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
207
- positive_btn: gr.Button(interactive=bool(next_review_state["files"])),
208
- negative_btn: gr.Button(interactive=bool(next_review_state["files"])),
209
- skip_btn: gr.Button(interactive=bool(next_review_state["files"])),
210
- no_samles_label: gr.Label(visible=not bool(next_review_state["files"])),
211
- review_item_col: gr.Column(visible=bool(next_review_state["files"])),
212
- regression_dl_btn: gr.Button(
213
- visible=update_dict[species_regression_plot].constructor_args["visible"] if species_regression_plot in update_dict else False
214
- ),
215
- }
216
-
217
- return update_dict
218
-
219
- def next_review(next_review_state: dict, target_dir: str | None = None):
220
- try:
221
- current_file = next_review_state["files"][0]
222
- except IndexError as e:
223
- if next_review_state["input_directory"]:
224
- raise gr.Error(loc.localize("review-tab-no-files-error")) from e
225
-
226
- return {review_state: next_review_state}
227
-
228
- if target_dir:
229
- selected_dir = os.path.join(
230
- next_review_state["input_directory"],
231
- next_review_state["current_species"] if next_review_state["current_species"] else "",
232
- target_dir,
233
- )
234
-
235
- os.makedirs(selected_dir, exist_ok=True)
236
-
237
- os.rename(
238
- current_file,
239
- os.path.join(selected_dir, os.path.basename(current_file)),
240
- )
241
-
242
- next_review_state[target_dir] += [current_file]
243
- next_review_state["files"].remove(current_file)
244
-
245
- next_review_state["history"].append((current_file, target_dir))
246
- else:
247
- next_review_state["skipped"].append(current_file)
248
- next_review_state["files"].remove(current_file)
249
- next_review_state["history"].append((current_file, None))
250
-
251
- return update_values(next_review_state)
252
-
253
- def select_subdir(new_value: str, next_review_state: dict):
254
- if new_value != next_review_state["current_species"]:
255
- return update_review(next_review_state, selected_species=new_value)
256
-
257
- return {review_state: next_review_state}
258
-
259
- def start_review(next_review_state):
260
- dir_name = gu.select_folder(state_key="review-input-dir")
261
-
262
- if dir_name:
263
- next_review_state["input_directory"] = dir_name
264
- specieslist = [
265
- e.name for e in os.scandir(next_review_state["input_directory"]) if e.is_dir() and e.name not in (POSITIVE_LABEL_DIR, NEGATIVE_LABEL_DIR)
266
- ]
267
-
268
- next_review_state["species_list"] = specieslist
269
-
270
- return update_review(next_review_state)
271
-
272
- return {review_state: next_review_state}
273
-
274
- def try_confidence(filename):
275
- try:
276
- val = float(os.path.basename(filename).split("_", 1)[0])
277
-
278
- if 0 > val > 1:
279
- return 0
280
-
281
- return val
282
- except ValueError:
283
- return 0
284
-
285
- def update_review(next_review_state: dict, selected_species: str | None = None):
286
- next_review_state["history"] = []
287
- next_review_state["skipped"] = []
288
-
289
- if selected_species:
290
- next_review_state["current_species"] = selected_species
291
- else:
292
- next_review_state["current_species"] = next_review_state["species_list"][0] if next_review_state["species_list"] else None
293
-
294
- todo_files, positives, negatives = collect_files(
295
- os.path.join(next_review_state["input_directory"], next_review_state["current_species"])
296
- if next_review_state["current_species"]
297
- else next_review_state["input_directory"]
298
- )
299
-
300
- todo_files = sorted(todo_files, key=try_confidence, reverse=True)
301
-
302
- next_review_state |= {
303
- "files": todo_files,
304
- POSITIVE_LABEL_DIR: positives,
305
- NEGATIVE_LABEL_DIR: negatives,
306
- }
307
-
308
- update_dict = {
309
- review_col: gr.Column(visible=True),
310
- review_state: next_review_state,
311
- undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
312
- positive_btn: gr.Button(interactive=bool(next_review_state["files"])),
313
- negative_btn: gr.Button(interactive=bool(next_review_state["files"])),
314
- skip_btn: gr.Button(interactive=bool(next_review_state["files"])),
315
- file_count_matrix: [
316
- [
317
- len(next_review_state["files"]),
318
- len(next_review_state[POSITIVE_LABEL_DIR]),
319
- len(next_review_state[NEGATIVE_LABEL_DIR]),
320
- ],
321
- ],
322
- species_regression_plot: create_log_plot(next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2),
323
- }
324
-
325
- if not selected_species:
326
- if next_review_state["species_list"]:
327
- update_dict |= {
328
- species_dropdown: gr.Dropdown(
329
- choices=next_review_state["species_list"],
330
- value=next_review_state["current_species"],
331
- visible=True,
332
- )
333
- }
334
- else:
335
- update_dict |= {species_dropdown: gr.Dropdown(visible=False)}
336
-
337
- if todo_files:
338
- update_dict |= {
339
- review_item_col: gr.Column(visible=True),
340
- review_audio: gr.Audio(value=todo_files[0], label=os.path.basename(todo_files[0])),
341
- spectrogram_image: utils.spectrogram_from_file(todo_files[0], fig_num=MATPLOTLIB_FIGURE_ID, fig_size=(8, 4)),
342
- no_samles_label: gr.Label(visible=False),
343
- }
344
- else:
345
- update_dict |= {review_item_col: gr.Column(visible=False), no_samles_label: gr.Label(visible=True)}
346
-
347
- update_dict[regression_dl_btn] = gr.Button(visible=update_dict[species_regression_plot].constructor_args["visible"])
348
-
349
- return update_dict
350
-
351
- def undo_review(next_review_state):
352
- if next_review_state["history"]:
353
- last_file, last_dir = next_review_state["history"].pop()
354
-
355
- if last_dir:
356
- os.rename(
357
- os.path.join(
358
- next_review_state["input_directory"],
359
- next_review_state["current_species"] if next_review_state["current_species"] else "",
360
- last_dir,
361
- os.path.basename(last_file),
362
- ),
363
- os.path.join(
364
- next_review_state["input_directory"],
365
- next_review_state["current_species"] if next_review_state["current_species"] else "",
366
- os.path.basename(last_file),
367
- ),
368
- )
369
-
370
- next_review_state[last_dir].remove(last_file)
371
- else:
372
- next_review_state["skipped"].remove(last_file)
373
-
374
- was_last_file = not next_review_state["files"]
375
- next_review_state["files"].insert(0, last_file)
376
-
377
- return update_values(next_review_state, skip_plot=not (was_last_file or last_dir))
378
-
379
- return {
380
- review_state: next_review_state,
381
- undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
382
- }
383
-
384
- def toggle_autoplay(value):
385
- return gr.Audio(autoplay=value)
386
-
387
- autoplay_checkbox.change(toggle_autoplay, inputs=autoplay_checkbox, outputs=review_audio)
388
-
389
- review_change_output = [
390
- review_col,
391
- review_item_col,
392
- review_audio,
393
- spectrogram_image,
394
- species_dropdown,
395
- no_samles_label,
396
- review_state,
397
- file_count_matrix,
398
- species_regression_plot,
399
- undo_btn,
400
- skip_btn,
401
- positive_btn,
402
- negative_btn,
403
- regression_dl_btn,
404
- ]
405
-
406
- spectrogram_dl_btn.click(partial(gu.download_plot, filename="spectrogram"), show_progress=False, inputs=spectrogram_image)
407
- regression_dl_btn.click(partial(gu.download_plot, filename="regression"), show_progress=False, inputs=species_regression_plot)
408
-
409
- species_dropdown.change(
410
- select_subdir,
411
- show_progress=True,
412
- inputs=[species_dropdown, review_state],
413
- outputs=review_change_output,
414
- )
415
-
416
- review_btn_output = [
417
- review_audio,
418
- spectrogram_image,
419
- review_state,
420
- review_item_col,
421
- no_samles_label,
422
- file_count_matrix,
423
- species_regression_plot,
424
- undo_btn,
425
- skip_btn,
426
- positive_btn,
427
- negative_btn,
428
- regression_dl_btn,
429
- ]
430
-
431
- positive_btn.click(
432
- partial(next_review, target_dir=POSITIVE_LABEL_DIR),
433
- inputs=review_state,
434
- outputs=review_btn_output,
435
- show_progress=True,
436
- show_progress_on=review_audio,
437
- )
438
-
439
- negative_btn.click(
440
- partial(next_review, target_dir=NEGATIVE_LABEL_DIR),
441
- inputs=review_state,
442
- outputs=review_btn_output,
443
- show_progress=True,
444
- show_progress_on=review_audio,
445
- )
446
-
447
- skip_btn.click(
448
- next_review,
449
- inputs=review_state,
450
- outputs=review_btn_output,
451
- show_progress=True,
452
- show_progress_on=review_audio,
453
- )
454
-
455
- undo_btn.click(
456
- undo_review,
457
- inputs=review_state,
458
- outputs=review_btn_output,
459
- show_progress=True,
460
- show_progress_on=review_audio,
461
- )
462
-
463
- select_directory_btn.click(
464
- start_review,
465
- inputs=review_state,
466
- outputs=review_change_output,
467
- show_progress=True,
468
- )
469
-
470
-
471
- if __name__ == "__main__":
472
- gu.open_window(build_review_tab)
1
+ import os
2
+ import random
3
+ from functools import partial
4
+
5
+ import gradio as gr
6
+
7
+ import birdnet_analyzer.config as cfg
8
+ import birdnet_analyzer.gui.localization as loc
9
+ import birdnet_analyzer.gui.utils as gu
10
+ from birdnet_analyzer import utils
11
+
12
+ POSITIVE_LABEL_DIR = "Positive"
13
+ NEGATIVE_LABEL_DIR = "Negative"
14
+ MATPLOTLIB_FIGURE_ID = "review-tab-spectrogram-plot"
15
+
16
+ SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
17
+
18
+
19
+ def build_review_tab():
20
+ def collect_segments(directory, shuffle=False):
21
+ segments = (
22
+ [
23
+ entry.path
24
+ for entry in os.scandir(directory)
25
+ if (entry.is_file() and not entry.name.startswith(".") and entry.name.rsplit(".", 1)[-1] in cfg.ALLOWED_FILETYPES)
26
+ ]
27
+ if os.path.isdir(directory)
28
+ else []
29
+ )
30
+
31
+ if shuffle:
32
+ random.shuffle(segments)
33
+
34
+ return segments
35
+
36
+ def collect_files(directory):
37
+ return (
38
+ collect_segments(directory),
39
+ collect_segments(os.path.join(directory, POSITIVE_LABEL_DIR)),
40
+ collect_segments(os.path.join(directory, NEGATIVE_LABEL_DIR)),
41
+ )
42
+
43
+ def create_log_plot(positives, negatives, fig_num=None):
44
+ import matplotlib
45
+ import matplotlib.pyplot as plt
46
+ import numpy as np
47
+ from scipy.special import expit
48
+ from sklearn import linear_model
49
+
50
+ matplotlib.use("agg")
51
+
52
+ f = plt.figure(fig_num, figsize=(12, 6))
53
+ f.clear()
54
+ f.tight_layout(pad=0)
55
+ f.set_dpi(300)
56
+
57
+ ax = f.add_subplot(111)
58
+ ax.set_xlim(0, 1)
59
+ ax.set_yticks([0, 1])
60
+ ax.set_ylabel(f"{loc.localize('review-tab-regression-plot-y-label-false')}/{loc.localize('review-tab-regression-plot-y-label-true')}")
61
+ ax.set_xlabel(loc.localize("review-tab-regression-plot-x-label"))
62
+
63
+ x_vals = []
64
+ y_val = []
65
+
66
+ for fl in positives + negatives:
67
+ try:
68
+ x_val = float(os.path.basename(fl).split("_", 1)[0])
69
+
70
+ if 0 > x_val > 1:
71
+ continue
72
+
73
+ x_vals.append([x_val])
74
+ y_val.append(1 if fl in positives else 0)
75
+ except ValueError:
76
+ pass
77
+
78
+ if (len(positives) + len(negatives)) >= 2 and len(set(y_val)) > 1:
79
+ log_model = linear_model.LogisticRegression(C=55)
80
+ log_model.fit(x_vals, y_val)
81
+ Xs = np.linspace(0, 10, 200)
82
+ Ys = expit(Xs * log_model.coef_ + log_model.intercept_).ravel()
83
+ target_ps = [0.85, 0.9, 0.95, 0.99]
84
+ thresholds = [(np.log(target_p / (1 - target_p)) - log_model.intercept_[0]) / log_model.coef_[0][0] for target_p in target_ps]
85
+ p_colors = ["blue", "purple", "orange", "green"]
86
+
87
+ for target_p, p_color, threshold in zip(target_ps, p_colors, thresholds, strict=True):
88
+ if threshold <= 1:
89
+ ax.vlines(
90
+ threshold,
91
+ 0,
92
+ target_p,
93
+ color=p_color,
94
+ linestyle="--",
95
+ linewidth=0.5,
96
+ label=f"p={target_p:.2f} threshold>={threshold:.2f}",
97
+ )
98
+ ax.hlines(target_p, 0, threshold, color=p_color, linestyle="--", linewidth=0.5)
99
+
100
+ ax.plot(Xs, Ys, color="red")
101
+ ax.scatter(thresholds, target_ps, color=p_colors, marker="x")
102
+
103
+ box = ax.get_position()
104
+ ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
105
+
106
+ if any(threshold <= 1 for threshold in thresholds):
107
+ ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
108
+
109
+ if len(y_val) > 0:
110
+ ax.scatter(x_vals, y_val, 2)
111
+
112
+ return gr.Plot(value=f, visible=bool(y_val))
113
+
114
+ with gr.Tab(loc.localize("review-tab-title"), elem_id="review-tab"):
115
+ review_state = gr.State(
116
+ {
117
+ "input_directory": "",
118
+ "species_list": [],
119
+ "current_species": "",
120
+ "files": [],
121
+ POSITIVE_LABEL_DIR: [],
122
+ NEGATIVE_LABEL_DIR: [],
123
+ "skipped": [],
124
+ "history": [],
125
+ }
126
+ )
127
+
128
+ select_directory_btn = gr.Button(loc.localize("review-tab-input-directory-button-label"))
129
+
130
+ with gr.Column(visible=False) as review_col:
131
+ with gr.Row():
132
+ species_dropdown = gr.Dropdown(label=loc.localize("review-tab-species-dropdown-label"))
133
+ file_count_matrix = gr.Matrix(
134
+ headers=[
135
+ loc.localize("review-tab-file-matrix-todo-header"),
136
+ loc.localize("review-tab-file-matrix-pos-header"),
137
+ loc.localize("review-tab-file-matrix-neg-header"),
138
+ ],
139
+ interactive=False,
140
+ elem_id="segments-results-grid",
141
+ )
142
+
143
+ with gr.Column() as review_item_col, gr.Row():
144
+ with gr.Column(), gr.Group():
145
+ spectrogram_image = gr.Plot(label=loc.localize("review-tab-spectrogram-plot-label"), show_label=False)
146
+ spectrogram_dl_btn = gr.Button("Download spectrogram", size="sm")
147
+
148
+ with gr.Column():
149
+ positive_btn = gr.Button(
150
+ loc.localize("review-tab-pos-button-label"),
151
+ elem_id="positive-button",
152
+ variant="huggingface",
153
+ icon=os.path.join(SCRIPT_DIR, "assets/arrow_up.svg"),
154
+ )
155
+ negative_btn = gr.Button(
156
+ loc.localize("review-tab-neg-button-label"),
157
+ elem_id="negative-button",
158
+ variant="huggingface",
159
+ icon=os.path.join(SCRIPT_DIR, "assets/arrow_down.svg"),
160
+ )
161
+
162
+ with gr.Row():
163
+ undo_btn = gr.Button(
164
+ loc.localize("review-tab-undo-button-label"),
165
+ elem_id="undo-button",
166
+ icon=os.path.join(SCRIPT_DIR, "assets/arrow_left.svg"),
167
+ )
168
+ skip_btn = gr.Button(
169
+ loc.localize("review-tab-skip-button-label"),
170
+ elem_id="skip-button",
171
+ icon=os.path.join(SCRIPT_DIR, "assets/arrow_right.svg"),
172
+ )
173
+
174
+ with gr.Group():
175
+ review_audio = gr.Audio(type="filepath", sources=[], show_download_button=False, autoplay=True)
176
+ autoplay_checkbox = gr.Checkbox(True, label=loc.localize("review-tab-autoplay-checkbox-label"))
177
+
178
+ no_samles_label = gr.Label(loc.localize("review-tab-no-files-label"), visible=False, show_label=False)
179
+ with gr.Group():
180
+ species_regression_plot = gr.Plot(label=loc.localize("review-tab-regression-plot-label"))
181
+ regression_dl_btn = gr.Button("Download regression", size="sm")
182
+
183
+ def update_values(next_review_state, skip_plot=False):
184
+ update_dict = {review_state: next_review_state}
185
+
186
+ if not skip_plot:
187
+ update_dict |= {
188
+ species_regression_plot: create_log_plot(next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2),
189
+ }
190
+
191
+ if next_review_state["files"]:
192
+ next_file = next_review_state["files"][0]
193
+ update_dict |= {
194
+ review_audio: gr.Audio(next_file, label=os.path.basename(next_file)),
195
+ spectrogram_image: utils.spectrogram_from_file(next_file, fig_num=MATPLOTLIB_FIGURE_ID, fig_size=(8, 4)),
196
+ }
197
+
198
+ update_dict |= {
199
+ file_count_matrix: [
200
+ [
201
+ len(next_review_state["files"]) + len(next_review_state["skipped"]),
202
+ len(next_review_state[POSITIVE_LABEL_DIR]),
203
+ len(next_review_state[NEGATIVE_LABEL_DIR]),
204
+ ],
205
+ ],
206
+ undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
207
+ positive_btn: gr.Button(interactive=bool(next_review_state["files"])),
208
+ negative_btn: gr.Button(interactive=bool(next_review_state["files"])),
209
+ skip_btn: gr.Button(interactive=bool(next_review_state["files"])),
210
+ no_samles_label: gr.Label(visible=not bool(next_review_state["files"])),
211
+ review_item_col: gr.Column(visible=bool(next_review_state["files"])),
212
+ regression_dl_btn: gr.Button(
213
+ visible=update_dict[species_regression_plot].constructor_args["visible"] if species_regression_plot in update_dict else False
214
+ ),
215
+ }
216
+
217
+ return update_dict
218
+
219
+ def next_review(next_review_state: dict, target_dir: str | None = None):
220
+ try:
221
+ current_file = next_review_state["files"][0]
222
+ except IndexError as e:
223
+ if next_review_state["input_directory"]:
224
+ raise gr.Error(loc.localize("review-tab-no-files-error")) from e
225
+
226
+ return {review_state: next_review_state}
227
+
228
+ if target_dir:
229
+ selected_dir = os.path.join(
230
+ next_review_state["input_directory"],
231
+ next_review_state["current_species"] if next_review_state["current_species"] else "",
232
+ target_dir,
233
+ )
234
+
235
+ os.makedirs(selected_dir, exist_ok=True)
236
+
237
+ os.rename(
238
+ current_file,
239
+ os.path.join(selected_dir, os.path.basename(current_file)),
240
+ )
241
+
242
+ next_review_state[target_dir] += [current_file]
243
+ next_review_state["files"].remove(current_file)
244
+
245
+ next_review_state["history"].append((current_file, target_dir))
246
+ else:
247
+ next_review_state["skipped"].append(current_file)
248
+ next_review_state["files"].remove(current_file)
249
+ next_review_state["history"].append((current_file, None))
250
+
251
+ return update_values(next_review_state)
252
+
253
+ def select_subdir(new_value: str, next_review_state: dict):
254
+ if new_value != next_review_state["current_species"]:
255
+ return update_review(next_review_state, selected_species=new_value)
256
+
257
+ return {review_state: next_review_state}
258
+
259
+ def start_review(next_review_state):
260
+ dir_name = gu.select_folder(state_key="review-input-dir")
261
+
262
+ if dir_name:
263
+ next_review_state["input_directory"] = dir_name
264
+ specieslist = [
265
+ e.name for e in os.scandir(next_review_state["input_directory"]) if e.is_dir() and e.name not in (POSITIVE_LABEL_DIR, NEGATIVE_LABEL_DIR)
266
+ ]
267
+
268
+ next_review_state["species_list"] = specieslist
269
+
270
+ return update_review(next_review_state)
271
+
272
+ return {review_state: next_review_state}
273
+
274
+ def try_confidence(filename):
275
+ try:
276
+ val = float(os.path.basename(filename).split("_", 1)[0])
277
+
278
+ if 0 > val > 1:
279
+ return 0
280
+
281
+ return val
282
+ except ValueError:
283
+ return 0
284
+
285
+ def update_review(next_review_state: dict, selected_species: str | None = None):
286
+ next_review_state["history"] = []
287
+ next_review_state["skipped"] = []
288
+
289
+ if selected_species:
290
+ next_review_state["current_species"] = selected_species
291
+ else:
292
+ next_review_state["current_species"] = next_review_state["species_list"][0] if next_review_state["species_list"] else None
293
+
294
+ todo_files, positives, negatives = collect_files(
295
+ os.path.join(next_review_state["input_directory"], next_review_state["current_species"])
296
+ if next_review_state["current_species"]
297
+ else next_review_state["input_directory"]
298
+ )
299
+
300
+ todo_files = sorted(todo_files, key=try_confidence, reverse=True)
301
+
302
+ next_review_state |= {
303
+ "files": todo_files,
304
+ POSITIVE_LABEL_DIR: positives,
305
+ NEGATIVE_LABEL_DIR: negatives,
306
+ }
307
+
308
+ update_dict = {
309
+ review_col: gr.Column(visible=True),
310
+ review_state: next_review_state,
311
+ undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
312
+ positive_btn: gr.Button(interactive=bool(next_review_state["files"])),
313
+ negative_btn: gr.Button(interactive=bool(next_review_state["files"])),
314
+ skip_btn: gr.Button(interactive=bool(next_review_state["files"])),
315
+ file_count_matrix: [
316
+ [
317
+ len(next_review_state["files"]),
318
+ len(next_review_state[POSITIVE_LABEL_DIR]),
319
+ len(next_review_state[NEGATIVE_LABEL_DIR]),
320
+ ],
321
+ ],
322
+ species_regression_plot: create_log_plot(next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2),
323
+ }
324
+
325
+ if not selected_species:
326
+ if next_review_state["species_list"]:
327
+ update_dict |= {
328
+ species_dropdown: gr.Dropdown(
329
+ choices=next_review_state["species_list"],
330
+ value=next_review_state["current_species"],
331
+ visible=True,
332
+ )
333
+ }
334
+ else:
335
+ update_dict |= {species_dropdown: gr.Dropdown(visible=False)}
336
+
337
+ if todo_files:
338
+ update_dict |= {
339
+ review_item_col: gr.Column(visible=True),
340
+ review_audio: gr.Audio(value=todo_files[0], label=os.path.basename(todo_files[0])),
341
+ spectrogram_image: utils.spectrogram_from_file(todo_files[0], fig_num=MATPLOTLIB_FIGURE_ID, fig_size=(8, 4)),
342
+ no_samles_label: gr.Label(visible=False),
343
+ }
344
+ else:
345
+ update_dict |= {review_item_col: gr.Column(visible=False), no_samles_label: gr.Label(visible=True)}
346
+
347
+ update_dict[regression_dl_btn] = gr.Button(visible=update_dict[species_regression_plot].constructor_args["visible"])
348
+
349
+ return update_dict
350
+
351
+ def undo_review(next_review_state):
352
+ if next_review_state["history"]:
353
+ last_file, last_dir = next_review_state["history"].pop()
354
+
355
+ if last_dir:
356
+ os.rename(
357
+ os.path.join(
358
+ next_review_state["input_directory"],
359
+ next_review_state["current_species"] if next_review_state["current_species"] else "",
360
+ last_dir,
361
+ os.path.basename(last_file),
362
+ ),
363
+ os.path.join(
364
+ next_review_state["input_directory"],
365
+ next_review_state["current_species"] if next_review_state["current_species"] else "",
366
+ os.path.basename(last_file),
367
+ ),
368
+ )
369
+
370
+ next_review_state[last_dir].remove(last_file)
371
+ else:
372
+ next_review_state["skipped"].remove(last_file)
373
+
374
+ was_last_file = not next_review_state["files"]
375
+ next_review_state["files"].insert(0, last_file)
376
+
377
+ return update_values(next_review_state, skip_plot=not (was_last_file or last_dir))
378
+
379
+ return {
380
+ review_state: next_review_state,
381
+ undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
382
+ }
383
+
384
+ def toggle_autoplay(value):
385
+ return gr.Audio(autoplay=value)
386
+
387
+ autoplay_checkbox.change(toggle_autoplay, inputs=autoplay_checkbox, outputs=review_audio)
388
+
389
+ review_change_output = [
390
+ review_col,
391
+ review_item_col,
392
+ review_audio,
393
+ spectrogram_image,
394
+ species_dropdown,
395
+ no_samles_label,
396
+ review_state,
397
+ file_count_matrix,
398
+ species_regression_plot,
399
+ undo_btn,
400
+ skip_btn,
401
+ positive_btn,
402
+ negative_btn,
403
+ regression_dl_btn,
404
+ ]
405
+
406
+ spectrogram_dl_btn.click(partial(gu.download_plot, filename="spectrogram"), show_progress="hidden", inputs=spectrogram_image)
407
+ regression_dl_btn.click(partial(gu.download_plot, filename="regression"), show_progress="hidden", inputs=species_regression_plot)
408
+
409
+ species_dropdown.change(
410
+ select_subdir,
411
+ show_progress="full",
412
+ inputs=[species_dropdown, review_state],
413
+ outputs=review_change_output,
414
+ )
415
+
416
+ review_btn_output = [
417
+ review_audio,
418
+ spectrogram_image,
419
+ review_state,
420
+ review_item_col,
421
+ no_samles_label,
422
+ file_count_matrix,
423
+ species_regression_plot,
424
+ undo_btn,
425
+ skip_btn,
426
+ positive_btn,
427
+ negative_btn,
428
+ regression_dl_btn,
429
+ ]
430
+
431
+ positive_btn.click(
432
+ partial(next_review, target_dir=POSITIVE_LABEL_DIR),
433
+ inputs=review_state,
434
+ outputs=review_btn_output,
435
+ show_progress="full",
436
+ show_progress_on=review_audio,
437
+ )
438
+
439
+ negative_btn.click(
440
+ partial(next_review, target_dir=NEGATIVE_LABEL_DIR),
441
+ inputs=review_state,
442
+ outputs=review_btn_output,
443
+ show_progress="full",
444
+ show_progress_on=review_audio,
445
+ )
446
+
447
+ skip_btn.click(
448
+ next_review,
449
+ inputs=review_state,
450
+ outputs=review_btn_output,
451
+ show_progress="full",
452
+ show_progress_on=review_audio,
453
+ )
454
+
455
+ undo_btn.click(
456
+ undo_review,
457
+ inputs=review_state,
458
+ outputs=review_btn_output,
459
+ show_progress="full",
460
+ show_progress_on=review_audio,
461
+ )
462
+
463
+ select_directory_btn.click(
464
+ start_review,
465
+ inputs=review_state,
466
+ outputs=review_change_output,
467
+ show_progress="full",
468
+ )
469
+
470
+
471
+ if __name__ == "__main__":
472
+ gu.open_window(build_review_tab)