birdnet-analyzer 2.0.1__py3-none-any.whl → 2.1.0__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 (48) hide show
  1. birdnet_analyzer/analyze/__init__.py +14 -0
  2. birdnet_analyzer/analyze/cli.py +5 -0
  3. birdnet_analyzer/analyze/core.py +6 -1
  4. birdnet_analyzer/analyze/utils.py +42 -40
  5. birdnet_analyzer/audio.py +2 -2
  6. birdnet_analyzer/cli.py +41 -18
  7. birdnet_analyzer/config.py +4 -3
  8. birdnet_analyzer/eBird_taxonomy_codes_2024E.json +13046 -0
  9. birdnet_analyzer/embeddings/core.py +2 -1
  10. birdnet_analyzer/embeddings/utils.py +42 -1
  11. birdnet_analyzer/evaluation/__init__.py +6 -13
  12. birdnet_analyzer/evaluation/assessment/performance_assessor.py +12 -57
  13. birdnet_analyzer/evaluation/assessment/plotting.py +61 -62
  14. birdnet_analyzer/evaluation/preprocessing/data_processor.py +1 -1
  15. birdnet_analyzer/gui/analysis.py +5 -1
  16. birdnet_analyzer/gui/assets/gui.css +8 -0
  17. birdnet_analyzer/gui/embeddings.py +37 -18
  18. birdnet_analyzer/gui/evaluation.py +14 -8
  19. birdnet_analyzer/gui/multi_file.py +25 -5
  20. birdnet_analyzer/gui/review.py +16 -63
  21. birdnet_analyzer/gui/settings.py +25 -4
  22. birdnet_analyzer/gui/single_file.py +14 -17
  23. birdnet_analyzer/gui/train.py +7 -16
  24. birdnet_analyzer/gui/utils.py +42 -55
  25. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ca.txt +1 -1
  26. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pl.txt +1 -1
  27. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sr.txt +108 -108
  28. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_zh.txt +1 -1
  29. birdnet_analyzer/lang/de.json +7 -0
  30. birdnet_analyzer/lang/en.json +7 -0
  31. birdnet_analyzer/lang/fi.json +7 -0
  32. birdnet_analyzer/lang/fr.json +7 -0
  33. birdnet_analyzer/lang/id.json +7 -0
  34. birdnet_analyzer/lang/pt-br.json +7 -0
  35. birdnet_analyzer/lang/ru.json +36 -29
  36. birdnet_analyzer/lang/se.json +7 -0
  37. birdnet_analyzer/lang/tlh.json +7 -0
  38. birdnet_analyzer/lang/zh_TW.json +7 -0
  39. birdnet_analyzer/model.py +21 -21
  40. birdnet_analyzer/search/core.py +1 -1
  41. birdnet_analyzer/utils.py +3 -4
  42. {birdnet_analyzer-2.0.1.dist-info → birdnet_analyzer-2.1.0.dist-info}/METADATA +18 -9
  43. {birdnet_analyzer-2.0.1.dist-info → birdnet_analyzer-2.1.0.dist-info}/RECORD +47 -47
  44. {birdnet_analyzer-2.0.1.dist-info → birdnet_analyzer-2.1.0.dist-info}/WHEEL +1 -1
  45. birdnet_analyzer/eBird_taxonomy_codes_2021E.json +0 -25280
  46. {birdnet_analyzer-2.0.1.dist-info → birdnet_analyzer-2.1.0.dist-info}/entry_points.txt +0 -0
  47. {birdnet_analyzer-2.0.1.dist-info → birdnet_analyzer-2.1.0.dist-info}/licenses/LICENSE +0 -0
  48. {birdnet_analyzer-2.0.1.dist-info → birdnet_analyzer-2.1.0.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,7 @@ from birdnet_analyzer.embeddings.core import get_database as get_embeddings_data
10
10
  from birdnet_analyzer.search.core import get_database as get_search_database
11
11
 
12
12
  SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
13
- PAGE_SIZE = 4
13
+ PAGE_SIZE = 6
14
14
 
15
15
 
16
16
  def play_audio(audio_infos):
@@ -37,7 +37,7 @@ def update_export_state(audio_infos, checkbox_value, export_state: dict):
37
37
  return export_state
38
38
 
39
39
 
40
- def rum_embeddings_with_tqdm_tracking(
40
+ def run_embeddings_with_tqdm_tracking(
41
41
  input_path,
42
42
  db_directory,
43
43
  db_name,
@@ -47,6 +47,7 @@ def rum_embeddings_with_tqdm_tracking(
47
47
  audio_speed,
48
48
  fmin,
49
49
  fmax,
50
+ file_output,
50
51
  progress=gr.Progress(track_tqdm=True),
51
52
  ):
52
53
  return run_embeddings(
@@ -59,6 +60,7 @@ def rum_embeddings_with_tqdm_tracking(
59
60
  audio_speed,
60
61
  fmin,
61
62
  fmax,
63
+ file_output,
62
64
  progress,
63
65
  )
64
66
 
@@ -74,6 +76,7 @@ def run_embeddings(
74
76
  audio_speed,
75
77
  fmin,
76
78
  fmax,
79
+ file_output,
77
80
  progress,
78
81
  ):
79
82
  from birdnet_analyzer.embeddings.utils import run
@@ -97,6 +100,7 @@ def run_embeddings(
97
100
  settings["BANDPASS_FMAX"],
98
101
  threads,
99
102
  batch_size,
103
+ file_output,
100
104
  )
101
105
  except Exception as e:
102
106
  db.db.close()
@@ -106,11 +110,11 @@ def run_embeddings(
106
110
  if fmin is None or fmax is None or fmin < cfg.SIG_FMIN or fmax > cfg.SIG_FMAX or fmin > fmax:
107
111
  raise gr.Error(f"{loc.localize('validation-no-valid-frequency')} [{cfg.SIG_FMIN}, {cfg.SIG_FMAX}]") from e
108
112
 
109
- run(input_path, db_path, overlap, audio_speed, fmin, fmax, threads, batch_size)
113
+ run(input_path, db_path, overlap, audio_speed, fmin, fmax, threads, batch_size, file_output)
110
114
 
111
115
  gr.Info(f"{loc.localize('embeddings-tab-finish-info')} {db_path}")
112
116
 
113
- return gr.Plot(), gr.Slider(visible=False), gr.Number(visible=False), gr.Number(visible=False)
117
+ return gr.Plot(), gr.Slider(interactive=False), gr.Number(interactive=False), gr.Number(interactive=False)
114
118
 
115
119
 
116
120
  @gu.gui_runtime_error_handler
@@ -275,7 +279,7 @@ def _build_extract_tab():
275
279
  gr.Number(interactive=True),
276
280
  )
277
281
 
278
- return None, None, gr.Slider(interactive=True), gr.Number(interactive=True), gr.Number(interactive=True)
282
+ return None, gr.Textbox(visible=False), gr.Slider(interactive=True), gr.Number(interactive=True), gr.Number(interactive=True)
279
283
 
280
284
  select_db_directory_btn.click(
281
285
  select_directory_and_update_tb,
@@ -284,6 +288,32 @@ def _build_extract_tab():
284
288
  show_progress=False,
285
289
  )
286
290
 
291
+ with gr.Accordion(loc.localize("embedding-file-output-accordion-label"), open=False):
292
+ with gr.Row():
293
+ select_file_output_directory_btn = gr.Button(loc.localize("embeddings-select-file-output-directory-button-label"))
294
+
295
+ with gr.Row():
296
+ file_output_tb = gr.Textbox(
297
+ value=None,
298
+ placeholder=loc.localize("embeddings-tab-file-output-directory-textbox-placeholder"),
299
+ interactive=True,
300
+ label=loc.localize("embeddings-tab-file-output-directory-textbox-label"),
301
+ )
302
+
303
+ def select_file_output_directory_and_update_tb():
304
+ dir_name = gu.select_directory(state_key="embeddings-file-output-dir", collect_files=False)
305
+
306
+ if dir_name:
307
+ return dir_name
308
+
309
+ return None
310
+
311
+ select_file_output_directory_btn.click(
312
+ select_file_output_directory_and_update_tb,
313
+ inputs=[],
314
+ outputs=[file_output_tb],
315
+ )
316
+
287
317
  def check_settings(dir_name, db_name):
288
318
  db_path = os.path.join(dir_name, db_name)
289
319
 
@@ -316,7 +346,7 @@ def _build_extract_tab():
316
346
  start_btn = gr.Button(loc.localize("embeddings-tab-start-button-label"), variant="huggingface")
317
347
 
318
348
  start_btn.click(
319
- rum_embeddings_with_tqdm_tracking,
349
+ run_embeddings_with_tqdm_tracking,
320
350
  inputs=[
321
351
  input_directory_state,
322
352
  db_directory_state,
@@ -327,10 +357,10 @@ def _build_extract_tab():
327
357
  audio_speed_slider,
328
358
  fmin_number,
329
359
  fmax_number,
360
+ file_output_tb,
330
361
  ],
331
362
  outputs=[progress_plot, audio_speed_slider, fmin_number, fmax_number],
332
363
  show_progress_on=progress_plot,
333
- show_progress=True,
334
364
  )
335
365
 
336
366
 
@@ -409,17 +439,6 @@ def _build_search_tab():
409
439
  value="cosine",
410
440
  interactive=True,
411
441
  )
412
- max_samples_number = gr.Number(
413
- label=loc.localize("embeddings-search-max-samples-number-label"),
414
- value=10,
415
- interactive=True,
416
- )
417
- score_fn_select = gr.Radio(
418
- label=loc.localize("embeddings-search-score-fn-select-label"),
419
- choices=["cosine", "dot", "euclidean"],
420
- value="cosine",
421
- interactive=True,
422
- )
423
442
  search_btn = gr.Button(loc.localize("embeddings-search-start-button-label"), variant="huggingface")
424
443
 
425
444
  with gr.Column():
@@ -339,6 +339,7 @@ def build_evaluation_tab():
339
339
  labels_state = gr.State()
340
340
  annotation_files_state = gr.State()
341
341
  prediction_files_state = gr.State()
342
+ plot_name_state = gr.State()
342
343
 
343
344
  def get_selection_tables(directory):
344
345
  from pathlib import Path
@@ -530,7 +531,10 @@ def build_evaluation_tab():
530
531
  )
531
532
  download_data_button.click(fn=download_data_table, inputs=[processor_state])
532
533
  metric_table = gr.Dataframe(show_label=False, type="pandas", visible=False, interactive=False)
533
- plot_output = gr.Plot(visible=False, show_label=False)
534
+
535
+ with gr.Group(visible=False) as plot_group:
536
+ plot_output = gr.Plot(show_label=False)
537
+ plot_output_dl_btn = gr.Button("Download plot", size="sm")
534
538
 
535
539
  # Update available selections (classes and recordings) and the processor state when files or mapping file change.
536
540
  # Also pass the current selection values so that user selections are preserved.
@@ -604,7 +608,7 @@ def build_evaluation_tab():
604
608
  "average precision (ap)": "ap",
605
609
  "auroc": "auroc",
606
610
  }
607
- metrics = tuple([valid_metrics[m] for m in selected_metrics if m in valid_metrics])
611
+ metrics = tuple(valid_metrics[m] for m in selected_metrics if m in valid_metrics)
608
612
 
609
613
  # Fall back to available classes from processor state if none selected.
610
614
  if not selected_classes_list and proc_state and proc_state.processor:
@@ -717,14 +721,14 @@ def build_evaluation_tab():
717
721
  fig = pa.plot_metrics(predictions, labels, per_class_metrics=class_wise_value)
718
722
  plt.close(fig)
719
723
 
720
- return gr.update(visible=True, value=fig)
724
+ return gr.update(visible=True), gr.update(value=fig), "metrics"
721
725
  except Exception as e:
722
726
  raise gr.Error(f"{loc.localize('eval-tab-error-plotting-metrics')}: {e}") from e
723
727
 
724
728
  plot_metrics_button.click(
725
729
  plot_metrics,
726
730
  inputs=[pa_state, predictions_state, labels_state, class_wise],
727
- outputs=[plot_output],
731
+ outputs=[plot_group, plot_output, plot_name_state],
728
732
  )
729
733
 
730
734
  def plot_confusion_matrix(pa: PerformanceAssessor, predictions, labels):
@@ -734,14 +738,14 @@ def build_evaluation_tab():
734
738
  fig = pa.plot_confusion_matrix(predictions, labels)
735
739
  plt.close(fig)
736
740
 
737
- return gr.update(visible=True, value=fig)
741
+ return gr.update(visible=True), fig, "confusion_matrix"
738
742
  except Exception as e:
739
743
  raise gr.Error(f"{loc.localize('eval-tab-error-plotting-confusion-matrix')}: {e}") from e
740
744
 
741
745
  plot_confusion_button.click(
742
746
  plot_confusion_matrix,
743
747
  inputs=[pa_state, predictions_state, labels_state],
744
- outputs=[plot_output],
748
+ outputs=[plot_group, plot_output, plot_name_state],
745
749
  )
746
750
 
747
751
  annotation_select_directory_btn.click(
@@ -780,16 +784,18 @@ def build_evaluation_tab():
780
784
  fig = pa.plot_metrics_all_thresholds(predictions, labels, per_class_metrics=class_wise_value)
781
785
  plt.close(fig)
782
786
 
783
- return gr.update(visible=True, value=fig)
787
+ return gr.update(visible=True), gr.update(value=fig), "metrics_all_thresholds"
784
788
  except Exception as e:
785
789
  raise gr.Error(f"{loc.localize('eval-tab-error-plotting-metrics-all-thresholds')}: {e}") from e
786
790
 
787
791
  plot_metrics_all_thresholds_button.click(
788
792
  plot_metrics_all_thresholds,
789
793
  inputs=[pa_state, predictions_state, labels_state, class_wise],
790
- outputs=[plot_output],
794
+ outputs=[plot_group, plot_output, plot_name_state],
791
795
  )
792
796
 
797
+ plot_output_dl_btn.click(gu.download_plot, inputs=[plot_output, plot_name_state])
798
+
793
799
 
794
800
  if __name__ == "__main__":
795
801
  gu.open_window(build_evaluation_tab)
@@ -1,4 +1,4 @@
1
- # ruff: noqa: I001
1
+ # ruff: noqa: I001
2
2
  import gradio as gr
3
3
 
4
4
  import birdnet_analyzer.config as cfg
@@ -11,7 +11,16 @@ OUTPUT_TYPE_MAP = {
11
11
  "CSV": "csv",
12
12
  "Kaleidoscope": "kaleidoscope",
13
13
  }
14
-
14
+ ADDITIONAL_COLUMNS_MAP = {
15
+ "Latitude": "lat",
16
+ "Longitude": "lon",
17
+ "Week": "week",
18
+ "Overlap": "overlap",
19
+ "Sensitivity": "sensitivity",
20
+ "Minimum confidence": "min_conf",
21
+ "Species list file": "species_list",
22
+ "Model file": "model",
23
+ }
15
24
 
16
25
  @gu.gui_runtime_error_handler
17
26
  def run_batch_analysis(
@@ -34,6 +43,7 @@ def run_batch_analysis(
34
43
  sf_thresh,
35
44
  custom_classifier_file,
36
45
  output_type,
46
+ additional_columns,
37
47
  combine_tables,
38
48
  locale,
39
49
  batch_size,
@@ -75,6 +85,7 @@ def run_batch_analysis(
75
85
  sf_thresh,
76
86
  custom_classifier_file,
77
87
  output_type,
88
+ additional_columns,
78
89
  combine_tables,
79
90
  locale if locale else "en",
80
91
  batch_size if batch_size and batch_size > 0 else 1,
@@ -113,9 +124,7 @@ def build_multi_analysis_tab():
113
124
 
114
125
  return ["", [[loc.localize("multi-tab-samples-dataframe-no-files-found")]]]
115
126
 
116
- select_directory_btn.click(
117
- select_directory_on_empty, outputs=[input_directory_state, directory_input], show_progress=True
118
- )
127
+ select_directory_btn.click(select_directory_on_empty, outputs=[input_directory_state, directory_input], show_progress=True)
119
128
 
120
129
  with gr.Column():
121
130
  select_out_directory_btn = gr.Button(loc.localize("multi-tab-output-selection-button-label"))
@@ -166,6 +175,12 @@ def build_multi_analysis_tab():
166
175
  label=loc.localize("multi-tab-output-radio-label"),
167
176
  info=loc.localize("multi-tab-output-radio-info"),
168
177
  )
178
+ additional_columns_ = gr.CheckboxGroup(
179
+ list(ADDITIONAL_COLUMNS_MAP.items()),
180
+ visible=False,
181
+ label=loc.localize("multi-tab-additional-columns-checkbox-label"),
182
+ info=loc.localize("multi-tab-additional-columns-checkbox-info"),
183
+ )
169
184
 
170
185
  with gr.Row():
171
186
  combine_tables_checkbox = gr.Checkbox(
@@ -228,6 +243,7 @@ def build_multi_analysis_tab():
228
243
  sf_thresh_number,
229
244
  selected_classifier_state,
230
245
  output_type_radio,
246
+ additional_columns_,
231
247
  combine_tables_checkbox,
232
248
  locale_radio,
233
249
  batch_size_number,
@@ -236,7 +252,11 @@ def build_multi_analysis_tab():
236
252
  skip_existing_checkbox,
237
253
  ]
238
254
 
255
+ def show_additional_columns(values):
256
+ return gr.update(visible="csv" in values)
257
+
239
258
  start_batch_analysis_btn.click(run_batch_analysis, inputs=inputs, outputs=result_grid)
259
+ output_type_radio.change(show_additional_columns, inputs=output_type_radio, outputs=additional_columns_)
240
260
 
241
261
  return lat_number, lon_number, map_plot
242
262
 
@@ -1,5 +1,3 @@
1
- import base64
2
- import io
3
1
  import os
4
2
  import random
5
3
  from functools import partial
@@ -13,6 +11,7 @@ from birdnet_analyzer import utils
13
11
 
14
12
  POSITIVE_LABEL_DIR = "Positive"
15
13
  NEGATIVE_LABEL_DIR = "Negative"
14
+ MATPLOTLIB_FIGURE_ID = "review-tab-spectrogram-plot"
16
15
 
17
16
  SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
18
17
 
@@ -23,11 +22,7 @@ def build_review_tab():
23
22
  [
24
23
  entry.path
25
24
  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
- )
25
+ if (entry.is_file() and not entry.name.startswith(".") and entry.name.rsplit(".", 1)[-1] in cfg.ALLOWED_FILETYPES)
31
26
  ]
32
27
  if os.path.isdir(directory)
33
28
  else []
@@ -55,16 +50,14 @@ def build_review_tab():
55
50
  matplotlib.use("agg")
56
51
 
57
52
  f = plt.figure(fig_num, figsize=(12, 6))
58
- f.clf()
53
+ f.clear()
59
54
  f.tight_layout(pad=0)
60
55
  f.set_dpi(300)
61
56
 
62
57
  ax = f.add_subplot(111)
63
58
  ax.set_xlim(0, 1)
64
59
  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
- )
60
+ ax.set_ylabel(f"{loc.localize('review-tab-regression-plot-y-label-false')}/{loc.localize('review-tab-regression-plot-y-label-true')}")
68
61
  ax.set_xlabel(loc.localize("review-tab-regression-plot-x-label"))
69
62
 
70
63
  x_vals = []
@@ -88,10 +81,7 @@ def build_review_tab():
88
81
  Xs = np.linspace(0, 10, 200)
89
82
  Ys = expit(Xs * log_model.coef_ + log_model.intercept_).ravel()
90
83
  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
- ]
84
+ thresholds = [(np.log(target_p / (1 - target_p)) - log_model.intercept_[0]) / log_model.coef_[0][0] for target_p in target_ps]
95
85
  p_colors = ["blue", "purple", "orange", "green"]
96
86
 
97
87
  for target_p, p_color, threshold in zip(target_ps, p_colors, thresholds, strict=True):
@@ -152,9 +142,7 @@ def build_review_tab():
152
142
 
153
143
  with gr.Column() as review_item_col, gr.Row():
154
144
  with gr.Column(), gr.Group():
155
- spectrogram_image = gr.Plot(
156
- label=loc.localize("review-tab-spectrogram-plot-label"), show_label=False
157
- )
145
+ spectrogram_image = gr.Plot(label=loc.localize("review-tab-spectrogram-plot-label"), show_label=False)
158
146
  spectrogram_dl_btn = gr.Button("Download spectrogram", size="sm")
159
147
 
160
148
  with gr.Column():
@@ -197,16 +185,14 @@ def build_review_tab():
197
185
 
198
186
  if not skip_plot:
199
187
  update_dict |= {
200
- species_regression_plot: create_log_plot(
201
- next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2
202
- ),
188
+ species_regression_plot: create_log_plot(next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2),
203
189
  }
204
190
 
205
191
  if next_review_state["files"]:
206
192
  next_file = next_review_state["files"][0]
207
193
  update_dict |= {
208
194
  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)),
195
+ spectrogram_image: utils.spectrogram_from_file(next_file, fig_num=MATPLOTLIB_FIGURE_ID, fig_size=(8, 4)),
210
196
  }
211
197
 
212
198
  update_dict |= {
@@ -224,9 +210,7 @@ def build_review_tab():
224
210
  no_samles_label: gr.Label(visible=not bool(next_review_state["files"])),
225
211
  review_item_col: gr.Column(visible=bool(next_review_state["files"])),
226
212
  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
213
+ visible=update_dict[species_regression_plot].constructor_args["visible"] if species_regression_plot in update_dict else False
230
214
  ),
231
215
  }
232
216
 
@@ -278,9 +262,7 @@ def build_review_tab():
278
262
  if dir_name:
279
263
  next_review_state["input_directory"] = dir_name
280
264
  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)
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)
284
266
  ]
285
267
 
286
268
  next_review_state["species_list"] = specieslist
@@ -307,9 +289,7 @@ def build_review_tab():
307
289
  if selected_species:
308
290
  next_review_state["current_species"] = selected_species
309
291
  else:
310
- next_review_state["current_species"] = (
311
- next_review_state["species_list"][0] if next_review_state["species_list"] else None
312
- )
292
+ next_review_state["current_species"] = next_review_state["species_list"][0] if next_review_state["species_list"] else None
313
293
 
314
294
  todo_files, positives, negatives = collect_files(
315
295
  os.path.join(next_review_state["input_directory"], next_review_state["current_species"])
@@ -339,9 +319,7 @@ def build_review_tab():
339
319
  len(next_review_state[NEGATIVE_LABEL_DIR]),
340
320
  ],
341
321
  ],
342
- species_regression_plot: create_log_plot(
343
- next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2
344
- ),
322
+ species_regression_plot: create_log_plot(next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2),
345
323
  }
346
324
 
347
325
  if not selected_species:
@@ -360,15 +338,13 @@ def build_review_tab():
360
338
  update_dict |= {
361
339
  review_item_col: gr.Column(visible=True),
362
340
  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)),
341
+ spectrogram_image: utils.spectrogram_from_file(todo_files[0], fig_num=MATPLOTLIB_FIGURE_ID, fig_size=(8, 4)),
364
342
  no_samles_label: gr.Label(visible=False),
365
343
  }
366
344
  else:
367
345
  update_dict |= {review_item_col: gr.Column(visible=False), no_samles_label: gr.Label(visible=True)}
368
346
 
369
- update_dict[regression_dl_btn] = gr.Button(
370
- visible=update_dict[species_regression_plot].constructor_args["visible"]
371
- )
347
+ update_dict[regression_dl_btn] = gr.Button(visible=update_dict[species_regression_plot].constructor_args["visible"])
372
348
 
373
349
  return update_dict
374
350
 
@@ -408,25 +384,6 @@ def build_review_tab():
408
384
  def toggle_autoplay(value):
409
385
  return gr.Audio(autoplay=value)
410
386
 
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
387
  autoplay_checkbox.change(toggle_autoplay, inputs=autoplay_checkbox, outputs=review_audio)
431
388
 
432
389
  review_change_output = [
@@ -446,12 +403,8 @@ def build_review_tab():
446
403
  regression_dl_btn,
447
404
  ]
448
405
 
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
- )
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)
455
408
 
456
409
  species_dropdown.change(
457
410
  select_subdir,
@@ -1,15 +1,36 @@
1
1
  import json
2
2
  import os
3
+ import sys
3
4
  from pathlib import Path
4
5
 
5
- import birdnet_analyzer.gui.utils as gu
6
+ import birdnet_analyzer.config as cfg
6
7
  from birdnet_analyzer import utils
7
8
 
9
+ if utils.FROZEN:
10
+ # divert stdout & stderr to logs.txt file since we have no console when deployed
11
+ userdir = Path.home()
12
+
13
+ if sys.platform == "win32":
14
+ userdir /= "AppData/Roaming"
15
+ elif sys.platform == "linux":
16
+ userdir /= ".local/share"
17
+ elif sys.platform == "darwin":
18
+ userdir /= "Library/Application Support"
19
+
20
+ APPDIR = userdir / "BirdNET-Analyzer-GUI"
21
+
22
+ APPDIR.mkdir(parents=True, exist_ok=True)
23
+
24
+ sys.stderr = sys.stdout = open(str(APPDIR / "logs.txt"), "a") # noqa: SIM115
25
+ cfg.ERROR_LOG_FILE = str(APPDIR / os.path.basename(cfg.ERROR_LOG_FILE))
26
+ else:
27
+ APPDIR = ""
28
+
8
29
  FALLBACK_LANGUAGE = "en"
9
30
  SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
10
- GUI_SETTINGS_PATH = os.path.join(gu.APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "gui-settings.json")
31
+ GUI_SETTINGS_PATH = os.path.join(APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "gui-settings.json")
11
32
  LANG_DIR = str(Path(SCRIPT_DIR).parent / "lang")
12
- STATE_SETTINGS_PATH = os.path.join(gu.APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "state.json")
33
+ STATE_SETTINGS_PATH = os.path.join(APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "state.json")
13
34
 
14
35
 
15
36
  def get_state_dict() -> dict:
@@ -35,7 +56,7 @@ def get_state_dict() -> dict:
35
56
  return {}
36
57
 
37
58
 
38
- def get_state(key: str, default=None) -> str:
59
+ def get_state(key: str, default=None):
39
60
  """
40
61
  Retrieves the value associated with the given key from the state dictionary.
41
62
 
@@ -7,6 +7,8 @@ import birdnet_analyzer.gui.localization as loc
7
7
  import birdnet_analyzer.gui.utils as gu
8
8
  from birdnet_analyzer import audio, utils
9
9
 
10
+ MATPLOTLIB_FIGURE_NUM = "single-file-tab-spectrogram-plot"
11
+
10
12
 
11
13
  @gu.gui_runtime_error_handler
12
14
  def run_single_file_analysis(
@@ -65,6 +67,7 @@ def run_single_file_analysis(
65
67
  custom_classifier_file,
66
68
  "csv",
67
69
  None,
70
+ False,
68
71
  locale if locale else "en",
69
72
  1,
70
73
  4,
@@ -102,9 +105,7 @@ def build_single_analysis_tab():
102
105
  audio_input = gr.Audio(type="filepath", label=loc.localize("single-audio-label"), sources=["upload"])
103
106
 
104
107
  with gr.Group():
105
- spectogram_output = gr.Plot(
106
- label=loc.localize("review-tab-spectrogram-plot-label"), visible=False, show_label=False
107
- )
108
+ spectogram_output = gr.Plot(label=loc.localize("review-tab-spectrogram-plot-label"), visible=False, show_label=False)
108
109
  generate_spectrogram_cb = gr.Checkbox(
109
110
  value=True,
110
111
  label=loc.localize("single-tab-spectrogram-checkbox-label"),
@@ -138,17 +139,13 @@ def build_single_analysis_tab():
138
139
  ) = gu.species_lists(False)
139
140
  locale_radio = gu.locale()
140
141
 
141
- single_file_analyze = gr.Button(
142
- loc.localize("analyze-start-button-label"), variant="huggingface", interactive=False
143
- )
142
+ single_file_analyze = gr.Button(loc.localize("analyze-start-button-label"), variant="huggingface", interactive=False)
144
143
 
145
144
  with gr.Row(visible=False) as action_row:
146
145
  table_download_button = gr.Button(
147
146
  loc.localize("single-tab-download-button-label"),
148
147
  )
149
- segment_audio = gr.Audio(
150
- autoplay=True, type="numpy", show_download_button=True, show_label=False, editable=False, visible=False
151
- )
148
+ segment_audio = gr.Audio(autoplay=True, type="numpy", show_download_button=True, show_label=False, editable=False, visible=False)
152
149
 
153
150
  output_dataframe = gr.Dataframe(
154
151
  type="pandas",
@@ -170,7 +167,7 @@ def build_single_analysis_tab():
170
167
  return (
171
168
  i["path"],
172
169
  gr.Audio(label=os.path.basename(i["path"])),
173
- gr.Plot(visible=True, value=utils.spectrogram_from_file(i["path"], fig_size=(20, 4)))
170
+ gr.Plot(visible=True, value=utils.spectrogram_from_file(i["path"], fig_size=(20, 4), fig_num=MATPLOTLIB_FIGURE_NUM))
174
171
  if generate_spectrogram
175
172
  else gr.Plot(visible=False),
176
173
  gr.Button(interactive=True),
@@ -183,9 +180,7 @@ def build_single_analysis_tab():
183
180
  def try_generate_spectrogram(audio_path, generate_spectrogram):
184
181
  if audio_path and generate_spectrogram:
185
182
  try:
186
- return gr.Plot(
187
- visible=True, value=utils.spectrogram_from_file(audio_path["path"], fig_size=(20, 4))
188
- )
183
+ return gr.Plot(visible=True, value=utils.spectrogram_from_file(audio_path["path"], fig_size=(20, 4), fig_num=MATPLOTLIB_FIGURE_NUM))
189
184
  except Exception as e:
190
185
  raise gr.Error(loc.localize("single-tab-generate-spectrogram-error")) from e
191
186
  else:
@@ -248,16 +243,18 @@ def build_single_analysis_tab():
248
243
  def download_table(filepath):
249
244
  if filepath:
250
245
  ext = os.path.splitext(filepath)[1]
251
- gu.save_file_dialog(
246
+ file_location = gu.save_file_dialog(
252
247
  state_key="single-file-table",
253
248
  default_filename=os.path.basename(filepath),
254
249
  filetypes=(f"{ext[1:]} (*{ext})",),
255
250
  )
256
251
 
252
+ if file_location:
253
+ with open(filepath, "rb") as src, open(file_location, "wb") as dst:
254
+ dst.write(src.read())
255
+
257
256
  output_dataframe.select(get_selected_audio, inputs=audio_path_state, outputs=segment_audio)
258
- single_file_analyze.click(
259
- run_single_file_analysis, inputs=inputs, outputs=[output_dataframe, action_row, table_path_state]
260
- )
257
+ single_file_analyze.click(run_single_file_analysis, inputs=inputs, outputs=[output_dataframe, action_row, table_path_state])
261
258
  table_download_button.click(download_table, inputs=table_path_state)
262
259
 
263
260
  return lat_number, lon_number, map_plot