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,797 +1,803 @@
1
- # ruff: noqa: PLW0603
2
- import base64
3
- import io
4
- import multiprocessing
5
- import os
6
- import sys
7
- from collections.abc import Callable
8
- from contextlib import suppress
9
- from pathlib import Path
10
-
11
- import gradio as gr
12
- import webview
13
-
14
- import birdnet_analyzer.config as cfg
15
- import birdnet_analyzer.gui.localization as loc
16
- from birdnet_analyzer import utils
17
- from birdnet_analyzer.gui import settings
18
-
19
- loc.load_local_state()
20
-
21
- SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
22
- ORIGINAL_TRANSLATED_LABELS_PATH = str(Path(SCRIPT_DIR).parent / cfg.TRANSLATED_LABELS_PATH)
23
- _CUSTOM_SPECIES = loc.localize("species-list-radio-option-custom-list")
24
- _PREDICT_SPECIES = loc.localize("species-list-radio-option-predict-list")
25
- _CUSTOM_CLASSIFIER = loc.localize("species-list-radio-option-custom-classifier")
26
- _ALL_SPECIES = loc.localize("species-list-radio-option-all")
27
- _WINDOW: webview.Window | None = None
28
- _URL = ""
29
- _HEART_LOGO = "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2IiBkYXRhLXZpZXctY29tcG9uZW50PSJ0cnVlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im04IDE0LjI1LjM0NS42NjZhLjc1Ljc1IDAgMCAxLS42OSAwbC0uMDA4LS4wMDQtLjAxOC0uMDFhNy4xNTIgNy4xNTIgMCAwIDEtLjMxLS4xNyAyMi4wNTUgMjIuMDU1IDAgMCAxLTMuNDM0LTIuNDE0QzIuMDQ1IDEwLjczMSAwIDguMzUgMCA1LjUgMCAyLjgzNiAyLjA4NiAxIDQuMjUgMSA1Ljc5NyAxIDcuMTUzIDEuODAyIDggMy4wMiA4Ljg0NyAxLjgwMiAxMC4yMDMgMSAxMS43NSAxIDEzLjkxNCAxIDE2IDIuODM2IDE2IDUuNWMwIDIuODUtMi4wNDUgNS4yMzEtMy44ODUgNi44MThhMjIuMDY2IDIyLjA2NiAwIDAgMS0zLjc0NCAyLjU4NGwtLjAxOC4wMS0uMDA2LjAwM2gtLjAwMlpNNC4yNSAyLjVjLTEuMzM2IDAtMi43NSAxLjE2NC0yLjc1IDMgMCAyLjE1IDEuNTggNC4xNDQgMy4zNjUgNS42ODJBMjAuNTggMjAuNTggMCAwIDAgOCAxMy4zOTNhMjAuNTggMjAuNTggMCAwIDAgMy4xMzUtMi4yMTFDMTIuOTIgOS42NDQgMTQuNSA3LjY1IDE0LjUgNS41YzAtMS44MzYtMS40MTQtMy0yLjc1LTMtMS4zNzMgMC0yLjYwOS45ODYtMy4wMjkgMi40NTZhLjc0OS43NDkgMCAwIDEtMS40NDIgMEM2Ljg1OSAzLjQ4NiA1LjYyMyAyLjUgNC4yNSAyLjVaIj48L3BhdGg+DQo8L3N2Zz4=" # noqa: E501
30
-
31
-
32
- def gui_runtime_error_handler(f):
33
- """
34
- A decorator function to handle errors during the execution of a callable.
35
-
36
- This function attempts to execute the provided callable `f`. If an exception
37
- occurs during execution, it logs the error using `utils.write_error_log` and
38
- raises a `gr.Error` exception.
39
-
40
- Args:
41
- f (callable): The function or callable object to be executed.
42
-
43
- Returns:
44
- The result of the callable `f` if no exception occurs.
45
-
46
- Raises:
47
- gr.Error: If an exception is raised during the execution of `f`.
48
- """
49
-
50
- def wrapper(*args, **kwargs):
51
- try:
52
- return f(*args, **kwargs)
53
- except Exception as e:
54
- utils.write_error_log(e)
55
- raise gr.Error(message=str(e), duration=None) from e
56
-
57
- return wrapper
58
-
59
-
60
- # Nishant - Following two functions (select_folder and get_files_and_durations) are written for Folder selection
61
- def select_folder(state_key=None):
62
- """
63
- Opens a folder selection dialog and returns the selected folder path.
64
- On Windows, it uses tkinter's filedialog to open the folder selection dialog.
65
- On other platforms, it uses webview's FOLDER_DIALOG to open the folder selection dialog.
66
- If a state_key is provided, the initial directory for the dialog is retrieved from the state.
67
- If a folder is selected and a state_key is provided, the selected folder path is saved to the state.
68
- Args:
69
- state_key (str, optional): The key to retrieve and save the folder path in the state. Defaults to None.
70
- Returns:
71
- str: The path of the selected folder, or None if no folder was selected.
72
- """
73
- if sys.platform == "win32":
74
- from tkinter import Tk, filedialog
75
-
76
- tk = Tk()
77
- tk.withdraw()
78
-
79
- initial_dir = settings.get_state(state_key, None) if state_key else None
80
- folder_selected = filedialog.askdirectory(initialdir=initial_dir)
81
-
82
- tk.destroy()
83
- else:
84
- initial_dir = settings.get_state(state_key, "") if state_key else ""
85
- dirname = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG, directory=initial_dir)
86
- folder_selected = dirname[0] if dirname else None
87
-
88
- if folder_selected and state_key:
89
- settings.set_state(state_key, folder_selected)
90
-
91
- return folder_selected
92
-
93
-
94
- def get_audio_files_and_durations(folder, max_files=None):
95
- """
96
- Collects audio files from a specified folder and retrieves their durations.
97
- Args:
98
- folder (str): The path to the folder containing audio files.
99
- max_files (int, optional): The maximum number of files to collect. If None, all files are collected.
100
- Returns:
101
- list: A list of lists, where each inner list contains the relative file path and its duration as a string.
102
- """
103
- import librosa
104
-
105
- files_and_durations = []
106
- files = utils.collect_audio_files(folder, max_files=max_files) # Use the collect_audio_files function
107
-
108
- for file_path in files:
109
- try:
110
- duration = format_seconds(librosa.get_duration(path=file_path))
111
-
112
- except Exception as _:
113
- duration = "0:00" # Default value in case of an error
114
-
115
- files_and_durations.append([os.path.relpath(file_path, folder), duration])
116
- return files_and_durations
117
-
118
-
119
- def set_window(window):
120
- """
121
- Sets the global _WINDOW variable to the provided window object.
122
-
123
- Args:
124
- window: The window object to be set as the global _WINDOW.
125
- """
126
- global _WINDOW
127
- _WINDOW = window
128
-
129
-
130
- def validate(value, msg):
131
- """Checks if the value ist not falsy.
132
-
133
- If the value is falsy, an error will be raised.
134
-
135
- Args:
136
- value: Value to be tested.
137
- msg: Message in case of an error.
138
- """
139
- if not value:
140
- raise gr.Error(msg)
141
-
142
-
143
- def format_seconds(secs: float):
144
- """Formats a number of seconds into a string.
145
-
146
- Formats the seconds into the format "h:mm:ss.ms"
147
-
148
- Args:
149
- secs: Number of seconds.
150
-
151
- Returns:
152
- A string with the formatted seconds.
153
- """
154
- hours, secs = divmod(secs, 3600)
155
- minutes, secs = divmod(secs, 60)
156
-
157
- return f"{hours:2.0f}:{minutes:02.0f}:{secs:06.3f}"
158
-
159
-
160
- def select_directory(collect_files=True, max_files=None, state_key=None):
161
- """Shows a directory selection system dialog.
162
-
163
- Uses the pywebview to create a system dialog.
164
-
165
- Args:
166
- collect_files: If True, also lists a files inside the directory.
167
-
168
- Returns:
169
- If collect_files==True, returns (directory path, list of (relative file path, audio length))
170
- else just the directory path.
171
- All values will be None of the dialog is cancelled.
172
- """
173
- import librosa
174
-
175
- dir_name = select_folder(state_key=state_key)
176
-
177
- if collect_files:
178
- if not dir_name:
179
- return None, None
180
-
181
- files = utils.collect_audio_files(dir_name, max_files=max_files)
182
-
183
- return dir_name, [[os.path.relpath(file, dir_name), format_seconds(librosa.get_duration(filename=file))] for file in files]
184
-
185
- return dir_name if dir_name else None
186
-
187
-
188
- def build_header():
189
- with gr.Row():
190
- gr.Markdown(
191
- f"""
192
- <div style='display: flex; align-items: center;'>
193
- <img src='data:image/png;base64,{utils.img2base64(os.path.join(SCRIPT_DIR, "assets/img/birdnet_logo.png"))}'
194
- style='width: 50px; height: 50px; margin-right: 10px;'>
195
- <h2>BirdNET Analyzer</h2>
196
- </div>
197
- """
198
- )
199
-
200
-
201
- def build_footer():
202
- with gr.Row():
203
- gr.Markdown(
204
- f"""
205
- <div style='display: flex; justify-content: space-around; align-items: center; padding: 10px; text-align: center'>
206
- <div>
207
- <div style="display: flex;flex-direction: row;">GUI version:&nbsp<span
208
- id="current-version">{os.environ["GUI_VERSION"] if utils.FROZEN else "main"}</span><span
209
- style="display: none" id="update-available"><a>+</a></span></div>
210
- <div>Model version: {cfg.MODEL_VERSION}</div>
211
- </div>
212
- <div>K. Lisa Yang Center for Conservation Bioacoustics<br>Chemnitz University of Technology</div>
213
- <div>{loc.localize("footer-help")}:&nbsp;<a href='https://birdnet.cornell.edu/analyzer'
214
- target='_blank'>birdnet.cornell.edu/analyzer</a>
215
- <br><img id='heart' src='{_HEART_LOGO}'>{loc.localize("footer-support")}: <a href='https://birdnet.cornell.edu/donate' target='_blank'>birdnet.cornell.edu/donate</a>
216
- </div>
217
-
218
- </div>""" # noqa: E501
219
- )
220
-
221
-
222
- def build_settings():
223
- with gr.Tab(loc.localize("settings-tab-title")) as settings_tab:
224
- with gr.Group():
225
- with gr.Row():
226
- options = [lang.rsplit(".", 1)[0] for lang in os.listdir(loc.LANGUAGE_DIR) if lang.endswith(".json")]
227
- languages_dropdown = gr.Dropdown(
228
- options,
229
- value=loc.TARGET_LANGUAGE,
230
- label=loc.localize("settings-tab-language-dropdown-label"),
231
- info=loc.localize("settings-tab-language-dropdown-info"),
232
- interactive=True,
233
- )
234
-
235
- with gr.Row():
236
- theme_radio = gr.Radio(
237
- [
238
- (loc.localize("settings-tab-theme-dropdown-dark-option"), "dark"),
239
- (loc.localize("settings-tab-theme-dropdown-light-option"), "light"),
240
- ],
241
- value=lambda: settings.theme(),
242
- label=loc.localize("settings-tab-theme-dropdown-label"),
243
- info="⚠️" + loc.localize("settings-tab-theme-dropdown-info"),
244
- interactive=True,
245
- scale=10,
246
- )
247
-
248
- gr.Markdown(
249
- """
250
- If you encounter a bug or error, please provide the error log.\n
251
- You can submit an issue on our [GitHub](https://github.com/birdnet-team/BirdNET-Analyzer/issues).
252
- """,
253
- label=loc.localize("settings-tab-error-log-textbox-label"),
254
- elem_classes="mh-200",
255
- )
256
-
257
- error_log_tb = gr.TextArea(
258
- label=loc.localize("settings-tab-error-log-textbox-label"),
259
- info=f"{loc.localize('settings-tab-error-log-textbox-info-path')}: {cfg.ERROR_LOG_FILE}",
260
- interactive=False,
261
- placeholder=loc.localize("settings-tab-error-log-textbox-placeholder"),
262
- show_copy_button=True,
263
- )
264
-
265
- def on_language_change(value):
266
- loc.set_language(value)
267
- gr.Warning(loc.localize("settings-tab-language-dropdown-info"))
268
-
269
- def on_theme_change(value):
270
- prev_theme = settings.theme()
271
- if prev_theme != value:
272
- settings.set_setting("theme", value)
273
- _WINDOW.load_url(_URL.rstrip("/") + f"?__theme={value}")
274
-
275
- def on_tab_select(value: gr.SelectData):
276
- if value.selected and os.path.exists(cfg.ERROR_LOG_FILE):
277
- with open(cfg.ERROR_LOG_FILE, encoding="utf-8") as f:
278
- lines = f.readlines()
279
- last_100_lines = lines[-100:]
280
- return "".join(last_100_lines)
281
-
282
- return ""
283
-
284
- languages_dropdown.input(on_language_change, inputs=languages_dropdown, show_progress=False)
285
- theme_radio.input(on_theme_change, inputs=theme_radio, show_progress=False)
286
- settings_tab.select(on_tab_select, outputs=error_log_tb, show_progress=False)
287
-
288
-
289
- def sample_sliders(opened=True):
290
- """Creates the gradio accordion for the inference settings.
291
-
292
- Args:
293
- opened: If True the accordion is open on init.
294
-
295
- Returns:
296
- A tuple with the created elements:
297
- (Slider (min confidence), Slider (sensitivity), Slider (overlap),
298
- Slider (audio speed), Number (fmin), Number (fmax))
299
- """
300
- with gr.Accordion(loc.localize("inference-settings-accordion-label"), open=opened):
301
- with gr.Group():
302
- with gr.Row():
303
- use_top_n_checkbox = gr.Checkbox(
304
- label=loc.localize("inference-settings-use-top-n-checkbox-label"),
305
- value=False,
306
- info=loc.localize("inference-settings-use-top-n-checkbox-info"),
307
- )
308
- top_n_input = gr.Number(
309
- value=5,
310
- minimum=1,
311
- precision=1,
312
- visible=False,
313
- label=loc.localize("inference-settings-top-n-number-label"),
314
- info=loc.localize("inference-settings-top-n-number-info"),
315
- )
316
- confidence_slider = gr.Slider(
317
- minimum=0.05,
318
- maximum=0.95,
319
- value=cfg.MIN_CONFIDENCE,
320
- step=0.05,
321
- label=loc.localize("inference-settings-confidence-slider-label"),
322
- info=loc.localize("inference-settings-confidence-slider-info"),
323
- )
324
-
325
- use_top_n_checkbox.change(
326
- lambda use_top_n: (gr.Number(visible=use_top_n), gr.Slider(visible=not use_top_n)),
327
- inputs=use_top_n_checkbox,
328
- outputs=[top_n_input, confidence_slider],
329
- show_progress=False,
330
- )
331
-
332
- with gr.Row():
333
- sensitivity_slider = gr.Slider(
334
- minimum=0.75,
335
- maximum=1.25,
336
- value=cfg.SIGMOID_SENSITIVITY,
337
- step=0.01,
338
- label=loc.localize("inference-settings-sensitivity-slider-label"),
339
- info=loc.localize("inference-settings-sensitivity-slider-info"),
340
- )
341
- overlap_slider = gr.Slider(
342
- minimum=0,
343
- maximum=2.9,
344
- value=cfg.SIG_OVERLAP,
345
- step=0.1,
346
- label=loc.localize("inference-settings-overlap-slider-label"),
347
- info=loc.localize("inference-settings-overlap-slider-info"),
348
- )
349
-
350
- with gr.Row():
351
- merge_consecutive_slider = gr.Slider(
352
- minimum=1,
353
- maximum=10,
354
- value=cfg.MERGE_CONSECUTIVE,
355
- step=1,
356
- label=loc.localize("inference-settings-merge-consecutive-slider-label"),
357
- info=loc.localize("inference-settings-merge-consecutive-slider-info"),
358
- )
359
- audio_speed_slider = gr.Slider(
360
- minimum=-10,
361
- maximum=10,
362
- value=cfg.AUDIO_SPEED,
363
- step=1,
364
- label=loc.localize("inference-settings-audio-speed-slider-label"),
365
- info=loc.localize("inference-settings-audio-speed-slider-info"),
366
- )
367
-
368
- with gr.Row():
369
- fmin_number = gr.Number(
370
- cfg.SIG_FMIN,
371
- minimum=0,
372
- label=loc.localize("inference-settings-fmin-number-label"),
373
- info=loc.localize("inference-settings-fmin-number-info"),
374
- )
375
-
376
- fmax_number = gr.Number(
377
- cfg.SIG_FMAX,
378
- minimum=0,
379
- label=loc.localize("inference-settings-fmax-number-label"),
380
- info=loc.localize("inference-settings-fmax-number-info"),
381
- )
382
-
383
- return (
384
- use_top_n_checkbox,
385
- top_n_input,
386
- confidence_slider,
387
- sensitivity_slider,
388
- overlap_slider,
389
- merge_consecutive_slider,
390
- audio_speed_slider,
391
- fmin_number,
392
- fmax_number,
393
- )
394
-
395
-
396
- def locale():
397
- """Creates the gradio elements for locale selection
398
-
399
- Reads the translated labels inside the checkpoints directory.
400
-
401
- Returns:
402
- The dropdown element.
403
- """
404
- label_files = os.listdir(ORIGINAL_TRANSLATED_LABELS_PATH)
405
- options = ["EN"] + [label_file.split("BirdNET_GLOBAL_6K_V2.4_Labels_", 1)[1].split(".txt")[0].upper() for label_file in label_files]
406
-
407
- return gr.Dropdown(
408
- options,
409
- value="EN",
410
- label=loc.localize("analyze-locale-dropdown-label"),
411
- info=loc.localize("analyze-locale-dropdown-info"),
412
- )
413
-
414
-
415
- def plot_map_scatter_mapbox(lat, lon, zoom=4):
416
- import plotly.express as px
417
-
418
- fig = px.scatter_map(lat=[lat], lon=[lon], zoom=zoom, map_style="open-street-map", size=[10])
419
- # fig.update_traces(marker=dict(size=10, color="red")) # Explicitly set color and size
420
- fig.update_layout(margin={"r": 0, "t": 0, "l": 0, "b": 0})
421
- return fig
422
-
423
-
424
- def species_list_coordinates(show_map=False):
425
- with gr.Row(equal_height=True):
426
- with gr.Column(scale=1), gr.Group():
427
- lat_number = gr.Slider(
428
- minimum=-90,
429
- maximum=90,
430
- value=0,
431
- step=1,
432
- label=loc.localize("species-list-coordinates-lat-number-label"),
433
- info=loc.localize("species-list-coordinates-lat-number-info"),
434
- )
435
- lon_number = gr.Slider(
436
- minimum=-180,
437
- maximum=180,
438
- value=0,
439
- step=1,
440
- label=loc.localize("species-list-coordinates-lon-number-label"),
441
- info=loc.localize("species-list-coordinates-lon-number-info"),
442
- )
443
-
444
- map_plot = gr.Plot(plot_map_scatter_mapbox(0, 0), show_label=False, scale=2, visible=show_map)
445
-
446
- lat_number.change(plot_map_scatter_mapbox, inputs=[lat_number, lon_number], outputs=map_plot, show_progress=False)
447
- lon_number.change(plot_map_scatter_mapbox, inputs=[lat_number, lon_number], outputs=map_plot, show_progress=False)
448
-
449
- with gr.Group():
450
- with gr.Row():
451
- yearlong_checkbox = gr.Checkbox(True, label=loc.localize("species-list-coordinates-yearlong-checkbox-label"))
452
- week_number = gr.Slider(
453
- minimum=1,
454
- maximum=48,
455
- value=1,
456
- step=1,
457
- interactive=False,
458
- label=loc.localize("species-list-coordinates-week-slider-label"),
459
- info=loc.localize("species-list-coordinates-week-slider-info"),
460
- )
461
-
462
- sf_thresh_number = gr.Slider(
463
- minimum=0.01,
464
- maximum=0.99,
465
- value=cfg.LOCATION_FILTER_THRESHOLD,
466
- step=0.01,
467
- label=loc.localize("species-list-coordinates-threshold-slider-label"),
468
- info=loc.localize("species-list-coordinates-threshold-slider-info"),
469
- )
470
-
471
- def on_change(use_yearlong):
472
- return gr.Slider(interactive=(not use_yearlong))
473
-
474
- yearlong_checkbox.change(on_change, inputs=yearlong_checkbox, outputs=week_number, show_progress=False)
475
-
476
- return lat_number, lon_number, week_number, sf_thresh_number, yearlong_checkbox, map_plot
477
-
478
-
479
- def save_file_dialog(filetypes=(), state_key=None, default_filename=""):
480
- """Creates a file save dialog.
481
-
482
- Args:
483
- filetypes: List of filetypes to be filtered in the dialog.
484
-
485
- Returns:
486
- The selected file or None of the dialog was canceled.
487
- """
488
- initial_selection = settings.get_state(state_key, "") if state_key else ""
489
- file = _WINDOW.create_file_dialog(webview.SAVE_DIALOG, file_types=filetypes, directory=initial_selection, save_filename=default_filename)
490
-
491
- if file:
492
- if state_key:
493
- settings.set_state(state_key, os.path.dirname(file))
494
-
495
- return file
496
-
497
- return None
498
-
499
-
500
- def select_file(filetypes=(), state_key=None):
501
- """Creates a file selection dialog.
502
-
503
- Args:
504
- filetypes: List of filetypes to be filtered in the dialog.
505
-
506
- Returns:
507
- The selected file or None of the dialog was canceled.
508
- """
509
- initial_selection = settings.get_state(state_key, "") if state_key else ""
510
- files = _WINDOW.create_file_dialog(webview.OPEN_DIALOG, file_types=filetypes, directory=initial_selection)
511
-
512
- if files:
513
- if state_key:
514
- settings.set_state(state_key, os.path.dirname(files[0]))
515
-
516
- return files[0]
517
-
518
- return None
519
-
520
-
521
- def show_species_choice(choice: str):
522
- """Sets the visibility of the species list choices.
523
-
524
- Args:
525
- choice: The label of the currently active choice.
526
-
527
- Returns:
528
- A list of [
529
- Row update,
530
- File update,
531
- Column update,
532
- Column update,
533
- ]
534
- """
535
- if choice == _CUSTOM_SPECIES:
536
- return [
537
- gr.Row(visible=False),
538
- gr.File(visible=True),
539
- gr.Column(visible=False),
540
- gr.Column(visible=False),
541
- ]
542
- if choice == _PREDICT_SPECIES:
543
- return [
544
- gr.Row(visible=True),
545
- gr.File(visible=False),
546
- gr.Column(visible=False),
547
- gr.Column(visible=False),
548
- ]
549
- if choice == _CUSTOM_CLASSIFIER:
550
- return [
551
- gr.Row(visible=False),
552
- gr.File(visible=False),
553
- gr.Column(visible=True),
554
- gr.Column(visible=False),
555
- ]
556
-
557
- return [
558
- gr.Row(visible=False),
559
- gr.File(visible=False),
560
- gr.Column(visible=False),
561
- gr.Column(visible=True),
562
- ]
563
-
564
-
565
- def species_lists(opened=True):
566
- """Creates the gradio accordion for species selection.
567
-
568
- Args:
569
- opened: If True the accordion is open on init.
570
-
571
- Returns:
572
- A tuple with the created elements:
573
- (Radio (choice), File (custom species list), Slider (lat), Slider (lon),
574
- Slider (week), Slider (threshold), Checkbox (yearlong?), State (custom classifier))
575
- """
576
- with gr.Accordion(loc.localize("species-list-accordion-label"), open=opened), gr.Row():
577
- species_list_radio = gr.Radio(
578
- [_CUSTOM_SPECIES, _PREDICT_SPECIES, _CUSTOM_CLASSIFIER, _ALL_SPECIES],
579
- value=_ALL_SPECIES,
580
- label=loc.localize("species-list-radio-label"),
581
- info=loc.localize("species-list-radio-info"),
582
- elem_classes="d-block",
583
- )
584
-
585
- with gr.Column(visible=False) as position_row:
586
- lat_number, lon_number, week_number, sf_thresh_number, yearlong_checkbox, map_plot = species_list_coordinates()
587
-
588
- species_file_input = gr.File(file_types=[".txt"], visible=False, label=loc.localize("species-list-custom-list-file-label"))
589
- empty_col = gr.Column()
590
-
591
- with gr.Column(visible=False) as custom_classifier_selector:
592
- classifier_selection_button = gr.Button(loc.localize("species-list-custom-classifier-selection-button-label"))
593
- classifier_file_input = gr.Files(file_types=[".tflite"], visible=False, interactive=False)
594
- selected_classifier_state = gr.State()
595
-
596
- def on_custom_classifier_selection_click():
597
- file = select_file(("TFLite classifier (*.tflite)",), state_key="custom_classifier_file")
598
-
599
- if file:
600
- labels = os.path.splitext(file)[0] + "_Labels.txt"
601
-
602
- if not os.path.isfile(labels):
603
- labels = file.replace("Model_FP32.tflite", "Labels.txt")
604
-
605
- return file, gr.File(value=[file, labels], visible=True)
606
-
607
- return None, None
608
-
609
- classifier_selection_button.click(
610
- on_custom_classifier_selection_click,
611
- outputs=[selected_classifier_state, classifier_file_input],
612
- show_progress=False,
613
- )
614
-
615
- species_list_radio.change(
616
- show_species_choice,
617
- inputs=[species_list_radio],
618
- outputs=[position_row, species_file_input, custom_classifier_selector, empty_col],
619
- show_progress=False,
620
- )
621
-
622
- return (
623
- species_list_radio,
624
- species_file_input,
625
- lat_number,
626
- lon_number,
627
- week_number,
628
- sf_thresh_number,
629
- yearlong_checkbox,
630
- selected_classifier_state,
631
- map_plot,
632
- )
633
-
634
-
635
- def download_plot(plot, filename=""):
636
- from PIL import Image
637
-
638
- imgdata = base64.b64decode(plot.plot.split(",", 1)[1])
639
- res = _WINDOW.create_file_dialog(
640
- webview.SAVE_DIALOG,
641
- file_types=("PNG (*.png)", "Webp (*.webp)", "JPG (*.jpg)"),
642
- save_filename=filename,
643
- )
644
-
645
- if res:
646
- if res.endswith(".webp"):
647
- with open(res, "wb") as f:
648
- f.write(imgdata)
649
- else:
650
- output_format = res.rsplit(".", 1)[-1].upper()
651
- img = Image.open(io.BytesIO(imgdata))
652
- img.save(res, output_format if output_format in ["PNG", "JPEG"] else "PNG")
653
-
654
-
655
- def _get_network_shortcuts():
656
- """
657
- Retrieves a list of network shortcut paths from the user's Network Shortcuts folder.
658
- This function accesses the Network Shortcuts folder (Nethood) on a Windows system,
659
- iterates through its contents, and attempts to resolve `.lnk` files (shortcuts)
660
- to their target paths. If successful, the resolved paths are added to the list of shortcuts.
661
- Returns:
662
- list: A list of resolved network shortcut paths.
663
- Notes:
664
- - This function uses the `pythoncom` and `win32com.shell` modules, which are part of the
665
- `pywin32` package.
666
- - Errors encountered while resolving shortcuts are printed to the console.
667
- """
668
- import pythoncom
669
- from win32com.shell import shell, shellcon # type: ignore[import]
670
-
671
- try:
672
- # https://learn.microsoft.com/de-de/windows/win32/shell/csidl
673
- # CSIDL_NETHOOD: Path to folder containing network shortcuts
674
- network_shortcuts = shell.SHGetFolderPath(0, shellcon.CSIDL_NETHOOD, None, 0)
675
- shortcuts = []
676
-
677
- for item in os.listdir(network_shortcuts):
678
- item_path = os.path.join(network_shortcuts, item)
679
-
680
- if os.path.isdir(item_path):
681
- # network shortcuts are folders containing a target.lnk file
682
- target_lnk = os.path.join(item_path, "target.lnk")
683
-
684
- if os.path.exists(target_lnk):
685
- try:
686
- # https://learn.microsoft.com/de-de/windows/win32/shell/links
687
- # CLSID_ShellLink: Class ID for Shell Link object
688
- shell_link = pythoncom.CoCreateInstance(shell.CLSID_ShellLink, None, pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink)
689
-
690
- # https://learn.microsoft.com/de-de/windows/win32/api/objidl/nn-objidl-ipersistfile
691
- # Query IPersistFile interface used to
692
- persist_file = shell_link.QueryInterface(pythoncom.IID_IPersistFile)
693
-
694
- # https://learn.microsoft.com/de-de/windows/win32/api/objidl/nf-objidl-ipersistfile-load
695
- # Load shell link file
696
- persist_file.Load(target_lnk)
697
-
698
- # https://learn.microsoft.com/de-de/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinka-getpath
699
- path_buffer, _ = shell_link.GetPath(shell.SLGP_RAWPATH)
700
-
701
- shortcuts.append(path_buffer)
702
- except Exception as e:
703
- print(f"Error reading {target_lnk}: {e}")
704
- raise e
705
-
706
- return shortcuts
707
- except Exception as e:
708
- utils.write_error_log(e)
709
- return []
710
-
711
-
712
- def _get_win_drives():
713
- from string import ascii_uppercase as UPPER_CASE
714
-
715
- return [f"{drive}:\\" for drive in UPPER_CASE] + _get_network_shortcuts()
716
-
717
-
718
- def open_window(builder: list[Callable] | Callable):
719
- """
720
- Opens a GUI window using the Gradio library and the webview module.
721
- Args:
722
- builder (list[Callable] | Callable): A callable or a list of callables that build the GUI components.
723
- """
724
- global _URL
725
- multiprocessing.freeze_support()
726
-
727
- utils.ensure_model_exists()
728
-
729
- with gr.Blocks(
730
- css=open(os.path.join(SCRIPT_DIR, "assets/gui.css")).read(),
731
- js=open(os.path.join(SCRIPT_DIR, "assets/gui.js")).read(),
732
- theme=gr.themes.Default(),
733
- analytics_enabled=False,
734
- ) as demo:
735
- build_header()
736
-
737
- map_plots = []
738
-
739
- if callable(builder):
740
- map_plots.append(builder())
741
- elif isinstance(builder, tuple | set | list):
742
- map_plots.extend(build() for build in builder)
743
-
744
- build_settings()
745
- build_footer()
746
-
747
- map_plots = [plot for plot in map_plots if plot]
748
-
749
- if map_plots:
750
- inputs = []
751
- outputs = []
752
- for lat, lon, plot in map_plots:
753
- inputs.extend([lat, lon])
754
- outputs.append(plot)
755
-
756
- def update_plots(*args):
757
- return [plot_map_scatter_mapbox(lat, lon) for lat, lon in utils.batched(args, 2, strict=True)]
758
-
759
- demo.load(update_plots, inputs=inputs, outputs=outputs)
760
-
761
- _URL = demo.queue(api_open=False).launch(
762
- prevent_thread_lock=True,
763
- quiet=True,
764
- show_api=False,
765
- enable_monitoring=False,
766
- allowed_paths=_get_win_drives() if sys.platform == "win32" else ["/"],
767
- )[1]
768
- webview.settings["ALLOW_DOWNLOADS"] = True
769
- _WINDOW = webview.create_window(
770
- "BirdNET-Analyzer",
771
- _URL.rstrip("/") + f"?__theme={settings.theme()}",
772
- width=1300,
773
- height=900,
774
- min_size=(1300, 900),
775
- )
776
- set_window(_WINDOW)
777
-
778
- with suppress(ModuleNotFoundError):
779
- import pyi_splash # type: ignore
780
-
781
- pyi_splash.close()
782
-
783
- if sys.platform == "win32":
784
- import ctypes
785
- from ctypes import wintypes
786
-
787
- from webview.platforms.winforms import BrowserView
788
-
789
- dwmapi = ctypes.windll.LoadLibrary("dwmapi")
790
- _WINDOW.events.loaded += lambda: dwmapi.DwmSetWindowAttribute(
791
- BrowserView.instances[_WINDOW.uid].Handle.ToInt32(),
792
- 20, # DWMWA_USE_IMMERSIVE_DARK_MODE
793
- ctypes.byref(ctypes.c_bool(settings.theme() == "dark")),
794
- ctypes.sizeof(wintypes.BOOL),
795
- )
796
-
797
- webview.start(private_mode=False)
1
+ # ruff: noqa: PLW0603
2
+ import base64
3
+ import io
4
+ import multiprocessing
5
+ import os
6
+ import sys
7
+ from collections.abc import Callable
8
+ from contextlib import suppress
9
+ from pathlib import Path
10
+
11
+ import gradio as gr
12
+ import webview
13
+
14
+ import birdnet_analyzer.config as cfg
15
+ import birdnet_analyzer.gui.localization as loc
16
+ from birdnet_analyzer import utils
17
+ from birdnet_analyzer.gui import settings
18
+
19
+ loc.load_local_state()
20
+
21
+ SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
22
+ ORIGINAL_TRANSLATED_LABELS_PATH = str(Path(SCRIPT_DIR).parent / cfg.TRANSLATED_LABELS_PATH)
23
+ _CUSTOM_SPECIES = loc.localize("species-list-radio-option-custom-list")
24
+ _PREDICT_SPECIES = loc.localize("species-list-radio-option-predict-list")
25
+ _CUSTOM_CLASSIFIER = loc.localize("species-list-radio-option-custom-classifier")
26
+ _ALL_SPECIES = loc.localize("species-list-radio-option-all")
27
+ _WINDOW: webview.Window | None = None
28
+ _URL = ""
29
+ _HEART_LOGO = "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHZlcnNpb249IjEuMSIgd2lkdGg9IjE2IiBkYXRhLXZpZXctY29tcG9uZW50PSJ0cnVlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxwYXRoIGQ9Im04IDE0LjI1LjM0NS42NjZhLjc1Ljc1IDAgMCAxLS42OSAwbC0uMDA4LS4wMDQtLjAxOC0uMDFhNy4xNTIgNy4xNTIgMCAwIDEtLjMxLS4xNyAyMi4wNTUgMjIuMDU1IDAgMCAxLTMuNDM0LTIuNDE0QzIuMDQ1IDEwLjczMSAwIDguMzUgMCA1LjUgMCAyLjgzNiAyLjA4NiAxIDQuMjUgMSA1Ljc5NyAxIDcuMTUzIDEuODAyIDggMy4wMiA4Ljg0NyAxLjgwMiAxMC4yMDMgMSAxMS43NSAxIDEzLjkxNCAxIDE2IDIuODM2IDE2IDUuNWMwIDIuODUtMi4wNDUgNS4yMzEtMy44ODUgNi44MThhMjIuMDY2IDIyLjA2NiAwIDAgMS0zLjc0NCAyLjU4NGwtLjAxOC4wMS0uMDA2LjAwM2gtLjAwMlpNNC4yNSAyLjVjLTEuMzM2IDAtMi43NSAxLjE2NC0yLjc1IDMgMCAyLjE1IDEuNTggNC4xNDQgMy4zNjUgNS42ODJBMjAuNTggMjAuNTggMCAwIDAgOCAxMy4zOTNhMjAuNTggMjAuNTggMCAwIDAgMy4xMzUtMi4yMTFDMTIuOTIgOS42NDQgMTQuNSA3LjY1IDE0LjUgNS41YzAtMS44MzYtMS40MTQtMy0yLjc1LTMtMS4zNzMgMC0yLjYwOS45ODYtMy4wMjkgMi40NTZhLjc0OS43NDkgMCAwIDEtMS40NDIgMEM2Ljg1OSAzLjQ4NiA1LjYyMyAyLjUgNC4yNSAyLjVaIj48L3BhdGg+DQo8L3N2Zz4=" # noqa: E501
30
+
31
+
32
+ def gui_runtime_error_handler(f):
33
+ """
34
+ A decorator function to handle errors during the execution of a callable.
35
+
36
+ This function attempts to execute the provided callable `f`. If an exception
37
+ occurs during execution, it logs the error using `utils.write_error_log` and
38
+ raises a `gr.Error` exception.
39
+
40
+ Args:
41
+ f (callable): The function or callable object to be executed.
42
+
43
+ Returns:
44
+ The result of the callable `f` if no exception occurs.
45
+
46
+ Raises:
47
+ gr.Error: If an exception is raised during the execution of `f`.
48
+ """
49
+
50
+ def wrapper(*args, **kwargs):
51
+ try:
52
+ return f(*args, **kwargs)
53
+ except Exception as e:
54
+ utils.write_error_log(e)
55
+ raise gr.Error(message=str(e), duration=None) from e
56
+
57
+ return wrapper
58
+
59
+
60
+ # Nishant - Following two functions (select_folder and get_files_and_durations) are written for Folder selection
61
+ def select_folder(state_key=None):
62
+ """
63
+ Opens a folder selection dialog and returns the selected folder path.
64
+ On Windows, it uses tkinter's filedialog to open the folder selection dialog.
65
+ On other platforms, it uses webview's FOLDER_DIALOG to open the folder selection dialog.
66
+ If a state_key is provided, the initial directory for the dialog is retrieved from the state.
67
+ If a folder is selected and a state_key is provided, the selected folder path is saved to the state.
68
+ Args:
69
+ state_key (str, optional): The key to retrieve and save the folder path in the state. Defaults to None.
70
+ Returns:
71
+ str: The path of the selected folder, or None if no folder was selected.
72
+ """
73
+ if sys.platform == "win32":
74
+ from tkinter import Tk, filedialog
75
+
76
+ tk = Tk()
77
+ tk.withdraw()
78
+
79
+ initial_dir = settings.get_state(state_key, None) if state_key else None
80
+ folder_selected = filedialog.askdirectory(initialdir=initial_dir)
81
+
82
+ tk.destroy()
83
+ else:
84
+ initial_dir = settings.get_state(state_key, "") if state_key else ""
85
+ dirname = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG, directory=initial_dir)
86
+ folder_selected = dirname[0] if dirname else None
87
+
88
+ if folder_selected and state_key:
89
+ settings.set_state(state_key, folder_selected)
90
+
91
+ return folder_selected
92
+
93
+
94
+ def get_audio_files_and_durations(folder, max_files=None):
95
+ """
96
+ Collects audio files from a specified folder and retrieves their durations.
97
+ Args:
98
+ folder (str): The path to the folder containing audio files.
99
+ max_files (int, optional): The maximum number of files to collect. If None, all files are collected.
100
+ Returns:
101
+ list: A list of lists, where each inner list contains the relative file path and its duration as a string.
102
+ """
103
+ import librosa
104
+
105
+ files_and_durations = []
106
+ files = utils.collect_audio_files(folder, max_files=max_files) # Use the collect_audio_files function
107
+
108
+ for file_path in files:
109
+ try:
110
+ duration = format_seconds(librosa.get_duration(path=file_path))
111
+
112
+ except Exception as _:
113
+ duration = "0:00" # Default value in case of an error
114
+
115
+ files_and_durations.append([os.path.relpath(file_path, folder), duration])
116
+ return files_and_durations
117
+
118
+
119
+ def set_window(window):
120
+ """
121
+ Sets the global _WINDOW variable to the provided window object.
122
+
123
+ Args:
124
+ window: The window object to be set as the global _WINDOW.
125
+ """
126
+ global _WINDOW
127
+ _WINDOW = window
128
+
129
+
130
+ def validate(value, msg):
131
+ """Checks if the value ist not falsy.
132
+
133
+ If the value is falsy, an error will be raised.
134
+
135
+ Args:
136
+ value: Value to be tested.
137
+ msg: Message in case of an error.
138
+ """
139
+ if not value:
140
+ raise gr.Error(msg)
141
+
142
+
143
+ def format_seconds(secs: float):
144
+ """Formats a number of seconds into a string.
145
+
146
+ Formats the seconds into the format "h:mm:ss.ms"
147
+
148
+ Args:
149
+ secs: Number of seconds.
150
+
151
+ Returns:
152
+ A string with the formatted seconds.
153
+ """
154
+ hours, secs = divmod(secs, 3600)
155
+ minutes, secs = divmod(secs, 60)
156
+
157
+ return f"{hours:2.0f}:{minutes:02.0f}:{secs:06.3f}"
158
+
159
+
160
+ def select_directory(collect_files=True, max_files=None, state_key=None):
161
+ """Shows a directory selection system dialog.
162
+
163
+ Uses the pywebview to create a system dialog.
164
+
165
+ Args:
166
+ collect_files: If True, also lists a files inside the directory.
167
+
168
+ Returns:
169
+ If collect_files==True, returns (directory path, list of (relative file path, audio length))
170
+ else just the directory path.
171
+ All values will be None of the dialog is cancelled.
172
+ """
173
+ import librosa
174
+
175
+ dir_name = select_folder(state_key=state_key)
176
+
177
+ if collect_files:
178
+ if not dir_name:
179
+ return None, None
180
+
181
+ files = utils.collect_audio_files(dir_name, max_files=max_files)
182
+
183
+ return dir_name, [[os.path.relpath(file, dir_name), format_seconds(librosa.get_duration(filename=file))] for file in files]
184
+
185
+ return dir_name if dir_name else None
186
+
187
+
188
+ def build_header():
189
+ with gr.Row():
190
+ gr.Markdown(
191
+ f"""
192
+ <div style='display: flex; align-items: center;'>
193
+ <img src='data:image/png;base64,{utils.img2base64(os.path.join(SCRIPT_DIR, "assets/img/birdnet_logo.png"))}'
194
+ style='width: 50px; height: 50px; margin-right: 10px;'>
195
+ <h2>BirdNET Analyzer</h2>
196
+ </div>
197
+ """
198
+ )
199
+
200
+
201
+ def build_footer():
202
+ with gr.Row():
203
+ gr.Markdown(
204
+ f"""
205
+ <div style='display: flex; justify-content: space-around; align-items: center; padding: 10px; text-align: center'>
206
+ <div>
207
+ <div style="display: flex;flex-direction: row;">GUI version:&nbsp<span
208
+ id="current-version">{os.environ["GUI_VERSION"] if utils.FROZEN else "main"}</span><span
209
+ style="display: none" id="update-available"><a>+</a></span></div>
210
+ <div>Model version: {cfg.MODEL_VERSION}</div>
211
+ </div>
212
+ <div>K. Lisa Yang Center for Conservation Bioacoustics<br>Chemnitz University of Technology</div>
213
+ <div>{loc.localize("footer-help")}:&nbsp;<a href='https://birdnet.cornell.edu/analyzer'
214
+ target='_blank'>birdnet.cornell.edu/analyzer</a>
215
+ <br><img id='heart' src='{_HEART_LOGO}'>{loc.localize("footer-support")}: <a href='https://birdnet.cornell.edu/donate' target='_blank'>birdnet.cornell.edu/donate</a>
216
+ </div>
217
+
218
+ </div>""" # noqa: E501
219
+ )
220
+
221
+
222
+ def build_settings():
223
+ with gr.Tab(loc.localize("settings-tab-title")) as settings_tab:
224
+ with gr.Group():
225
+ with gr.Row():
226
+ options = [lang.rsplit(".", 1)[0] for lang in os.listdir(loc.LANGUAGE_DIR) if lang.endswith(".json")]
227
+ languages_dropdown = gr.Dropdown(
228
+ options,
229
+ value=loc.TARGET_LANGUAGE,
230
+ label=loc.localize("settings-tab-language-dropdown-label"),
231
+ info=loc.localize("settings-tab-language-dropdown-info"),
232
+ interactive=True,
233
+ )
234
+
235
+ with gr.Row():
236
+ theme_radio = gr.Radio(
237
+ [
238
+ (loc.localize("settings-tab-theme-dropdown-dark-option"), "dark"),
239
+ (loc.localize("settings-tab-theme-dropdown-light-option"), "light"),
240
+ ],
241
+ value=lambda: settings.theme(),
242
+ label=loc.localize("settings-tab-theme-dropdown-label"),
243
+ info="⚠️" + loc.localize("settings-tab-theme-dropdown-info"),
244
+ interactive=True,
245
+ scale=10,
246
+ )
247
+
248
+ gr.Markdown(
249
+ """
250
+ If you encounter a bug or error, please provide the error log.\n
251
+ You can submit an issue on our [GitHub](https://github.com/birdnet-team/BirdNET-Analyzer/issues).
252
+ """,
253
+ label=loc.localize("settings-tab-error-log-textbox-label"),
254
+ elem_classes="mh-200",
255
+ )
256
+
257
+ error_log_tb = gr.TextArea(
258
+ label=loc.localize("settings-tab-error-log-textbox-label"),
259
+ info=f"{loc.localize('settings-tab-error-log-textbox-info-path')}: {cfg.ERROR_LOG_FILE}",
260
+ interactive=False,
261
+ placeholder=loc.localize("settings-tab-error-log-textbox-placeholder"),
262
+ show_copy_button=True,
263
+ )
264
+
265
+ def on_language_change(value):
266
+ loc.set_language(value)
267
+ gr.Warning(loc.localize("settings-tab-language-dropdown-info"))
268
+
269
+ def on_theme_change(value):
270
+ prev_theme = settings.theme()
271
+ if prev_theme != value:
272
+ settings.set_setting("theme", value)
273
+ _WINDOW.load_url(_URL.rstrip("/") + f"?__theme={value}")
274
+
275
+ def on_tab_select(value: gr.SelectData):
276
+ if value.selected and os.path.exists(cfg.ERROR_LOG_FILE):
277
+ with open(cfg.ERROR_LOG_FILE, encoding="utf-8") as f:
278
+ lines = f.readlines()
279
+ last_100_lines = lines[-100:]
280
+ return "".join(last_100_lines)
281
+
282
+ return ""
283
+
284
+ languages_dropdown.input(on_language_change, inputs=languages_dropdown, show_progress="hidden")
285
+ theme_radio.input(on_theme_change, inputs=theme_radio, show_progress="hidden")
286
+ settings_tab.select(on_tab_select, outputs=error_log_tb, show_progress="hidden")
287
+
288
+
289
+ def sample_sliders(opened=True):
290
+ """Creates the gradio accordion for the inference settings.
291
+
292
+ Args:
293
+ opened: If True the accordion is open on init.
294
+
295
+ Returns:
296
+ A tuple with the created elements:
297
+ (Slider (min confidence), Slider (sensitivity), Slider (overlap),
298
+ Slider (audio speed), Number (fmin), Number (fmax))
299
+ """
300
+ with gr.Accordion(loc.localize("inference-settings-accordion-label"), open=opened):
301
+ with gr.Group():
302
+ with gr.Row():
303
+ use_top_n_checkbox = gr.Checkbox(
304
+ label=loc.localize("inference-settings-use-top-n-checkbox-label"),
305
+ value=False,
306
+ info=loc.localize("inference-settings-use-top-n-checkbox-info"),
307
+ )
308
+ top_n_input = gr.Number(
309
+ value=5,
310
+ minimum=1,
311
+ precision=1,
312
+ visible=False,
313
+ label=loc.localize("inference-settings-top-n-number-label"),
314
+ info=loc.localize("inference-settings-top-n-number-info"),
315
+ )
316
+ confidence_slider = gr.Slider(
317
+ minimum=0.05,
318
+ maximum=0.95,
319
+ value=cfg.MIN_CONFIDENCE,
320
+ step=0.05,
321
+ label=loc.localize("inference-settings-confidence-slider-label"),
322
+ info=loc.localize("inference-settings-confidence-slider-info"),
323
+ )
324
+
325
+ use_top_n_checkbox.change(
326
+ lambda use_top_n: (gr.Number(visible=use_top_n), gr.Slider(visible=not use_top_n)),
327
+ inputs=use_top_n_checkbox,
328
+ outputs=[top_n_input, confidence_slider],
329
+ show_progress="hidden",
330
+ )
331
+
332
+ with gr.Row():
333
+ sensitivity_slider = gr.Slider(
334
+ minimum=0.75,
335
+ maximum=1.25,
336
+ value=cfg.SIGMOID_SENSITIVITY,
337
+ step=0.01,
338
+ label=loc.localize("inference-settings-sensitivity-slider-label"),
339
+ info=loc.localize("inference-settings-sensitivity-slider-info"),
340
+ )
341
+ overlap_slider = gr.Slider(
342
+ minimum=0,
343
+ maximum=2.9,
344
+ value=cfg.SIG_OVERLAP,
345
+ step=0.1,
346
+ label=loc.localize("inference-settings-overlap-slider-label"),
347
+ info=loc.localize("inference-settings-overlap-slider-info"),
348
+ )
349
+
350
+ with gr.Row():
351
+ merge_consecutive_slider = gr.Slider(
352
+ minimum=1,
353
+ maximum=10,
354
+ value=cfg.MERGE_CONSECUTIVE,
355
+ step=1,
356
+ label=loc.localize("inference-settings-merge-consecutive-slider-label"),
357
+ info=loc.localize("inference-settings-merge-consecutive-slider-info"),
358
+ )
359
+ audio_speed_slider = gr.Slider(
360
+ minimum=-10,
361
+ maximum=10,
362
+ value=cfg.AUDIO_SPEED,
363
+ step=1,
364
+ label=loc.localize("inference-settings-audio-speed-slider-label"),
365
+ info=loc.localize("inference-settings-audio-speed-slider-info"),
366
+ )
367
+
368
+ with gr.Row():
369
+ fmin_number = gr.Number(
370
+ cfg.SIG_FMIN,
371
+ minimum=0,
372
+ label=loc.localize("inference-settings-fmin-number-label"),
373
+ info=loc.localize("inference-settings-fmin-number-info"),
374
+ )
375
+
376
+ fmax_number = gr.Number(
377
+ cfg.SIG_FMAX,
378
+ minimum=0,
379
+ label=loc.localize("inference-settings-fmax-number-label"),
380
+ info=loc.localize("inference-settings-fmax-number-info"),
381
+ )
382
+
383
+ return (
384
+ use_top_n_checkbox,
385
+ top_n_input,
386
+ confidence_slider,
387
+ sensitivity_slider,
388
+ overlap_slider,
389
+ merge_consecutive_slider,
390
+ audio_speed_slider,
391
+ fmin_number,
392
+ fmax_number,
393
+ )
394
+
395
+
396
+ def locale():
397
+ """Creates the gradio elements for locale selection
398
+
399
+ Reads the translated labels inside the checkpoints directory.
400
+
401
+ Returns:
402
+ The dropdown element.
403
+ """
404
+ label_files = os.listdir(ORIGINAL_TRANSLATED_LABELS_PATH)
405
+ options = ["EN"] + [label_file.split("BirdNET_GLOBAL_6K_V2.4_Labels_", 1)[1].split(".txt")[0].upper() for label_file in label_files]
406
+
407
+ return gr.Dropdown(
408
+ options,
409
+ value="EN",
410
+ label=loc.localize("analyze-locale-dropdown-label"),
411
+ info=loc.localize("analyze-locale-dropdown-info"),
412
+ )
413
+
414
+
415
+ def plot_map_scatter_mapbox(lat, lon, zoom=4):
416
+ import plotly.express as px
417
+
418
+ fig = px.scatter_map(lat=[lat], lon=[lon], zoom=zoom, map_style="open-street-map", size=[10])
419
+ # fig.update_traces(marker=dict(size=10, color="red")) # Explicitly set color and size
420
+ fig.update_layout(margin={"r": 0, "t": 0, "l": 0, "b": 0})
421
+ return fig
422
+
423
+
424
+ def species_list_coordinates(show_map=False):
425
+ with gr.Row(equal_height=True):
426
+ with gr.Column(scale=1), gr.Group():
427
+ lat_number = gr.Slider(
428
+ minimum=-90,
429
+ maximum=90,
430
+ value=0,
431
+ step=1,
432
+ label=loc.localize("species-list-coordinates-lat-number-label"),
433
+ info=loc.localize("species-list-coordinates-lat-number-info"),
434
+ )
435
+ lon_number = gr.Slider(
436
+ minimum=-180,
437
+ maximum=180,
438
+ value=0,
439
+ step=1,
440
+ label=loc.localize("species-list-coordinates-lon-number-label"),
441
+ info=loc.localize("species-list-coordinates-lon-number-info"),
442
+ )
443
+
444
+ map_plot = gr.Plot(plot_map_scatter_mapbox(0, 0), show_label=False, scale=2, visible=show_map)
445
+
446
+ lat_number.change(plot_map_scatter_mapbox, inputs=[lat_number, lon_number], outputs=map_plot, show_progress="hidden")
447
+ lon_number.change(plot_map_scatter_mapbox, inputs=[lat_number, lon_number], outputs=map_plot, show_progress="hidden")
448
+
449
+ with gr.Group():
450
+ with gr.Row():
451
+ yearlong_checkbox = gr.Checkbox(True, label=loc.localize("species-list-coordinates-yearlong-checkbox-label"))
452
+ week_number = gr.Slider(
453
+ minimum=1,
454
+ maximum=48,
455
+ value=1,
456
+ step=1,
457
+ interactive=False,
458
+ label=loc.localize("species-list-coordinates-week-slider-label"),
459
+ info=loc.localize("species-list-coordinates-week-slider-info"),
460
+ )
461
+
462
+ sf_thresh_number = gr.Slider(
463
+ minimum=0.01,
464
+ maximum=0.99,
465
+ value=cfg.LOCATION_FILTER_THRESHOLD,
466
+ step=0.01,
467
+ label=loc.localize("species-list-coordinates-threshold-slider-label"),
468
+ info=loc.localize("species-list-coordinates-threshold-slider-info"),
469
+ )
470
+
471
+ def on_change(use_yearlong):
472
+ return gr.Slider(interactive=(not use_yearlong))
473
+
474
+ yearlong_checkbox.change(on_change, inputs=yearlong_checkbox, outputs=week_number, show_progress="hidden")
475
+
476
+ return lat_number, lon_number, week_number, sf_thresh_number, yearlong_checkbox, map_plot
477
+
478
+
479
+ def save_file_dialog(filetypes=(), state_key=None, default_filename=""):
480
+ """Creates a file save dialog.
481
+
482
+ Args:
483
+ filetypes: List of filetypes to be filtered in the dialog.
484
+
485
+ Returns:
486
+ The selected file or None of the dialog was canceled.
487
+ """
488
+ initial_selection = settings.get_state(state_key, "") if state_key else ""
489
+ file = _WINDOW.create_file_dialog(webview.SAVE_DIALOG, file_types=filetypes, directory=initial_selection, save_filename=default_filename)
490
+
491
+ if file:
492
+ if state_key:
493
+ settings.set_state(state_key, os.path.dirname(file))
494
+
495
+ return str(file)
496
+
497
+ return None
498
+
499
+
500
+ def select_file(filetypes=(), state_key=None):
501
+ """Creates a file selection dialog.
502
+
503
+ Args:
504
+ filetypes: List of filetypes to be filtered in the dialog.
505
+
506
+ Returns:
507
+ The selected file or None of the dialog was canceled.
508
+ """
509
+ initial_selection = settings.get_state(state_key, "") if state_key else ""
510
+ files = _WINDOW.create_file_dialog(webview.OPEN_DIALOG, file_types=filetypes, directory=initial_selection)
511
+
512
+ if files:
513
+ if state_key:
514
+ settings.set_state(state_key, os.path.dirname(files[0]))
515
+
516
+ return files[0]
517
+
518
+ return None
519
+
520
+
521
+ def show_species_choice(choice: str):
522
+ """Sets the visibility of the species list choices.
523
+
524
+ Args:
525
+ choice: The label of the currently active choice.
526
+
527
+ Returns:
528
+ A list of [
529
+ Row update,
530
+ File update,
531
+ Column update,
532
+ Column update,
533
+ ]
534
+ """
535
+ if choice == _CUSTOM_SPECIES:
536
+ return [
537
+ gr.Row(visible=False),
538
+ gr.File(visible=True),
539
+ gr.Column(visible=False),
540
+ gr.Column(visible=False),
541
+ ]
542
+ if choice == _PREDICT_SPECIES:
543
+ return [
544
+ gr.Row(visible=True),
545
+ gr.File(visible=False),
546
+ gr.Column(visible=False),
547
+ gr.Column(visible=False),
548
+ ]
549
+ if choice == _CUSTOM_CLASSIFIER:
550
+ return [
551
+ gr.Row(visible=False),
552
+ gr.File(visible=False),
553
+ gr.Column(visible=True),
554
+ gr.Column(visible=False),
555
+ ]
556
+
557
+ return [
558
+ gr.Row(visible=False),
559
+ gr.File(visible=False),
560
+ gr.Column(visible=False),
561
+ gr.Column(visible=True),
562
+ ]
563
+
564
+
565
+ def species_lists(opened=True):
566
+ """Creates the gradio accordion for species selection.
567
+
568
+ Args:
569
+ opened: If True the accordion is open on init.
570
+
571
+ Returns:
572
+ A tuple with the created elements:
573
+ (Radio (choice), File (custom species list), Slider (lat), Slider (lon),
574
+ Slider (week), Slider (threshold), Checkbox (yearlong?), State (custom classifier))
575
+ """
576
+ with gr.Accordion(loc.localize("species-list-accordion-label"), open=opened), gr.Row():
577
+ species_list_radio = gr.Radio(
578
+ [_CUSTOM_SPECIES, _PREDICT_SPECIES, _CUSTOM_CLASSIFIER, _ALL_SPECIES],
579
+ value=_ALL_SPECIES,
580
+ label=loc.localize("species-list-radio-label"),
581
+ info=loc.localize("species-list-radio-info"),
582
+ elem_classes="d-block",
583
+ )
584
+
585
+ with gr.Column(visible=False) as position_row:
586
+ lat_number, lon_number, week_number, sf_thresh_number, yearlong_checkbox, map_plot = species_list_coordinates()
587
+
588
+ species_file_input = gr.File(file_types=[".txt"], visible=False, label=loc.localize("species-list-custom-list-file-label"))
589
+ empty_col = gr.Column()
590
+
591
+ with gr.Column(visible=False) as custom_classifier_selector:
592
+ classifier_selection_button = gr.Button(loc.localize("species-list-custom-classifier-selection-button-label"))
593
+ classifier_file_input = gr.Files(file_types=[".tflite"], visible=False, interactive=False)
594
+ selected_classifier_state = gr.State()
595
+
596
+ def on_custom_classifier_selection_click():
597
+ file = select_file(("TFLite classifier (*.tflite)",), state_key="custom_classifier_file")
598
+
599
+ if not file:
600
+ return None, None
601
+
602
+ base_name = os.path.splitext(file)[0]
603
+ labels = base_name + "_Labels.txt"
604
+
605
+ if not os.path.isfile(labels):
606
+ labels = file.replace("Model_FP32.tflite", "Labels.txt")
607
+
608
+ if not file.endswith("Model_FP32.tflite") or not os.path.isfile(labels):
609
+ gr.Warning(loc.localize("species-list-custom-classifier-no-labelfile-warning"))
610
+
611
+ return file, gr.File(value=[file], visible=True)
612
+
613
+ return file, gr.File(value=[file, labels], visible=True)
614
+
615
+ classifier_selection_button.click(
616
+ on_custom_classifier_selection_click,
617
+ outputs=[selected_classifier_state, classifier_file_input],
618
+ show_progress="hidden",
619
+ )
620
+
621
+ species_list_radio.change(
622
+ show_species_choice,
623
+ inputs=[species_list_radio],
624
+ outputs=[position_row, species_file_input, custom_classifier_selector, empty_col],
625
+ show_progress="hidden",
626
+ )
627
+
628
+ return (
629
+ species_list_radio,
630
+ species_file_input,
631
+ lat_number,
632
+ lon_number,
633
+ week_number,
634
+ sf_thresh_number,
635
+ yearlong_checkbox,
636
+ selected_classifier_state,
637
+ map_plot,
638
+ )
639
+
640
+
641
+ def download_plot(plot, filename=""):
642
+ from PIL import Image
643
+
644
+ imgdata = base64.b64decode(plot.plot.split(",", 1)[1])
645
+ res = _WINDOW.create_file_dialog(
646
+ webview.SAVE_DIALOG,
647
+ file_types=("PNG (*.png)", "Webp (*.webp)", "JPG (*.jpg)"),
648
+ save_filename=filename,
649
+ )
650
+
651
+ if res:
652
+ if res.endswith(".webp"):
653
+ with open(res, "wb") as f:
654
+ f.write(imgdata)
655
+ else:
656
+ output_format = res.rsplit(".", 1)[-1].upper()
657
+ img = Image.open(io.BytesIO(imgdata))
658
+ img.save(res, output_format if output_format in ["PNG", "JPEG"] else "PNG")
659
+
660
+
661
+ def _get_network_shortcuts():
662
+ """
663
+ Retrieves a list of network shortcut paths from the user's Network Shortcuts folder.
664
+ This function accesses the Network Shortcuts folder (Nethood) on a Windows system,
665
+ iterates through its contents, and attempts to resolve `.lnk` files (shortcuts)
666
+ to their target paths. If successful, the resolved paths are added to the list of shortcuts.
667
+ Returns:
668
+ list: A list of resolved network shortcut paths.
669
+ Notes:
670
+ - This function uses the `pythoncom` and `win32com.shell` modules, which are part of the
671
+ `pywin32` package.
672
+ - Errors encountered while resolving shortcuts are printed to the console.
673
+ """
674
+ import pythoncom
675
+ from win32com.shell import shell, shellcon # type: ignore[import]
676
+
677
+ try:
678
+ # https://learn.microsoft.com/de-de/windows/win32/shell/csidl
679
+ # CSIDL_NETHOOD: Path to folder containing network shortcuts
680
+ network_shortcuts = shell.SHGetFolderPath(0, shellcon.CSIDL_NETHOOD, None, 0)
681
+ shortcuts = []
682
+
683
+ for item in os.listdir(network_shortcuts):
684
+ item_path = os.path.join(network_shortcuts, item)
685
+
686
+ if os.path.isdir(item_path):
687
+ # network shortcuts are folders containing a target.lnk file
688
+ target_lnk = os.path.join(item_path, "target.lnk")
689
+
690
+ if os.path.exists(target_lnk):
691
+ try:
692
+ # https://learn.microsoft.com/de-de/windows/win32/shell/links
693
+ # CLSID_ShellLink: Class ID for Shell Link object
694
+ shell_link = pythoncom.CoCreateInstance(shell.CLSID_ShellLink, None, pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink)
695
+
696
+ # https://learn.microsoft.com/de-de/windows/win32/api/objidl/nn-objidl-ipersistfile
697
+ # Query IPersistFile interface used to
698
+ persist_file = shell_link.QueryInterface(pythoncom.IID_IPersistFile)
699
+
700
+ # https://learn.microsoft.com/de-de/windows/win32/api/objidl/nf-objidl-ipersistfile-load
701
+ # Load shell link file
702
+ persist_file.Load(target_lnk)
703
+
704
+ # https://learn.microsoft.com/de-de/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinka-getpath
705
+ path_buffer, _ = shell_link.GetPath(shell.SLGP_RAWPATH)
706
+
707
+ shortcuts.append(path_buffer)
708
+ except Exception as e:
709
+ print(f"Error reading {target_lnk}: {e}")
710
+ raise e
711
+
712
+ return shortcuts
713
+ except Exception as e:
714
+ utils.write_error_log(e)
715
+ return []
716
+
717
+
718
+ def _get_win_drives():
719
+ from string import ascii_uppercase as UPPER_CASE
720
+
721
+ return [f"{drive}:\\" for drive in UPPER_CASE] + _get_network_shortcuts()
722
+
723
+
724
+ def open_window(builder: list[Callable] | Callable):
725
+ """
726
+ Opens a GUI window using the Gradio library and the webview module.
727
+ Args:
728
+ builder (list[Callable] | Callable): A callable or a list of callables that build the GUI components.
729
+ """
730
+ global _URL
731
+ multiprocessing.freeze_support()
732
+
733
+ utils.ensure_model_exists()
734
+
735
+ with gr.Blocks(
736
+ css=open(os.path.join(SCRIPT_DIR, "assets/gui.css")).read(),
737
+ js=open(os.path.join(SCRIPT_DIR, "assets/gui.js")).read(),
738
+ theme=gr.themes.Default(),
739
+ analytics_enabled=False,
740
+ ) as demo:
741
+ build_header()
742
+
743
+ map_plots = []
744
+
745
+ if callable(builder):
746
+ map_plots.append(builder())
747
+ elif isinstance(builder, tuple | set | list):
748
+ map_plots.extend(build() for build in builder)
749
+
750
+ build_settings()
751
+ build_footer()
752
+
753
+ map_plots = [plot for plot in map_plots if plot]
754
+
755
+ if map_plots:
756
+ inputs = []
757
+ outputs = []
758
+ for lat, lon, plot in map_plots:
759
+ inputs.extend([lat, lon])
760
+ outputs.append(plot)
761
+
762
+ def update_plots(*args):
763
+ return [plot_map_scatter_mapbox(lat, lon) for lat, lon in utils.batched(args, 2, strict=True)]
764
+
765
+ demo.load(update_plots, inputs=inputs, outputs=outputs)
766
+
767
+ _URL = demo.queue(api_open=False).launch(
768
+ prevent_thread_lock=True,
769
+ quiet=True,
770
+ show_api=False,
771
+ enable_monitoring=False,
772
+ allowed_paths=_get_win_drives() if sys.platform == "win32" else ["/"],
773
+ )[1]
774
+ webview.settings["ALLOW_DOWNLOADS"] = True
775
+ _WINDOW = webview.create_window(
776
+ "BirdNET-Analyzer",
777
+ _URL.rstrip("/") + f"?__theme={settings.theme()}",
778
+ width=1300,
779
+ height=900,
780
+ min_size=(1300, 900),
781
+ )
782
+ set_window(_WINDOW)
783
+
784
+ with suppress(ModuleNotFoundError):
785
+ import pyi_splash # type: ignore
786
+
787
+ pyi_splash.close()
788
+
789
+ if sys.platform == "win32":
790
+ import ctypes
791
+ from ctypes import wintypes
792
+
793
+ from webview.platforms.winforms import BrowserView
794
+
795
+ dwmapi = ctypes.windll.LoadLibrary("dwmapi")
796
+ _WINDOW.events.loaded += lambda: dwmapi.DwmSetWindowAttribute(
797
+ BrowserView.instances[_WINDOW.uid].Handle.ToInt32(),
798
+ 20, # DWMWA_USE_IMMERSIVE_DARK_MODE
799
+ ctypes.byref(ctypes.c_bool(settings.theme() == "dark")),
800
+ ctypes.sizeof(wintypes.BOOL),
801
+ )
802
+
803
+ webview.start(private_mode=False)