birdnet-analyzer 2.0.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 (117) hide show
  1. birdnet_analyzer/__init__.py +8 -0
  2. birdnet_analyzer/analyze/__init__.py +5 -0
  3. birdnet_analyzer/analyze/__main__.py +4 -0
  4. birdnet_analyzer/analyze/cli.py +25 -0
  5. birdnet_analyzer/analyze/core.py +245 -0
  6. birdnet_analyzer/analyze/utils.py +701 -0
  7. birdnet_analyzer/audio.py +372 -0
  8. birdnet_analyzer/cli.py +707 -0
  9. birdnet_analyzer/config.py +242 -0
  10. birdnet_analyzer/eBird_taxonomy_codes_2021E.json +25280 -0
  11. birdnet_analyzer/embeddings/__init__.py +4 -0
  12. birdnet_analyzer/embeddings/__main__.py +3 -0
  13. birdnet_analyzer/embeddings/cli.py +13 -0
  14. birdnet_analyzer/embeddings/core.py +70 -0
  15. birdnet_analyzer/embeddings/utils.py +193 -0
  16. birdnet_analyzer/evaluation/__init__.py +195 -0
  17. birdnet_analyzer/evaluation/__main__.py +3 -0
  18. birdnet_analyzer/gui/__init__.py +23 -0
  19. birdnet_analyzer/gui/__main__.py +3 -0
  20. birdnet_analyzer/gui/analysis.py +174 -0
  21. birdnet_analyzer/gui/assets/arrow_down.svg +4 -0
  22. birdnet_analyzer/gui/assets/arrow_left.svg +4 -0
  23. birdnet_analyzer/gui/assets/arrow_right.svg +4 -0
  24. birdnet_analyzer/gui/assets/arrow_up.svg +4 -0
  25. birdnet_analyzer/gui/assets/gui.css +29 -0
  26. birdnet_analyzer/gui/assets/gui.js +94 -0
  27. birdnet_analyzer/gui/assets/img/birdnet-icon.ico +0 -0
  28. birdnet_analyzer/gui/assets/img/birdnet_logo.png +0 -0
  29. birdnet_analyzer/gui/assets/img/birdnet_logo_no_transparent.png +0 -0
  30. birdnet_analyzer/gui/assets/img/clo-logo-bird.svg +1 -0
  31. birdnet_analyzer/gui/embeddings.py +620 -0
  32. birdnet_analyzer/gui/evaluation.py +813 -0
  33. birdnet_analyzer/gui/localization.py +68 -0
  34. birdnet_analyzer/gui/multi_file.py +246 -0
  35. birdnet_analyzer/gui/review.py +527 -0
  36. birdnet_analyzer/gui/segments.py +191 -0
  37. birdnet_analyzer/gui/settings.py +129 -0
  38. birdnet_analyzer/gui/single_file.py +269 -0
  39. birdnet_analyzer/gui/species.py +95 -0
  40. birdnet_analyzer/gui/train.py +698 -0
  41. birdnet_analyzer/gui/utils.py +808 -0
  42. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_af.txt +6522 -0
  43. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ar.txt +6522 -0
  44. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_bg.txt +6522 -0
  45. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ca.txt +6522 -0
  46. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_cs.txt +6522 -0
  47. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_da.txt +6522 -0
  48. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_de.txt +6522 -0
  49. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_el.txt +6522 -0
  50. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_en_uk.txt +6522 -0
  51. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_es.txt +6522 -0
  52. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_fi.txt +6522 -0
  53. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_fr.txt +6522 -0
  54. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_he.txt +6522 -0
  55. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_hr.txt +6522 -0
  56. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_hu.txt +6522 -0
  57. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_in.txt +6522 -0
  58. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_is.txt +6522 -0
  59. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_it.txt +6522 -0
  60. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ja.txt +6522 -0
  61. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ko.txt +6522 -0
  62. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_lt.txt +6522 -0
  63. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ml.txt +6522 -0
  64. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_nl.txt +6522 -0
  65. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_no.txt +6522 -0
  66. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pl.txt +6522 -0
  67. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pt_BR.txt +6522 -0
  68. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pt_PT.txt +6522 -0
  69. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ro.txt +6522 -0
  70. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ru.txt +6522 -0
  71. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sk.txt +6522 -0
  72. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sl.txt +6522 -0
  73. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sr.txt +6522 -0
  74. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sv.txt +6522 -0
  75. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_th.txt +6522 -0
  76. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_tr.txt +6522 -0
  77. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_uk.txt +6522 -0
  78. birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_zh.txt +6522 -0
  79. birdnet_analyzer/lang/de.json +335 -0
  80. birdnet_analyzer/lang/en.json +335 -0
  81. birdnet_analyzer/lang/fi.json +335 -0
  82. birdnet_analyzer/lang/fr.json +335 -0
  83. birdnet_analyzer/lang/id.json +335 -0
  84. birdnet_analyzer/lang/pt-br.json +335 -0
  85. birdnet_analyzer/lang/ru.json +335 -0
  86. birdnet_analyzer/lang/se.json +335 -0
  87. birdnet_analyzer/lang/tlh.json +335 -0
  88. birdnet_analyzer/lang/zh_TW.json +335 -0
  89. birdnet_analyzer/model.py +1243 -0
  90. birdnet_analyzer/search/__init__.py +3 -0
  91. birdnet_analyzer/search/__main__.py +3 -0
  92. birdnet_analyzer/search/cli.py +12 -0
  93. birdnet_analyzer/search/core.py +78 -0
  94. birdnet_analyzer/search/utils.py +111 -0
  95. birdnet_analyzer/segments/__init__.py +3 -0
  96. birdnet_analyzer/segments/__main__.py +3 -0
  97. birdnet_analyzer/segments/cli.py +14 -0
  98. birdnet_analyzer/segments/core.py +78 -0
  99. birdnet_analyzer/segments/utils.py +394 -0
  100. birdnet_analyzer/species/__init__.py +3 -0
  101. birdnet_analyzer/species/__main__.py +3 -0
  102. birdnet_analyzer/species/cli.py +14 -0
  103. birdnet_analyzer/species/core.py +35 -0
  104. birdnet_analyzer/species/utils.py +75 -0
  105. birdnet_analyzer/train/__init__.py +3 -0
  106. birdnet_analyzer/train/__main__.py +3 -0
  107. birdnet_analyzer/train/cli.py +14 -0
  108. birdnet_analyzer/train/core.py +113 -0
  109. birdnet_analyzer/train/utils.py +847 -0
  110. birdnet_analyzer/translate.py +104 -0
  111. birdnet_analyzer/utils.py +419 -0
  112. birdnet_analyzer-2.0.0.dist-info/METADATA +129 -0
  113. birdnet_analyzer-2.0.0.dist-info/RECORD +117 -0
  114. birdnet_analyzer-2.0.0.dist-info/WHEEL +5 -0
  115. birdnet_analyzer-2.0.0.dist-info/entry_points.txt +11 -0
  116. birdnet_analyzer-2.0.0.dist-info/licenses/LICENSE +19 -0
  117. birdnet_analyzer-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,191 @@
1
+ import concurrent.futures
2
+ import os
3
+ from functools import partial
4
+
5
+ import gradio as gr
6
+
7
+ import birdnet_analyzer.config as cfg
8
+ import birdnet_analyzer.gui.utils as gu
9
+ import birdnet_analyzer.gui.localization as loc
10
+
11
+ from birdnet_analyzer.segments.utils import extract_segments
12
+
13
+ def extract_segments_wrapper(entry):
14
+ return (entry[0][0], extract_segments(entry))
15
+
16
+
17
+ @gu.gui_runtime_error_handler
18
+ def _extract_segments(
19
+ audio_dir, result_dir, output_dir, min_conf, num_seq, audio_speed, seq_length, threads, progress=gr.Progress()
20
+ ):
21
+ from birdnet_analyzer.segments.utils import parse_folders, parse_files
22
+
23
+ gu.validate(audio_dir, loc.localize("validation-no-audio-directory-selected"))
24
+
25
+ if not result_dir:
26
+ result_dir = audio_dir
27
+
28
+ if not output_dir:
29
+ output_dir = audio_dir
30
+
31
+ if progress is not None:
32
+ progress(0, desc=f"{loc.localize('progress-search')} ...")
33
+
34
+ # Parse audio and result folders
35
+ cfg.FILE_LIST = parse_folders(audio_dir, result_dir)
36
+
37
+ # Set output folder
38
+ cfg.OUTPUT_PATH = output_dir
39
+
40
+ # Set number of threads
41
+ cfg.CPU_THREADS = int(threads)
42
+
43
+ # Set confidence threshold
44
+ cfg.MIN_CONFIDENCE = max(0.01, min(0.99, min_conf))
45
+
46
+ # Parse file list and make list of segments
47
+ cfg.FILE_LIST = parse_files(cfg.FILE_LIST, max(1, int(num_seq)))
48
+
49
+ # Audio speed
50
+ cfg.AUDIO_SPEED = max(0.1, 1.0 / (audio_speed * -1)) if audio_speed < 0 else max(1.0, float(audio_speed))
51
+
52
+ # Add config items to each file list entry.
53
+ # We have to do this for Windows which does not
54
+ # support fork() and thus each process has to
55
+ # have its own config. USE LINUX!
56
+ # flist = [(entry, max(cfg.SIG_LENGTH, float(seq_length)), cfg.getConfig()) for entry in cfg.FILE_LIST]
57
+ flist = [(entry, float(seq_length), cfg.get_config()) for entry in cfg.FILE_LIST]
58
+
59
+ result_list = []
60
+
61
+ # Extract segments
62
+ if cfg.CPU_THREADS < 2:
63
+ for i, entry in enumerate(flist):
64
+ result = extract_segments_wrapper(entry)
65
+ result_list.append(result)
66
+
67
+ if progress is not None:
68
+ progress((i, len(flist)), total=len(flist), unit="files")
69
+ else:
70
+ with concurrent.futures.ProcessPoolExecutor(max_workers=cfg.CPU_THREADS) as executor:
71
+ futures = (executor.submit(extract_segments_wrapper, arg) for arg in flist)
72
+ for i, f in enumerate(concurrent.futures.as_completed(futures), start=1):
73
+ if progress is not None:
74
+ progress((i, len(flist)), total=len(flist), unit="files")
75
+ result = f.result()
76
+
77
+ result_list.append(result)
78
+
79
+ return [[os.path.relpath(r[0], audio_dir), r[1]] for r in result_list]
80
+
81
+
82
+ def build_segments_tab():
83
+ with gr.Tab(loc.localize("segments-tab-title")):
84
+ audio_directory_state = gr.State()
85
+ result_directory_state = gr.State()
86
+ output_directory_state = gr.State()
87
+
88
+ def select_directory_to_state_and_tb(state_key):
89
+ return (gu.select_directory(collect_files=False, state_key=state_key),) * 2
90
+
91
+ with gr.Row():
92
+ select_audio_directory_btn = gr.Button(
93
+ loc.localize("segments-tab-select-audio-input-directory-button-label")
94
+ )
95
+ selected_audio_directory_tb = gr.Textbox(show_label=False, interactive=False)
96
+ select_audio_directory_btn.click(
97
+ partial(select_directory_to_state_and_tb, state_key="segments-audio-dir"),
98
+ outputs=[selected_audio_directory_tb, audio_directory_state],
99
+ show_progress=False,
100
+ )
101
+
102
+ with gr.Row():
103
+ select_result_directory_btn = gr.Button(
104
+ loc.localize("segments-tab-select-results-input-directory-button-label")
105
+ )
106
+ selected_result_directory_tb = gr.Textbox(
107
+ show_label=False,
108
+ interactive=False,
109
+ placeholder=loc.localize("segments-tab-results-input-textbox-placeholder"),
110
+ )
111
+ select_result_directory_btn.click(
112
+ partial(select_directory_to_state_and_tb, state_key="segments-result-dir"),
113
+ outputs=[result_directory_state, selected_result_directory_tb],
114
+ show_progress=False,
115
+ )
116
+
117
+ with gr.Row():
118
+ select_output_directory_btn = gr.Button(loc.localize("segments-tab-output-selection-button-label"))
119
+ selected_output_directory_tb = gr.Textbox(
120
+ show_label=False,
121
+ interactive=False,
122
+ placeholder=loc.localize("segments-tab-output-selection-textbox-placeholder"),
123
+ )
124
+ select_output_directory_btn.click(
125
+ partial(select_directory_to_state_and_tb, state_key="segments-output-dir"),
126
+ outputs=[selected_output_directory_tb, output_directory_state],
127
+ show_progress=False,
128
+ )
129
+
130
+ min_conf_slider = gr.Slider(
131
+ minimum=0.1,
132
+ maximum=0.99,
133
+ step=0.01,
134
+ value=cfg.MIN_CONFIDENCE,
135
+ label=loc.localize("segments-tab-min-confidence-slider-label"),
136
+ info=loc.localize("segments-tab-min-confidence-slider-info"),
137
+ )
138
+ num_seq_number = gr.Number(
139
+ 100,
140
+ label=loc.localize("segments-tab-max-seq-number-label"),
141
+ info=loc.localize("segments-tab-max-seq-number-info"),
142
+ minimum=1,
143
+ )
144
+ audio_speed_slider = gr.Slider(
145
+ minimum=-10,
146
+ maximum=10,
147
+ value=cfg.AUDIO_SPEED,
148
+ step=1,
149
+ label=loc.localize("inference-settings-audio-speed-slider-label"),
150
+ info=loc.localize("inference-settings-audio-speed-slider-info"),
151
+ )
152
+ seq_length_number = gr.Number(
153
+ cfg.SIG_LENGTH,
154
+ label=loc.localize("segments-tab-seq-length-number-label"),
155
+ info=loc.localize("segments-tab-seq-length-number-info"),
156
+ minimum=0.1,
157
+ )
158
+ threads_number = gr.Number(
159
+ 4,
160
+ label=loc.localize("segments-tab-threads-number-label"),
161
+ info=loc.localize("segments-tab-threads-number-info"),
162
+ minimum=1,
163
+ )
164
+
165
+ extract_segments_btn = gr.Button(loc.localize("segments-tab-extract-button-label"), variant="huggingface")
166
+
167
+ result_grid = gr.Matrix(
168
+ headers=[
169
+ loc.localize("segments-tab-result-dataframe-column-file-header"),
170
+ loc.localize("segments-tab-result-dataframe-column-execution-header"),
171
+ ],
172
+ )
173
+
174
+ extract_segments_btn.click(
175
+ _extract_segments,
176
+ inputs=[
177
+ audio_directory_state,
178
+ result_directory_state,
179
+ output_directory_state,
180
+ min_conf_slider,
181
+ num_seq_number,
182
+ audio_speed_slider,
183
+ seq_length_number,
184
+ threads_number,
185
+ ],
186
+ outputs=result_grid,
187
+ )
188
+
189
+
190
+ if __name__ == "__main__":
191
+ gu.open_window(build_segments_tab)
@@ -0,0 +1,129 @@
1
+ import os
2
+ from pathlib import Path
3
+ import json
4
+
5
+ import birdnet_analyzer.gui.utils as gu
6
+ import birdnet_analyzer.utils as utils
7
+
8
+
9
+ FALLBACK_LANGUAGE = "en"
10
+ SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
11
+ GUI_SETTINGS_PATH = os.path.join(gu.APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "gui-settings.json")
12
+ LANG_DIR = str(Path(SCRIPT_DIR).parent / "lang")
13
+ STATE_SETTINGS_PATH = os.path.join(gu.APPDIR if utils.FROZEN else os.path.dirname(SCRIPT_DIR), "state.json")
14
+
15
+
16
+ def get_state_dict() -> dict:
17
+ """
18
+ Retrieves the state dictionary from a JSON file specified by STATE_SETTINGS_PATH.
19
+
20
+ If the file does not exist, it creates an empty JSON file and returns an empty dictionary.
21
+ If any other exception occurs during file operations, it logs the error and returns an empty dictionary.
22
+
23
+ Returns:
24
+ dict: The state dictionary loaded from the JSON file, or an empty dictionary if the file does not exist or an error occurs.
25
+ """
26
+ try:
27
+ with open(STATE_SETTINGS_PATH, "r", encoding="utf-8") as f:
28
+ return json.load(f)
29
+ except FileNotFoundError:
30
+ try:
31
+ with open(STATE_SETTINGS_PATH, "w", encoding="utf-8") as f:
32
+ json.dump({}, f)
33
+ return {}
34
+ except Exception as e:
35
+ utils.write_error_log(e)
36
+ return {}
37
+
38
+
39
+ def get_state(key: str, default=None) -> str:
40
+ """
41
+ Retrieves the value associated with the given key from the state dictionary.
42
+
43
+ Args:
44
+ key (str): The key to look up in the state dictionary.
45
+ default: The value to return if the key is not found. Defaults to None.
46
+
47
+ Returns:
48
+ str: The value associated with the key if found, otherwise the default value.
49
+ """
50
+ return get_state_dict().get(key, default)
51
+
52
+
53
+ def set_state(key: str, value: str):
54
+ """
55
+ Updates the state dictionary with the given key-value pair and writes it to a JSON file.
56
+
57
+ Args:
58
+ key (str): The key to update in the state dictionary.
59
+ value (str): The value to associate with the key in the state dictionary.
60
+ """
61
+ try:
62
+ state = get_state_dict()
63
+ state[key] = value
64
+
65
+ with open(STATE_SETTINGS_PATH, "w") as f:
66
+ json.dump(state, f, indent=4)
67
+ except Exception as e:
68
+ utils.write_error_log(e)
69
+
70
+
71
+ def ensure_settings_file():
72
+ """
73
+ Ensures that the settings file exists at the specified path. If the file does not exist,
74
+ it creates a new settings file with default settings.
75
+
76
+ If the file creation fails, the error is logged.
77
+ """
78
+ if not os.path.exists(GUI_SETTINGS_PATH):
79
+ try:
80
+ with open(GUI_SETTINGS_PATH, "w") as f:
81
+ settings = {"language-id": FALLBACK_LANGUAGE, "theme": "light"}
82
+ f.write(json.dumps(settings, indent=4))
83
+ except Exception as e:
84
+ utils.write_error_log(e)
85
+
86
+
87
+ def get_setting(key, default=None):
88
+ """
89
+ Retrieves the value associated with the given key from the settings file.
90
+
91
+ Args:
92
+ key (str): The key to look up in the settings file.
93
+ default: The value to return if the key is not found. Defaults to None.
94
+
95
+ Returns:
96
+ str: The value associated with the key if found, otherwise the default value.
97
+ """
98
+ ensure_settings_file()
99
+
100
+ try:
101
+ with open(GUI_SETTINGS_PATH, "r", encoding="utf-8") as f:
102
+ settings_dict: dict = json.load(f)
103
+
104
+ return settings_dict.get(key, default)
105
+ except FileNotFoundError:
106
+ return default
107
+
108
+
109
+ def set_setting(key, value):
110
+ ensure_settings_file()
111
+ settings_dict = {}
112
+
113
+ try:
114
+ with open(GUI_SETTINGS_PATH, "r+", encoding="utf-8") as f:
115
+ settings_dict = json.load(f)
116
+ settings_dict[key] = value
117
+ f.seek(0)
118
+ json.dump(settings_dict, f, indent=4)
119
+ f.truncate()
120
+
121
+ except FileNotFoundError:
122
+ pass
123
+
124
+
125
+ def theme():
126
+ options = ("light", "dark")
127
+ current_time = get_setting("theme", "light")
128
+
129
+ return current_time if current_time in options else "light"
@@ -0,0 +1,269 @@
1
+ import os
2
+
3
+ import gradio as gr
4
+
5
+ import birdnet_analyzer.audio as audio
6
+ import birdnet_analyzer.config as cfg
7
+ import birdnet_analyzer.gui.localization as loc
8
+ import birdnet_analyzer.gui.utils as gu
9
+ import birdnet_analyzer.utils as utils
10
+
11
+
12
+ @gu.gui_runtime_error_handler
13
+ def run_single_file_analysis(
14
+ input_path,
15
+ use_top_n,
16
+ top_n,
17
+ confidence,
18
+ sensitivity,
19
+ overlap,
20
+ merge_consecutive,
21
+ audio_speed,
22
+ fmin,
23
+ fmax,
24
+ species_list_choice,
25
+ species_list_file,
26
+ lat,
27
+ lon,
28
+ week,
29
+ use_yearlong,
30
+ sf_thresh,
31
+ custom_classifier_file,
32
+ locale,
33
+ ):
34
+ import csv
35
+ from datetime import timedelta
36
+
37
+ from birdnet_analyzer.gui.analysis import run_analysis
38
+
39
+ if species_list_choice == gu._CUSTOM_SPECIES:
40
+ gu.validate(species_list_file, loc.localize("validation-no-species-list-selected"))
41
+
42
+ gu.validate(input_path, loc.localize("validation-no-file-selected"))
43
+
44
+ if fmin is None or fmax is None or fmin < cfg.SIG_FMIN or fmax > cfg.SIG_FMAX or fmin > fmax:
45
+ raise gr.Error(f"{loc.localize('validation-no-valid-frequency')} [{cfg.SIG_FMIN}, {cfg.SIG_FMAX}]")
46
+
47
+ result_filepath = run_analysis(
48
+ input_path,
49
+ None,
50
+ use_top_n,
51
+ top_n,
52
+ confidence,
53
+ sensitivity,
54
+ overlap,
55
+ merge_consecutive,
56
+ audio_speed,
57
+ fmin,
58
+ fmax,
59
+ species_list_choice,
60
+ species_list_file,
61
+ lat,
62
+ lon,
63
+ week,
64
+ use_yearlong,
65
+ sf_thresh,
66
+ custom_classifier_file,
67
+ "csv",
68
+ None,
69
+ "en" if not locale else locale,
70
+ 1,
71
+ 4,
72
+ None,
73
+ skip_existing=False,
74
+ save_params=False,
75
+ progress=None,
76
+ )
77
+
78
+ if not result_filepath:
79
+ raise gr.Error(loc.localize("single-tab-analyze-file-error"))
80
+
81
+ # read the result file to return the data to be displayed.
82
+ with open(result_filepath, "r", encoding="utf-8") as f:
83
+ reader = csv.reader(f)
84
+ data = list(reader)
85
+ data = [lc[0:-1] for lc in data[1:]] # remove last column (file path) and first row (header)
86
+
87
+ for row in data:
88
+ for col_idx in range(2):
89
+ seconds = float(row[col_idx])
90
+ time_str = str(timedelta(seconds=seconds))
91
+
92
+ if "." in time_str:
93
+ time_str = time_str[: time_str.index(".") + 2]
94
+
95
+ row[col_idx] = time_str
96
+ row.insert(0, "▶")
97
+
98
+ return data, gr.update(visible=True), result_filepath
99
+
100
+
101
+ def build_single_analysis_tab():
102
+ with gr.Tab(loc.localize("single-tab-title")):
103
+ audio_input = gr.Audio(type="filepath", label=loc.localize("single-audio-label"), sources=["upload"])
104
+
105
+ with gr.Group():
106
+ spectogram_output = gr.Plot(
107
+ label=loc.localize("review-tab-spectrogram-plot-label"), visible=False, show_label=False
108
+ )
109
+ generate_spectrogram_cb = gr.Checkbox(
110
+ value=True,
111
+ label=loc.localize("single-tab-spectrogram-checkbox-label"),
112
+ info=loc.localize("single-tab-spectrogram-checkbox-info"),
113
+ )
114
+ audio_path_state = gr.State()
115
+ table_path_state = gr.State()
116
+
117
+ (
118
+ use_top_n,
119
+ top_n_input,
120
+ confidence_slider,
121
+ sensitivity_slider,
122
+ overlap_slider,
123
+ merge_consecutive_slider,
124
+ audio_speed_slider,
125
+ fmin_number,
126
+ fmax_number,
127
+ ) = gu.sample_sliders(False)
128
+
129
+ (
130
+ species_list_radio,
131
+ species_file_input,
132
+ lat_number,
133
+ lon_number,
134
+ week_number,
135
+ sf_thresh_number,
136
+ yearlong_checkbox,
137
+ selected_classifier_state,
138
+ map_plot,
139
+ ) = gu.species_lists(False)
140
+ locale_radio = gu.locale()
141
+
142
+ single_file_analyze = gr.Button(
143
+ loc.localize("analyze-start-button-label"), variant="huggingface", interactive=False
144
+ )
145
+
146
+ with gr.Row(visible=False) as action_row:
147
+ table_download_button = gr.Button(
148
+ loc.localize("single-tab-download-button-label"),
149
+ )
150
+ segment_audio = gr.Audio(
151
+ autoplay=True, type="numpy", show_download_button=True, show_label=False, editable=False, visible=False
152
+ )
153
+
154
+ output_dataframe = gr.Dataframe(
155
+ type="pandas",
156
+ headers=[
157
+ "",
158
+ loc.localize("single-tab-output-header-start"),
159
+ loc.localize("single-tab-output-header-end"),
160
+ loc.localize("single-tab-output-header-sci-name"),
161
+ loc.localize("single-tab-output-header-common-name"),
162
+ loc.localize("single-tab-output-header-confidence"),
163
+ ],
164
+ elem_id="single-file-output",
165
+ interactive=False,
166
+ )
167
+
168
+ def get_audio_path(i, generate_spectrogram):
169
+ if i:
170
+ try:
171
+ return (
172
+ i["path"],
173
+ gr.Audio(label=os.path.basename(i["path"])),
174
+ gr.Plot(visible=True, value=utils.spectrogram_from_file(i["path"], fig_size=(20, 4)))
175
+ if generate_spectrogram
176
+ else gr.Plot(visible=False),
177
+ gr.Button(interactive=True),
178
+ )
179
+ except:
180
+ raise gr.Error(loc.localize("single-tab-generate-spectrogram-error"))
181
+ else:
182
+ return None, None, gr.Plot(visible=False), gr.update(interactive=False)
183
+
184
+ def try_generate_spectrogram(audio_path, generate_spectrogram):
185
+ if audio_path and generate_spectrogram:
186
+ try:
187
+ return gr.Plot(
188
+ visible=True, value=utils.spectrogram_from_file(audio_path["path"], fig_size=(20, 4))
189
+ )
190
+ except:
191
+ raise gr.Error(loc.localize("single-tab-generate-spectrogram-error"))
192
+ else:
193
+ return gr.Plot()
194
+
195
+ generate_spectrogram_cb.change(
196
+ try_generate_spectrogram,
197
+ inputs=[audio_input, generate_spectrogram_cb],
198
+ outputs=spectogram_output,
199
+ preprocess=False,
200
+ )
201
+
202
+ audio_input.change(
203
+ get_audio_path,
204
+ inputs=[audio_input, generate_spectrogram_cb],
205
+ outputs=[audio_path_state, audio_input, spectogram_output, single_file_analyze],
206
+ preprocess=False,
207
+ )
208
+
209
+ inputs = [
210
+ audio_path_state,
211
+ use_top_n,
212
+ top_n_input,
213
+ confidence_slider,
214
+ sensitivity_slider,
215
+ overlap_slider,
216
+ merge_consecutive_slider,
217
+ audio_speed_slider,
218
+ fmin_number,
219
+ fmax_number,
220
+ species_list_radio,
221
+ species_file_input,
222
+ lat_number,
223
+ lon_number,
224
+ week_number,
225
+ yearlong_checkbox,
226
+ sf_thresh_number,
227
+ selected_classifier_state,
228
+ locale_radio,
229
+ ]
230
+
231
+ def time_to_seconds(time_str):
232
+ try:
233
+ hours, minutes, seconds = time_str.split(":")
234
+ total_seconds = int(hours) * 3600 + int(minutes) * 60 + float(seconds)
235
+ return total_seconds
236
+
237
+ except ValueError as e:
238
+ raise ValueError("Input must be in the format hh:mm:ss or hh:mm:ss.ssssss with numeric values.") from e
239
+
240
+ def get_selected_audio(evt: gr.SelectData, audio_path):
241
+ if evt.index[1] == 0 and evt.row_value[1] and evt.row_value[2]:
242
+ start = time_to_seconds(evt.row_value[1])
243
+ end = time_to_seconds(evt.row_value[2])
244
+
245
+ data, sr = audio.open_audio_file(audio_path, offset=start, duration=end - start)
246
+ return gr.update(visible=True, value=(sr, data))
247
+
248
+ return gr.update()
249
+
250
+ def download_table(filepath):
251
+ if filepath:
252
+ ext = os.path.splitext(filepath)[1]
253
+ gu.save_file_dialog(
254
+ state_key="single-file-table",
255
+ default_filename=os.path.basename(filepath),
256
+ filetypes=(f"{ext[1:]} (*{ext})",),
257
+ )
258
+
259
+ output_dataframe.select(get_selected_audio, inputs=audio_path_state, outputs=segment_audio)
260
+ single_file_analyze.click(
261
+ run_single_file_analysis, inputs=inputs, outputs=[output_dataframe, action_row, table_path_state]
262
+ )
263
+ table_download_button.click(download_table, inputs=table_path_state)
264
+
265
+ return lat_number, lon_number, map_plot
266
+
267
+
268
+ if __name__ == "__main__":
269
+ gu.open_window(build_single_analysis_tab)
@@ -0,0 +1,95 @@
1
+ import os
2
+
3
+ import gradio as gr
4
+
5
+ import birdnet_analyzer.config as cfg
6
+ import birdnet_analyzer.gui.utils as gu
7
+ import birdnet_analyzer.gui.localization as loc
8
+ import birdnet_analyzer.gui.settings as settings
9
+
10
+
11
+ @gu.gui_runtime_error_handler
12
+ def run_species_list(out_path, filename, lat, lon, week, use_yearlong, sf_thresh, sortby):
13
+ from birdnet_analyzer.species.utils import run
14
+
15
+ gu.validate(out_path, loc.localize("validation-no-directory-selected"))
16
+
17
+ run(
18
+ os.path.join(out_path, filename if filename else "species_list.txt"),
19
+ lat,
20
+ lon,
21
+ -1 if use_yearlong else week,
22
+ sf_thresh,
23
+ sortby,
24
+ )
25
+
26
+ gr.Info(f"{loc.localize('species-tab-finish-info')} {cfg.OUTPUT_PATH}")
27
+
28
+
29
+ def build_species_tab():
30
+ with gr.Tab(loc.localize("species-tab-title")) as species_tab:
31
+ output_directory_state = gr.State()
32
+ select_directory_btn = gr.Button(loc.localize("species-tab-select-output-directory-button-label"))
33
+ classifier_name = gr.Textbox(
34
+ "species_list.txt",
35
+ visible=False,
36
+ info=loc.localize("species-tab-filename-textbox-label"),
37
+ )
38
+
39
+ def select_directory_and_update_tb(name_tb):
40
+ dir_name = gu.select_folder(state_key="species-output-dir")
41
+
42
+ if dir_name:
43
+ settings.set_state("species-output-dir", dir_name)
44
+ return (
45
+ dir_name,
46
+ gr.Textbox(label=dir_name, visible=True, value=name_tb),
47
+ )
48
+
49
+ return None, name_tb
50
+
51
+ select_directory_btn.click(
52
+ select_directory_and_update_tb,
53
+ inputs=classifier_name,
54
+ outputs=[output_directory_state, classifier_name],
55
+ show_progress=False,
56
+ )
57
+
58
+ lat_number, lon_number, week_number, sf_thresh_number, yearlong_checkbox, map_plot = (
59
+ gu.species_list_coordinates(show_map=True)
60
+ )
61
+
62
+ sortby = gr.Radio(
63
+ [
64
+ (loc.localize("species-tab-sort-radio-option-frequency"), "freq"),
65
+ (loc.localize("species-tab-sort-radio-option-alphabetically"), "alpha"),
66
+ ],
67
+ value="freq",
68
+ label=loc.localize("species-tab-sort-radio-label"),
69
+ info=loc.localize("species-tab-sort-radio-info"),
70
+ )
71
+
72
+ start_btn = gr.Button(loc.localize("species-tab-start-button-label"), variant="huggingface")
73
+ start_btn.click(
74
+ run_species_list,
75
+ inputs=[
76
+ output_directory_state,
77
+ classifier_name,
78
+ lat_number,
79
+ lon_number,
80
+ week_number,
81
+ yearlong_checkbox,
82
+ sf_thresh_number,
83
+ sortby,
84
+ ],
85
+ )
86
+
87
+ species_tab.select(
88
+ lambda lat, lon: gu.plot_map_scatter_mapbox(lat, lon, zoom=3), inputs=[lat_number, lon_number], outputs=map_plot
89
+ )
90
+
91
+ return lat_number, lon_number, map_plot
92
+
93
+
94
+ if __name__ == "__main__":
95
+ gu.open_window(build_species_tab)