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.
- birdnet_analyzer/__init__.py +8 -0
- birdnet_analyzer/analyze/__init__.py +5 -0
- birdnet_analyzer/analyze/__main__.py +4 -0
- birdnet_analyzer/analyze/cli.py +25 -0
- birdnet_analyzer/analyze/core.py +245 -0
- birdnet_analyzer/analyze/utils.py +701 -0
- birdnet_analyzer/audio.py +372 -0
- birdnet_analyzer/cli.py +707 -0
- birdnet_analyzer/config.py +242 -0
- birdnet_analyzer/eBird_taxonomy_codes_2021E.json +25280 -0
- birdnet_analyzer/embeddings/__init__.py +4 -0
- birdnet_analyzer/embeddings/__main__.py +3 -0
- birdnet_analyzer/embeddings/cli.py +13 -0
- birdnet_analyzer/embeddings/core.py +70 -0
- birdnet_analyzer/embeddings/utils.py +193 -0
- birdnet_analyzer/evaluation/__init__.py +195 -0
- birdnet_analyzer/evaluation/__main__.py +3 -0
- birdnet_analyzer/gui/__init__.py +23 -0
- birdnet_analyzer/gui/__main__.py +3 -0
- birdnet_analyzer/gui/analysis.py +174 -0
- birdnet_analyzer/gui/assets/arrow_down.svg +4 -0
- birdnet_analyzer/gui/assets/arrow_left.svg +4 -0
- birdnet_analyzer/gui/assets/arrow_right.svg +4 -0
- birdnet_analyzer/gui/assets/arrow_up.svg +4 -0
- birdnet_analyzer/gui/assets/gui.css +29 -0
- birdnet_analyzer/gui/assets/gui.js +94 -0
- birdnet_analyzer/gui/assets/img/birdnet-icon.ico +0 -0
- birdnet_analyzer/gui/assets/img/birdnet_logo.png +0 -0
- birdnet_analyzer/gui/assets/img/birdnet_logo_no_transparent.png +0 -0
- birdnet_analyzer/gui/assets/img/clo-logo-bird.svg +1 -0
- birdnet_analyzer/gui/embeddings.py +620 -0
- birdnet_analyzer/gui/evaluation.py +813 -0
- birdnet_analyzer/gui/localization.py +68 -0
- birdnet_analyzer/gui/multi_file.py +246 -0
- birdnet_analyzer/gui/review.py +527 -0
- birdnet_analyzer/gui/segments.py +191 -0
- birdnet_analyzer/gui/settings.py +129 -0
- birdnet_analyzer/gui/single_file.py +269 -0
- birdnet_analyzer/gui/species.py +95 -0
- birdnet_analyzer/gui/train.py +698 -0
- birdnet_analyzer/gui/utils.py +808 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_af.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ar.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_bg.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ca.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_cs.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_da.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_de.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_el.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_en_uk.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_es.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_fi.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_fr.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_he.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_hr.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_hu.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_in.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_is.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_it.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ja.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ko.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_lt.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ml.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_nl.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_no.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pl.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pt_BR.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_pt_PT.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ro.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_ru.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sk.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sl.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sr.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_sv.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_th.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_tr.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_uk.txt +6522 -0
- birdnet_analyzer/labels/V2.4/BirdNET_GLOBAL_6K_V2.4_Labels_zh.txt +6522 -0
- birdnet_analyzer/lang/de.json +335 -0
- birdnet_analyzer/lang/en.json +335 -0
- birdnet_analyzer/lang/fi.json +335 -0
- birdnet_analyzer/lang/fr.json +335 -0
- birdnet_analyzer/lang/id.json +335 -0
- birdnet_analyzer/lang/pt-br.json +335 -0
- birdnet_analyzer/lang/ru.json +335 -0
- birdnet_analyzer/lang/se.json +335 -0
- birdnet_analyzer/lang/tlh.json +335 -0
- birdnet_analyzer/lang/zh_TW.json +335 -0
- birdnet_analyzer/model.py +1243 -0
- birdnet_analyzer/search/__init__.py +3 -0
- birdnet_analyzer/search/__main__.py +3 -0
- birdnet_analyzer/search/cli.py +12 -0
- birdnet_analyzer/search/core.py +78 -0
- birdnet_analyzer/search/utils.py +111 -0
- birdnet_analyzer/segments/__init__.py +3 -0
- birdnet_analyzer/segments/__main__.py +3 -0
- birdnet_analyzer/segments/cli.py +14 -0
- birdnet_analyzer/segments/core.py +78 -0
- birdnet_analyzer/segments/utils.py +394 -0
- birdnet_analyzer/species/__init__.py +3 -0
- birdnet_analyzer/species/__main__.py +3 -0
- birdnet_analyzer/species/cli.py +14 -0
- birdnet_analyzer/species/core.py +35 -0
- birdnet_analyzer/species/utils.py +75 -0
- birdnet_analyzer/train/__init__.py +3 -0
- birdnet_analyzer/train/__main__.py +3 -0
- birdnet_analyzer/train/cli.py +14 -0
- birdnet_analyzer/train/core.py +113 -0
- birdnet_analyzer/train/utils.py +847 -0
- birdnet_analyzer/translate.py +104 -0
- birdnet_analyzer/utils.py +419 -0
- birdnet_analyzer-2.0.0.dist-info/METADATA +129 -0
- birdnet_analyzer-2.0.0.dist-info/RECORD +117 -0
- birdnet_analyzer-2.0.0.dist-info/WHEEL +5 -0
- birdnet_analyzer-2.0.0.dist-info/entry_points.txt +11 -0
- birdnet_analyzer-2.0.0.dist-info/licenses/LICENSE +19 -0
- 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)
|