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