diff --git a/.gitignore b/.gitignore
index e8ace913..c4b4312e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,8 @@ foo*
installers/
desktop.ini
*.iss
+*deploy*
+*hook*
# Custom classifier
checkpoints/custom/
@@ -34,7 +36,7 @@ __pycache__/
# Distribution / packaging
.Python
-build/
+build*
develop-eggs/
dist/
downloads/
@@ -58,8 +60,6 @@ MANIFEST
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
-!BirdNET-Analyzer-mac.spec
-!BirdNET-Analyzer-win.spec
# Installer logs
pip-log.txt
@@ -161,4 +161,5 @@ train_data/
train_cache.npz
autotune/
-gui-settings.json
\ No newline at end of file
+gui-settings.json
+state.json
\ No newline at end of file
diff --git a/BirdNET-Analyzer-mac.spec b/BirdNET-Analyzer-mac.spec
deleted file mode 100644
index 8585133c..00000000
--- a/BirdNET-Analyzer-mac.spec
+++ /dev/null
@@ -1,65 +0,0 @@
-# -*- mode: python ; coding: utf-8 -*-
-
-import os
-
-from dotenv import load_dotenv
-
-load_dotenv()
-
-a = Analysis(
- ['gui.py'],
- pathex=[],
- binaries=[],
- datas=[
- ('eBird_taxonomy_codes_2021E.json', '.'),
- ('checkpoints', 'checkpoints'),
- ('example/soundscape.wav', 'example'),
- ('example/species_list.txt', 'example'),
- ('labels', 'labels'),
- ("gui", "gui"),
- ("gui-settings.json", "."),
- ("lang", "lang")],
- hiddenimports=["scipy._lib.array_api_compat.numpy.fft", "scipy.special._special_ufuncs"],
- hookspath=['extra-hooks'],
- hooksconfig={},
- runtime_hooks=[],
- excludes=[],
- noarchive=False,
- module_collection_mode={
- 'gradio': 'py', # Collect gradio package as source .py files
- 'tensorflow': 'py'
- },
-)
-pyz = PYZ(a.pure)
-
-exe = EXE(
- pyz,
- a.scripts,
- a.binaries,
- a.datas,
- [],
- name='BirdNET-Analyzer-GUI',
- debug=False,
- bootloader_ignore_signals=False,
- strip=False,
- upx=True,
- upx_exclude=[],
- runtime_tmpdir=None,
- console=False,
- disable_windowed_traceback=False,
- argv_emulation=False,
- target_arch=None,
- codesign_identity=os.environ["APPLE_DEVELOPER_ID_APPLICATION"],
- entitlements_file="entitlements.plist",
- icon=['gui/img/birdnet-icon.ico'],
-)
-app = BUNDLE(
- exe,
- name='BirdNET-Analyzer-GUI.app',
- icon='gui/img/birdnet-icon.ico',
- bundle_identifier=None,
- info_plist={
- 'NSPrincipalClass': 'NSApplication',
- 'NSAppleScriptEnabled': False,
- },
-)
diff --git a/BirdNET-Analyzer-win.spec b/BirdNET-Analyzer-win.spec
deleted file mode 100644
index a11fbf50..00000000
--- a/BirdNET-Analyzer-win.spec
+++ /dev/null
@@ -1,122 +0,0 @@
-# -*- mode: python ; coding: utf-8 -*-
-
-
-block_cipher = None
-
-
-analyzer = Analysis(
- ["analyze.py"],
- pathex=[],
- binaries=[],
- datas=[
- ("eBird_taxonomy_codes_2021E.json", "."),
- ("checkpoints", "checkpoints"),
- ("example/soundscape.wav", "example"),
- ("example/species_list.txt", "example"),
- ("labels", "labels"),
- ("gui", "gui"),
- ],
- hiddenimports=[],
- hookspath=["extra-hooks"],
- hooksconfig={},
- runtime_hooks=[],
- excludes=[],
- win_no_prefer_redirects=False,
- win_private_assemblies=False,
- cipher=block_cipher,
- noarchive=False,
-)
-analyzer_pyz = PYZ(analyzer.pure, analyzer.zipped_data, cipher=block_cipher)
-
-analyzer_exe = EXE(
- analyzer_pyz,
- analyzer.scripts,
- [],
- exclude_binaries=True,
- name="BirdNET-Analyzer",
- debug=False,
- bootloader_ignore_signals=False,
- strip=False,
- upx=True,
- console=True,
- disable_windowed_traceback=False,
- argv_emulation=False,
- target_arch=None,
- codesign_identity=None,
- entitlements_file=None,
- icon=["gui\\img\\birdnet-icon.ico"],
-)
-
-gui = Analysis(
- ["gui.py"],
- pathex=[],
- binaries=[],
- datas=[
- ("eBird_taxonomy_codes_2021E.json", "."),
- ("checkpoints", "checkpoints"),
- ("example/soundscape.wav", "example"),
- ("example/species_list.txt", "example"),
- ("labels", "labels"),
- ("gui", "gui"),
- ("gui-settings.json", "."),
- ("lang", "lang")
- ],
- hiddenimports=[],
- hookspath=["extra-hooks"],
- hooksconfig={},
- runtime_hooks=[],
- excludes=[],
- win_no_prefer_redirects=False,
- win_private_assemblies=False,
- cipher=block_cipher,
- noarchive=False,
- module_collection_mode={"gradio": "py", "tensorflow": "py"}, # Collect gradio package as source .py files
-)
-gui_pyz = PYZ(gui.pure, gui.zipped_data, cipher=block_cipher)
-
-splash = Splash(
- 'gui/img/birdnet_logo_no_transparent.png',
- gui.binaries,
- gui.datas,
- text_pos=None,
- text_size=12,
- minify_script=True,
- always_on_top=True,
-)
-
-gui_exe = EXE(
- gui_pyz,
- gui.scripts,
- splash,
- [],
- exclude_binaries=True,
- name="BirdNET-Analyzer-GUI",
- debug=False,
- bootloader_ignore_signals=False,
- strip=False,
- upx=True,
- console=False,
- disable_windowed_traceback=False,
- argv_emulation=False,
- target_arch=None,
- codesign_identity=None,
- entitlements_file=None,
- icon=["gui\\img\\birdnet-icon.ico"],
-)
-
-
-coll = COLLECT(
- analyzer_exe,
- analyzer.binaries,
- analyzer.zipfiles,
- analyzer.datas,
- splash.binaries,
- gui_exe,
- gui.binaries,
- gui.zipfiles,
- gui.datas,
- strip=False,
- upx=True,
- upx_exclude=[],
- name="BirdNET-Analyzer",
-)
diff --git a/README.adoc b/README.adoc
index 55bbff73..7184a0e5 100644
--- a/README.adoc
+++ b/README.adoc
@@ -434,6 +434,8 @@ source venv-birdnet/bin/activate
python -m pip install -U pip
----
+WARNING: :exclamation:**Make sure that you are using Python 3.10, if not install it from the https://www.python.org/downloads/release/python-3100/[Python website].**:exclamation:
+
The nexttime you want to use BirdNET, go to the BirdNET-Analyzer folder and run `source venv-birdnet/bin/activate` to activate the virtual environment.
==== Install dependencies
@@ -775,7 +777,7 @@ Due to these custom labels, the location filter and locale will be disabled.
== Segment review
-Please read the excellent paper from Connor M. Woods: https://scholar.google.com/citations?view_op=view_citation&hl=en&user=Uwta4wYAAAAJ&sortby=pubdate&citation_for_view=Uwta4wYAAAAJ:j3f4tGmQtD8C[Guidelines for appropriate use of BirdNET scores and other detector outputs].
+Please read the excellent paper from Connor M. Wood and Stefan Kahl: https://scholar.google.com/citations?view_op=view_citation&hl=en&user=Uwta4wYAAAAJ&sortby=pubdate&citation_for_view=Uwta4wYAAAAJ:j3f4tGmQtD8C[Guidelines for appropriate use of BirdNET scores and other detector outputs].
The *Review* tab in the GUI is an implementation of the workflow described in the paper. It allows you to review the segments that were detected by BirdNET and to label the segments manually. This can helb you to choose an appropriate threshold for your specific use case.
diff --git a/analyze.py b/analyze.py
index 53a881fb..285da0ba 100644
--- a/analyze.py
+++ b/analyze.py
@@ -75,9 +75,8 @@ def generate_raven_table(timestamps: list[str], result: dict[str, list], afile_p
out_string += (
f"{selection_id}\tSpectrogram 1\t1\t0\t3\t{low_freq}\t{high_freq}\tnocall\tnocall\t1.0\t{afile_path}\t0\n"
)
-
- with open(result_path, "w", encoding="utf-8") as rfile:
- rfile.write(out_string)
+
+ utils.save_result_file(result_path, out_string)
def generate_audacity(timestamps: list[str], result: dict[str, list], result_path: str) -> str:
@@ -97,8 +96,7 @@ def generate_audacity(timestamps: list[str], result: dict[str, list], result_pat
# Write result string to file
out_string += rstring
- with open(result_path, "w", encoding="utf-8") as rfile:
- rfile.write(out_string)
+ utils.save_result_file(result_path, out_string)
def generate_rtable(timestamps: list[str], result: dict[str, list], afile_path: str, result_path: str) -> str:
@@ -111,7 +109,7 @@ def generate_rtable(timestamps: list[str], result: dict[str, list], afile_path:
for c in result[timestamp]:
if c[1] > cfg.MIN_CONFIDENCE and (not cfg.SPECIES_LIST or c[0] in cfg.SPECIES_LIST):
label = cfg.TRANSLATED_LABELS[cfg.LABELS.index(c[0])]
- rstring += "\n{},{},{},{},{},{:.4f},{:.4f},{:.4f},{},{},{},{},{},{}".format(
+ rstring += "{},{},{},{},{},{:.4f},{:.4f},{:.4f},{},{},{},{},{},{}\n".format(
afile_path,
start,
end,
@@ -131,8 +129,7 @@ def generate_rtable(timestamps: list[str], result: dict[str, list], afile_path:
# Write result string to file
out_string += rstring
- with open(result_path, "w", encoding="utf-8") as rfile:
- rfile.write(out_string)
+ utils.save_result_file(result_path, out_string)
def generate_kaleidoscope(timestamps: list[str], result: dict[str, list], afile_path: str, result_path: str) -> str:
@@ -167,8 +164,7 @@ def generate_kaleidoscope(timestamps: list[str], result: dict[str, list], afile_
# Write result string to file
out_string += rstring
- with open(result_path, "w", encoding="utf-8") as rfile:
- rfile.write(out_string)
+ utils.save_result_file(result_path, out_string)
def generate_csv(timestamps: list[str], result: dict[str, list], afile_path: str, result_path: str) -> str:
@@ -187,8 +183,7 @@ def generate_csv(timestamps: list[str], result: dict[str, list], afile_path: str
# Write result string to file
out_string += rstring
- with open(result_path, "w", encoding="utf-8") as rfile:
- rfile.write(out_string)
+ utils.save_result_file(result_path, out_string)
def saveResultFiles(r: dict[str, list], result_files: dict[str, str], afile_path: str):
diff --git a/config.py b/config.py
index 8cd95a0a..cdad2f95 100644
--- a/config.py
+++ b/config.py
@@ -2,9 +2,6 @@
# Misc settings #
#################
-# GUI version
-GUI_VERSION: str = "1.2.0"
-
# Random seed for gaussian noise
RANDOM_SEED: int = 42
@@ -112,8 +109,8 @@
# 'csv' denotes a generic CSV file with start, end, species and confidence.
RESULT_TYPES: set[str] | list[str] = {"table"}
OUTPUT_RAVEN_FILENAME: str = "BirdNET_SelectionTable.txt" # this is for combined Raven selection tables only
-OUTPUT_RTABLE_FILENAME: str = "BirdNET_RTable.txt"
-OUTPUT_KALEIDOSCOPE_FILENAME: str = "BirdNET_Kaleidoscope.txt"
+OUTPUT_RTABLE_FILENAME: str = "BirdNET_RTable.csv"
+OUTPUT_KALEIDOSCOPE_FILENAME: str = "BirdNET_Kaleidoscope.csv"
OUTPUT_CSV_FILENAME: str = "BirdNET_CombinedTable.csv"
# Whether to skip existing results in the output path
diff --git a/deploy.py b/deploy.py
deleted file mode 100644
index 6e6aebf5..00000000
--- a/deploy.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Module for deployment as an application.
-
-Raises:
- ValueError: When the specified os is not supported.
-"""
-import PyInstaller.__main__
-
-
-def build(target_os):
- """Uses PyInstaller to build a BirdNET application.
-
- Args:
- target_os (str): The targeted operating system.
-
- Raises:
- ValueError: Is raised if the specified operating system is not supported.
- """
- if target_os not in {"win", "mac"}:
- raise ValueError(f"OS {target_os} is not supported use win or mac.")
-
- PyInstaller.__main__.run(["--clean", "--noconfirm", f"BirdNET-Analyzer-{target_os}.spec"])
-
-if __name__ == "__main__":
- import argparse
-
- parser = argparse.ArgumentParser(description="Bundles BirdNET into an application.")
- parser.add_argument("target_os", choices=["win", "mac"], help="Choose the operating for which the application should be build.")
- args, _ = parser.parse_known_args()
-
- build(args.target_os)
diff --git a/extra-hooks/hook-gradio.py b/extra-hooks/hook-gradio.py
deleted file mode 100644
index 1e0aa9be..00000000
--- a/extra-hooks/hook-gradio.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from PyInstaller.utils.hooks import collect_data_files
-
-datas = collect_data_files("gradio")
diff --git a/extra-hooks/hook-gradio_client.py b/extra-hooks/hook-gradio_client.py
deleted file mode 100644
index 2f5fd45d..00000000
--- a/extra-hooks/hook-gradio_client.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from PyInstaller.utils.hooks import collect_data_files
-
-datas = collect_data_files("gradio_client")
diff --git a/extra-hooks/hook-librosa.py b/extra-hooks/hook-librosa.py
deleted file mode 100644
index 25686ad5..00000000
--- a/extra-hooks/hook-librosa.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from PyInstaller.utils.hooks import collect_data_files
-
-datas = collect_data_files("librosa")
diff --git a/gui.py b/gui.py
index 8a340f21..64ffcdd8 100644
--- a/gui.py
+++ b/gui.py
@@ -7,6 +7,7 @@
import config as cfg
+
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
# divert stdout & stderr to logs.txt file since we have no console when deployed
userdir = Path.home()
@@ -42,7 +43,7 @@
from train import trainModel
import localization as loc
-loc.load_localization()
+loc.load_local_state()
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
@@ -317,7 +318,7 @@ def runAnalysis(
if input_dir:
cfg.OUTPUT_PATH = output_path if output_path else input_dir
else:
- cfg.OUTPUT_PATH = output_path if output_path else input_path.split(".", 1)[0] + ".csv"
+ cfg.OUTPUT_PATH = output_path if output_path else os.path.dirname(input_path)
# Parse input files
if input_dir:
@@ -388,7 +389,9 @@ def runAnalysis(
analyze.combineResults([i[1] for i in result_list])
print("done!", flush=True)
- return [[os.path.relpath(r[0], input_dir), bool(r[1])] for r in result_list] if input_dir else cfg.OUTPUT_PATH
+ return (
+ [[os.path.relpath(r[0], input_dir), bool(r[1])] for r in result_list] if input_dir else result_list[0][1]["csv"]
+ )
_CUSTOM_SPECIES = loc.localize("species-list-radio-option-custom-list")
@@ -441,15 +444,20 @@ def show_species_choice(choice: str):
]
-def select_subdirectories():
+def select_subdirectories(state_key=None):
"""Creates a directory selection dialog.
Returns:
A tuples of (directory, list of subdirectories) or (None, None) if the dialog was canceled.
"""
- dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG)
+
+ initial_dir = loc.get_state(state_key, "") if state_key else ""
+ dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG, directory=initial_dir)
if dir_name:
+ if state_key:
+ loc.set_state(state_key, dir_name[0])
+
subdirs = utils.list_subdirectories(dir_name[0])
labels = []
@@ -465,7 +473,7 @@ def select_subdirectories():
return None, None
-def select_file(filetypes=()):
+def select_file(filetypes=(), state_key=None):
"""Creates a file selection dialog.
Args:
@@ -474,9 +482,16 @@ def select_file(filetypes=()):
Returns:
The selected file or None of the dialog was canceled.
"""
- files = _WINDOW.create_file_dialog(webview.OPEN_DIALOG, file_types=filetypes)
+ initial_selection = loc.get_state(state_key, "") if state_key else ""
+ files = _WINDOW.create_file_dialog(webview.OPEN_DIALOG, file_types=filetypes, directory=initial_selection)
+
+ if files:
+ if state_key:
+ loc.set_state(state_key, files[0])
- return files[0] if files else None
+ return files[0]
+
+ return None
def format_seconds(secs: float):
@@ -496,7 +511,7 @@ def format_seconds(secs: float):
return f"{hours:2.0f}:{minutes:02.0f}:{secs:06.3f}"
-def select_directory(collect_files=True, max_files=None):
+def select_directory(collect_files=True, max_files=None, state_key=None):
"""Shows a directory selection system dialog.
Uses the pywebview to create a system dialog.
@@ -509,7 +524,11 @@ def select_directory(collect_files=True, max_files=None):
else just the directory path.
All values will be None of the dialog is cancelled.
"""
- dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG)
+ initial_dir = loc.get_state(state_key, "") if state_key else ""
+ dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG, directory=initial_dir)
+
+ if dir_name and state_key:
+ loc.set_state(state_key, dir_name[0])
if collect_files:
if not dir_name:
@@ -880,14 +899,14 @@ def species_lists(opened=True):
selected_classifier_state = gr.State()
def on_custom_classifier_selection_click():
- file = select_file(("TFLite classifier (*.tflite)",))
+ file = select_file(("TFLite classifier (*.tflite)",), state_key="custom_classifier_file")
if file:
labels = os.path.splitext(file)[0] + "_Labels.txt"
return file, gr.File(value=[file, labels], visible=True)
- return None
+ return None, None
classifier_selection_button.click(
on_custom_classifier_selection_click,
@@ -936,7 +955,7 @@ def build_footer():
f"""
-
GUI version: 
{cfg.GUI_VERSION}+
+
GUI version: 
{os.environ['GUI_VERSION'] if FROZEN else 'main'}+
Model version: {cfg.MODEL_VERSION}
K. Lisa Yang Center for Conservation Bioacoustics
Chemnitz University of Technology
@@ -1026,7 +1045,7 @@ def build_multi_analysis_tab():
)
def select_directory_on_empty():
- res = select_directory(max_files=101)
+ res = select_directory(max_files=101, state_key="batch-analysis-data-dir")
if res[1]:
if len(res[1]) > 100:
@@ -1049,7 +1068,7 @@ def select_directory_on_empty():
)
def select_directory_wrapper():
- return (select_directory(collect_files=False),) * 2
+ return (select_directory(collect_files=False, state_key="batch-analysis-output-dir"),) * 2
select_out_directory_btn.click(
select_directory_wrapper,
@@ -1161,7 +1180,9 @@ def build_train_tab():
elem_classes="matrix-mh-200",
)
select_directory_btn.click(
- select_subdirectories, outputs=[input_directory_state, directory_input], show_progress=False
+ partial(select_subdirectories, state_key="train-data-dir"),
+ outputs=[input_directory_state, directory_input],
+ show_progress=False,
)
with gr.Column():
@@ -1182,9 +1203,11 @@ def build_train_tab():
)
def select_directory_and_update_tb():
- dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG)
+ initial_dir = loc.get_state("train-output-dir", "")
+ dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG, directory=initial_dir)
if dir_name:
+ loc.set_state("train-output-dir", dir_name[0])
return (
dir_name[0],
gr.Textbox(label=dir_name[0] + "\\", visible=True),
@@ -1360,9 +1383,11 @@ def on_crop_select(new_crop_mode):
)
def select_directory_and_update():
- dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG)
+ initial_dir = loc.get_state("train-data-cache-file-output", "")
+ dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG, directory=initial_dir)
if dir_name:
+ loc.set_state("train-data-cache-file-output", dir_name[0])
return (
dir_name[0],
gr.Textbox(label=dir_name[0] + "\\", visible=True),
@@ -1381,7 +1406,7 @@ def select_directory_and_update():
cache_file_input = gr.File(file_types=[".npz"], visible=False, interactive=False)
def on_cache_file_selection_click():
- file = select_file(("NPZ file (*.npz)",))
+ file = select_file(("NPZ file (*.npz)",), state_key="train_data_cache_file")
if file:
return file, gr.File(value=file, visible=True)
@@ -1440,8 +1465,8 @@ def build_segments_tab():
result_directory_state = gr.State()
output_directory_state = gr.State()
- def select_directory_to_state_and_tb():
- return (select_directory(collect_files=False),) * 2
+ def select_directory_to_state_and_tb(state_key):
+ return (select_directory(collect_files=False, state_key=state_key),) * 2
with gr.Row():
select_audio_directory_btn = gr.Button(
@@ -1449,7 +1474,7 @@ def select_directory_to_state_and_tb():
)
selected_audio_directory_tb = gr.Textbox(show_label=False, interactive=False)
select_audio_directory_btn.click(
- select_directory_to_state_and_tb,
+ partial(select_directory_to_state_and_tb, state_key="segments-audio-dir"),
outputs=[selected_audio_directory_tb, audio_directory_state],
show_progress=False,
)
@@ -1464,7 +1489,7 @@ def select_directory_to_state_and_tb():
placeholder=loc.localize("segments-tab-results-input-textbox-placeholder"),
)
select_result_directory_btn.click(
- select_directory_to_state_and_tb,
+ partial(select_directory_to_state_and_tb, state_key="segments-result-dir"),
outputs=[result_directory_state, selected_result_directory_tb],
show_progress=False,
)
@@ -1477,7 +1502,7 @@ def select_directory_to_state_and_tb():
placeholder=loc.localize("segments-tab-output-selection-textbox-placeholder"),
)
select_output_directory_btn.click(
- select_directory_to_state_and_tb,
+ partial(select_directory_to_state_and_tb, state_key="segments-output-dir"),
outputs=[selected_output_directory_tb, output_directory_state],
show_progress=False,
)
@@ -1560,7 +1585,7 @@ def collect_files(directory):
def create_log_plot(positives, negatives, fig_num=None):
import matplotlib.pyplot as plt
- import sklearn
+ from sklearn import linear_model
import numpy as np
from scipy.special import expit
@@ -1581,14 +1606,19 @@ def create_log_plot(positives, negatives, fig_num=None):
for fl in positives + negatives:
try:
- x_vals.append(float(os.path.basename(fl).split("_", 1)[0]))
+ x_val = float(os.path.basename(fl).split("_", 1)[0])
+
+ if 0 > x_val > 1:
+ continue
+
+ x_vals.append([x_val])
y_val.append(1 if fl in positives else 0)
except ValueError:
pass
if (len(positives) + len(negatives)) >= 2 and len(set(y_val)) > 1:
- log_model = sklearn.linear_model.LogisticRegression(C=55)
- log_model.fit([[x] for x in x_vals], y_val)
+ log_model = linear_model.LogisticRegression(C=55)
+ log_model.fit(x_vals, y_val)
Xs = np.linspace(0, 10, 200)
Ys = expit(Xs * log_model.coef_ + log_model.intercept_).ravel()
target_ps = [0.85, 0.9, 0.95, 0.99]
@@ -1599,16 +1629,17 @@ def create_log_plot(positives, negatives, fig_num=None):
p_colors = ["blue", "purple", "orange", "green"]
for target_p, p_color, threshold in zip(target_ps, p_colors, thresholds):
- ax.vlines(
- threshold,
- 0,
- target_p,
- color=p_color,
- linestyle="--",
- linewidth=0.5,
- label=f"p={target_p:.2f} treshold>={threshold:.2f}",
- )
- ax.hlines(target_p, 0, threshold, color=p_color, linestyle="--", linewidth=0.5)
+ if threshold <= 1:
+ ax.vlines(
+ threshold,
+ 0,
+ target_p,
+ color=p_color,
+ linestyle="--",
+ linewidth=0.5,
+ label=f"p={target_p:.2f} threshold>={threshold:.2f}",
+ )
+ ax.hlines(target_p, 0, threshold, color=p_color, linestyle="--", linewidth=0.5)
ax.plot(Xs, Ys, color="red")
ax.scatter(thresholds, target_ps, color=p_colors, marker="x")
@@ -1616,7 +1647,7 @@ def create_log_plot(positives, negatives, fig_num=None):
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
-
+
if len(y_val) > 0:
ax.scatter(x_vals, y_val, 2)
@@ -1626,12 +1657,13 @@ def create_log_plot(positives, negatives, fig_num=None):
review_state = gr.State(
{
"input_directory": "",
- "spcies_list": [],
+ "species_list": [],
"current_species": "",
"files": [],
- "current": 0,
POSITIVE_LABEL_DIR: [],
NEGATIVE_LABEL_DIR: [],
+ "skipped": [],
+ "history": [],
}
)
@@ -1647,6 +1679,7 @@ def create_log_plot(positives, negatives, fig_num=None):
loc.localize("review-tab-file-matrix-neg-header"),
],
interactive=False,
+ elem_id="segments-results-grid",
)
with gr.Column() as review_item_col:
@@ -1654,6 +1687,9 @@ def create_log_plot(positives, negatives, fig_num=None):
spectrogram_image = gr.Plot(label=loc.localize("review-tab-spectrogram-plot-label"))
with gr.Column():
+ with gr.Row():
+ skip_btn = gr.Button(loc.localize("review-tab-skip-button-label"))
+ undo_btn = gr.Button(loc.localize("review-tab-undo-button-label"))
positive_btn = gr.Button(loc.localize("review-tab-pos-button-label"))
negative_btn = gr.Button(loc.localize("review-tab-neg-button-label"))
review_audio = gr.Audio(
@@ -1666,11 +1702,44 @@ def create_log_plot(positives, negatives, fig_num=None):
no_samles_label = gr.Label(loc.localize("review-tab-no-files-label"), visible=False)
species_regression_plot = gr.Plot(label=loc.localize("review-tab-regression-plot-label"))
- def next_review(next_review_state: dict, target_dir: str = None):
- current_file = next_review_state["files"][next_review_state["current"]]
-
+ def update_values(next_review_state, skip_plot=False):
update_dict = {review_state: next_review_state}
+ if not skip_plot:
+ update_dict |= {
+ species_regression_plot: create_log_plot(
+ next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2
+ ),
+ }
+
+ if next_review_state["files"]:
+ next_file = next_review_state["files"][0]
+ update_dict |= {
+ review_audio: gr.Audio(next_file, label=os.path.basename(next_file)),
+ spectrogram_image: utils.spectrogram_from_file(next_file),
+ }
+ else:
+ update_dict |= {
+ no_samles_label: gr.Label(visible=True),
+ review_item_col: gr.Column(visible=False),
+ }
+
+ update_dict |= {
+ file_count_matrix: [
+ [
+ len(next_review_state["files"]) + len(next_review_state["skipped"]),
+ len(next_review_state[POSITIVE_LABEL_DIR]),
+ len(next_review_state[NEGATIVE_LABEL_DIR]),
+ ],
+ ],
+ undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
+ }
+
+ return update_dict
+
+ def next_review(next_review_state: dict, target_dir: str = None):
+ current_file = next_review_state["files"][0]
+
if target_dir:
selected_dir = os.path.join(
next_review_state["input_directory"], next_review_state["current_species"], target_dir
@@ -1686,44 +1755,13 @@ def next_review(next_review_state: dict, target_dir: str = None):
next_review_state[target_dir] += [current_file]
next_review_state["files"].remove(current_file)
- update_dict |= {
- species_regression_plot: create_log_plot(
- next_review_state[POSITIVE_LABEL_DIR], next_review_state[NEGATIVE_LABEL_DIR], 2
- ),
- }
-
- if not next_review_state["files"]:
- update_dict |= {
- no_samles_label: gr.Label(visible=True),
- review_item_col: gr.Column(visible=False),
- }
- else:
- next_file = next_review_state["files"][next_review_state["current"]]
- update_dict |= {
- review_audio: gr.Audio(next_file, label=os.path.basename(next_file)),
- spectrogram_image: utils.spectrogram_from_file(next_file),
- }
-
- update_dict |= {
- file_count_matrix: [
- [
- len(next_review_state["files"]),
- len(next_review_state[POSITIVE_LABEL_DIR]),
- len(next_review_state[NEGATIVE_LABEL_DIR]),
- ],
- ],
- }
+ next_review_state["history"].append((current_file, target_dir))
else:
- if next_review_state["current"] + 1 < len(next_review_state["files"]):
- next_review_state["current"] += 1
- next_file = next_review_state["files"][next_review_state["current"]]
-
- update_dict |= {
- review_audio: gr.Audio(next_file, label=os.path.basename(next_file)),
- spectrogram_image: utils.spectrogram_from_file(next_file),
- }
+ next_review_state["skipped"].append(current_file)
+ next_review_state["files"].remove(current_file)
+ next_review_state["history"].append((current_file, None))
- return update_dict
+ return update_values(next_review_state)
def select_subdir(new_value: str, next_review_state: dict):
if new_value != next_review_state["current_species"]:
@@ -1732,9 +1770,11 @@ def select_subdir(new_value: str, next_review_state: dict):
return {review_state: next_review_state}
def start_review(next_review_state):
- dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG)
+ initial_dir = loc.get_state("review-input-dir", "")
+ dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG, directory=initial_dir)
if dir_name:
+ loc.set_state("review-input-dir", dir_name[0])
next_review_state["input_directory"] = dir_name[0]
specieslist = [e.name for e in os.scandir(next_review_state["input_directory"]) if e.is_dir()]
@@ -1749,6 +1789,9 @@ def start_review(next_review_state):
return {review_state: next_review_state}
def update_review(next_review_state: dict, selected_species: str = None):
+ next_review_state["history"] = []
+ next_review_state["skipped"] = []
+
if selected_species:
next_review_state["current_species"] = selected_species
else:
@@ -1767,6 +1810,7 @@ def update_review(next_review_state: dict, selected_species: str = None):
update_dict = {
review_col: gr.Column(visible=True),
review_state: next_review_state,
+ undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
file_count_matrix: [
[
len(next_review_state["files"]),
@@ -1797,7 +1841,38 @@ def update_review(next_review_state: dict, selected_species: str = None):
update_dict |= {review_item_col: gr.Column(visible=False), no_samles_label: gr.Label(visible=True)}
return update_dict
-
+
+ def undo_review(next_review_state):
+ if next_review_state["history"]:
+ last_file, last_dir = next_review_state["history"].pop()
+
+ if last_dir:
+ os.rename(
+ os.path.join(
+ next_review_state["input_directory"],
+ next_review_state["current_species"],
+ last_dir,
+ os.path.basename(last_file),
+ ),
+ os.path.join(
+ next_review_state["input_directory"],
+ next_review_state["current_species"],
+ os.path.basename(last_file),
+ ),
+ )
+
+ next_review_state[last_dir].remove(last_file)
+ else:
+ next_review_state["skipped"].remove(last_file)
+
+ next_review_state["files"].insert(0, last_file)
+
+ return update_values(next_review_state, skip_plot=not last_dir)
+ return {
+ review_state: next_review_state,
+ undo_btn: gr.Button(interactive=bool(next_review_state["history"])),
+ }
+
def toggle_autoplay(value):
return gr.Audio(autoplay=value)
@@ -1813,6 +1888,7 @@ def toggle_autoplay(value):
review_state,
file_count_matrix,
species_regression_plot,
+ undo_btn,
]
species_dropdown.change(
@@ -1830,6 +1906,7 @@ def toggle_autoplay(value):
no_samles_label,
file_count_matrix,
species_regression_plot,
+ undo_btn,
]
positive_btn.click(
@@ -1846,6 +1923,20 @@ def toggle_autoplay(value):
show_progress=True,
)
+ skip_btn.click(
+ next_review,
+ inputs=review_state,
+ outputs=review_btn_output,
+ show_progress=True,
+ )
+
+ undo_btn.click(
+ undo_review,
+ inputs=review_state,
+ outputs=review_btn_output,
+ show_progress=True,
+ )
+
select_directory_btn.click(
start_review,
inputs=review_state,
@@ -1864,9 +1955,11 @@ def build_species_tab():
)
def select_directory_and_update_tb(name_tb):
- dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG)
+ initial_dir = loc.get_state("species-output-dir", "")
+ dir_name = _WINDOW.create_file_dialog(webview.FOLDER_DIALOG, directory=initial_dir)
if dir_name:
+ loc.set_state("species-output-dir", dir_name[0])
return (
dir_name[0],
gr.Textbox(label=dir_name[0] + "\\", visible=True, value=name_tb),
@@ -1942,11 +2035,7 @@ def build_settings():
)
def on_language_change(value):
- if value and value != loc.TARGET_LANGUAGE:
- loc.set_language(value)
- return gr.Button(visible=True)
-
- return gr.Button(visible=False)
+ loc.set_language(value)
def on_tab_select(value: gr.SelectData):
if value.selected and os.path.exists(cfg.ERROR_LOG_FILE):
diff --git a/gui/gui.js b/gui/gui.js
index 6d380fc0..3d0e304a 100644
--- a/gui/gui.js
+++ b/gui/gui.js
@@ -1,45 +1,49 @@
function init() {
function checkForNewerVersion() {
- console.log("Checking for newer version...");
+ let gui_version_element = document.getElementById("current-version")
+
+ if (gui_version_element && gui_version_element.textContent != "main") {
+ console.log("Checking for newer version...");
- function sendGetRequest(url) {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.open("GET", url);
- xhr.onload = () => {
- if (xhr.status === 200) {
- resolve(xhr.responseText);
- } else {
- reject(new Error(`Request failed with status ${xhr.status}`));
- }
- };
- xhr.onerror = () => {
- reject(new Error("Request failed"));
- };
- xhr.send();
- });
- }
+ function sendGetRequest(url) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ xhr.onload = () => {
+ if (xhr.status === 200) {
+ resolve(xhr.responseText);
+ } else {
+ reject(new Error(`Request failed with status ${xhr.status}`));
+ }
+ };
+ xhr.onerror = () => {
+ reject(new Error("Request failed"));
+ };
+ xhr.send();
+ });
+ }
- const apiUrl = "https://api.github.com/repos/kahst/BirdNET-Analyzer/releases/latest";
+ const apiUrl = "https://api.github.com/repos/kahst/BirdNET-Analyzer/releases/latest";
- sendGetRequest(apiUrl)
- .then(response => {
- const current_version = "v" + document.getElementById("current-version").textContent;
- const response_object = JSON.parse(response);
- const latest_version = response_object.tag_name;
+ sendGetRequest(apiUrl)
+ .then(response => {
+ const current_version = "v" + document.getElementById("current-version").textContent;
+ const response_object = JSON.parse(response);
+ const latest_version = response_object.tag_name;
- if (current_version !== latest_version) {
- const updateNotification = document.getElementById("update-available");
+ if (current_version !== latest_version) {
+ const updateNotification = document.getElementById("update-available");
- updateNotification.style.display = "block";
- const linkElement = updateNotification.getElementsByTagName("a")[0]
- linkElement.href = response_object.html_url;
- linkElement.target = "_blank";
- }
- })
- .catch(error => {
- console.error(error);
- });
+ updateNotification.style.display = "block";
+ const linkElement = updateNotification.getElementsByTagName("a")[0]
+ linkElement.href = response_object.html_url;
+ linkElement.target = "_blank";
+ }
+ })
+ .catch(error => {
+ console.error(error);
+ });
+ }
}
function overwriteStyles() {
diff --git a/lang/de.json b/lang/de.json
index 94d141b2..a7c32900 100644
--- a/lang/de.json
+++ b/lang/de.json
@@ -147,6 +147,8 @@
"review-tab-regression-plot-y-label-false": "Falsch",
"review-tab-regression-plot-y-label-true": "Wahr",
"review-tab-autoplay-checkbox-label": "autom. Abspielen",
+ "review-tab-skip-button-label": "Überspringen",
+ "review-tab-undo-button-label": "Rückgängig",
"species-tab-title": "Arten",
"species-tab-select-output-directory-button-label": "Wählen Sie das Ausgabeverzeichnis",
"species-tab-filename-textbox-label": "Name der Datei, wenn nicht angegeben, wird 'species_list.txt' verwendet.",
diff --git a/lang/en.json b/lang/en.json
index f50fc0f0..0615a113 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -147,6 +147,8 @@
"review-tab-regression-plot-y-label-false": "False",
"review-tab-regression-plot-y-label-true": "True",
"review-tab-autoplay-checkbox-label": "Autoplay",
+ "review-tab-skip-button-label": "Skip",
+ "review-tab-undo-button-label": "Undo",
"species-tab-title": "Species",
"species-tab-select-output-directory-button-label": "Select output directory",
"species-tab-filename-textbox-label": "Name of the file, if not specified 'species_list.txt' will be used.",
diff --git a/lang/fr.json b/lang/fr.json
new file mode 100644
index 00000000..2117a0ed
--- /dev/null
+++ b/lang/fr.json
@@ -0,0 +1,190 @@
+{
+ "single-tab-title": "Analyse rapide",
+ "single-audio-label": "fichier",
+ "single-tab-output-header-start": "Commencer",
+ "single-tab-output-header-end": "Arrêter",
+ "single-tab-output-header-sci-name": "Nom scientifique",
+ "single-tab-output-header-common-name": "Nom commun",
+ "single-tab-output-header-confidence": "Confiance",
+ "inference-settings-accordion-label": "Paramètres d'inférence",
+ "inference-settings-confidence-slider-label": "Confiance minimum",
+ "inference-settings-confidence-slider-info": "Ajustez le seuil pour ignorer les résultats dont le niveau de confiance est inférieur à ce seuil.",
+ "inference-settings-sensitivity-slider-label": "Sensibilité",
+ "inference-settings-sensitivity-slider-info": "Ajustez la distribution des scores de prédiction. Des valeurs plus élevées se traduisent par des scores plus élevés.",
+ "inference-settings-overlap-slider-label": "Chevauchement (s)",
+ "inference-settings-overlap-slider-info": "BirdNET utilise des segments de 3s. Détermine le chevauchement avec le segment précédent.",
+ "inference-settings-fmin-number-label": "Fréquence minimale de la bande passante (Hz)",
+ "inference-settings-fmin-number-info": "Notez que les coupures de fréquence doivent également être utilisées pendant la formation pour être efficaces.",
+ "inference-settings-fmax-number-label": "Fréquence maximale de la bande passante (Hz)",
+ "inference-settings-fmax-number-info": "Notez que les coupures de fréquence doivent également être utilisées pendant la formation pour être efficaces.",
+ "species-list-accordion-label": "Sélection des espèces",
+ "species-list-radio-label": "Liste des espèces",
+ "species-list-radio-info": "Filtrer les espèces qui sont incluses dans le résultat.",
+ "species-list-radio-option-custom-list": "Liste des espèces personnalisées",
+ "species-list-radio-option-predict-list": "Espèces par lieu",
+ "species-list-radio-option-custom-classifier": "Classificateur personnalisé",
+ "species-list-radio-option-all": "Toutes les espèces",
+ "species-list-custom-list-file-label": "Fichier",
+ "species-list-coordinates-lat-number-label": "Latitude",
+ "species-list-coordinates-lat-number-info": "Latitude du lieu d'enregistrement.",
+ "species-list-coordinates-lon-number-label": "Longitude",
+ "species-list-coordinates-lon-number-info": "Longitude du lieu d’enregistrement.",
+ "species-list-coordinates-yearlong-checkbox-label": "Toute l'année",
+ "species-list-coordinates-week-slider-label": "Semaine",
+ "species-list-coordinates-week-slider-info": "Spécifiez la semaine de l'année où l'enregistrement a été effectué, en utilisant un système simplifié où chaque mois est divisé en quatre semaines. Choisissez une valeur de 1 à 48.",
+ "species-list-coordinates-threshold-slider-label": "Seuil du filtre de localisation",
+ "species-list-coordinates-threshold-slider-info": "Probabilité d'occurrence minimale pour qu'une espèce soit incluse.",
+ "species-list-custom-classifier-selection-button-label": "Sélectionner un classificateur",
+ "analyze-locale-dropdown-label": "Locale",
+ "analyze-locale-dropdown-info": "Locale pour les noms communs des espèces traduits dans les résultats",
+ "analyze-start-button-label": "Analyser",
+ "multi-tab-title": "Analyse par lots",
+ "multi-tab-input-selection-button-label": "Sélection du répertoire d'entrée (récursif)",
+ "multi-tab-samples-dataframe-column-subpath-header": "Sous-chemin",
+ "multi-tab-samples-dataframe-column-duration-header": "Longueur",
+ "multi-tab-samples-dataframe-no-files-found": "Aucun fichiers trouvés",
+ "multi-tab-output-selection-button-label": "Sélectionner le répertoire de sortie",
+ "multi-tab-output-textbox-label": "Répertoire de sortie",
+ "multi-tab-output-textbox-placeholder": "S'il n'est pas sélectionné, le répertoire d'entrée sera utilisé.",
+ "multi-tab-output-accordion-label": "Paramètres de sortie",
+ "multi-tab-output-radio-label": "Type de résultat",
+ "multi-tab-output-radio-info": "Spécifier le format de sortie des classifications.",
+ "multi-tab-output-combine-tables-checkbox-label": "Combiner les tableaux de sélection",
+ "multi-tab-output-combine-tables-checkbox-info": "Cochez cette option pour fusionner tous les tableaux de sélection en un seul tableau.",
+ "multi-tab-output-combined-table-name-textbox-label": "Nom du fichier de la table combinée",
+ "multi-tab-output-combined-table-name-textbox-info": "Nom de la table de sélection combinée.",
+ "multi-tab-skip-existing-checkbox-label": "Sauter les résultats existants",
+ "multi-tab-skip-existing-checkbox-info": "Sauter les fichiers qui ont déjà un résultat.",
+ "multi-tab-batchsize-number-label": "Taille du lot",
+ "multi-tab-batchsize-number-info": "Nombre d'échantillons à traiter en même temps.",
+ "multi-tab-threads-number-label": "Cœurs",
+ "multi-tab-threads-number-info": "Nombre de cœurs du CPU.",
+ "multi-tab-result-dataframe-column-file-header": "Fichier",
+ "multi-tab-result-dataframe-column-execution-header": "Execution",
+ "training-tab-title": "Apprentissage",
+ "training-tab-input-selection-button-label": "Sélectionner les données d'apprentissage",
+ "training-tab-classes-dataframe-column-classes-header": "Classes",
+ "training-tab-select-output-button-label": "Sélectionner la sortie du classificateur",
+ "training-tab-classifier-textbox-info": "Le nom du nouveau classificateur.",
+ "training-tab-output-format-radio-label": "Format de sortie du modèle",
+ "training-tab-output-format-radio-info": "Format du classificateur formé.",
+ "training-tab-output-format-both": "à la fois",
+ "training-tab-autotune-checkbox-label": "Utiliser l'autotune",
+ "training-tab-autotune-checkbox-info": "Recherche les meilleurs hyperparamètres, mais prend plus de temps.",
+ "training-tab-autotune-trials-number-label": "Essais",
+ "training-tab-autotune-trials-number-info": "Nombre d'entraînements pour le réglage des hyperparamètres.",
+ "training-tab-autotune-executions-number-label": "Executions par essais",
+ "training-tab-autotune-executions-number-info": "Le nombre de fois qu'un entraînement avec un ensemble d'hyperparamètres est répété pendant le réglage des hyperparamètres (cela réduit la variance).",
+ "training-tab-epochs-number-label": "Époques",
+ "training-tab-epochs-number-info": "Nombre d'époques d'apprentissage.",
+ "training-tab-batchsize-number-label": "Taille du lot",
+ "training-tab-batchsize-number-info": "Nombre d'échantillons à traiter dans un lot.",
+ "training-tab-learningrate-number-label": "Taux d'apprentissage",
+ "training-tab-learningrate-number-info": "Taux d'apprentissage de l'optimiseur.",
+ "training-tab-upsampling-radio-label": "Mode de suréchantillonnage",
+ "training-tab-upsampling-radio-info": "Équilibrer les données de formation en suréchantillonnant les classes minoritaires.",
+ "training-tab-upsampling-radio-option-repeat": "répéter",
+ "training-tab-upsampling-radio-option-mean": "moyen",
+ "training-tab-upsampling-ratio-slider-label": "Rapport de suréchantillonnage",
+ "training-tab-upsampling-ratio-slider-info": "Le ratio minimum pour une classe minoritaire par rapport à la classe majoritaire après le rééchantillonnage.",
+ "training-tab-hiddenunits-number-label": "Unités cachées",
+ "training-tab-hiddenunits-number-info": "Nombre d'unités cachées. Si la valeur est >0, un classificateur à deux couches est utilisé.",
+ "training-tab-use-mixup-checkbox-label": "Utiliser le mixage",
+ "training-tab-use-mixup-checkbox-info": "Le mixage est une technique d'augmentation des données qui génère de nouveaux échantillons en mélangeant deux échantillons et leurs étiquettes.",
+ "training-tab-crop-mode-radio-label": "Mode de recadrage",
+ "training-tab-crop-mode-radio-info": "Ajuster la façon de récolter les échantillons qui sont plus longs que l'entrée du modèle.",
+ "training-tab-crop-mode-radio-option-center": "centré",
+ "training-tab-crop-mode-radio-option-first": "début",
+ "training-tab-crop-mode-radio-option-segments": "segmenté",
+ "training-tab-crop-overlap-number-label": "Chevauchement des segments de culture",
+ "training-tab-crop-overlap-number-info": "Ajuster le chevauchement des échantillons de formation.",
+ "training-tab-model-save-mode-radio-label": "Mode économie de modèle",
+ "training-tab-model-save-mode-radio-info": "L'option « replace » écrase la couche de classification originale, en ne laissant que les classes formées, et l'option « append » combine la couche de classification originale avec la nouvelle.",
+ "training-tab-model-save-mode-radio-option-replace": "remplacer",
+ "training-tab-model-save-mode-radio-option-append": "ajouter",
+ "training-tab-cache-mode-radio-label": "Mode de cache des données d'entraînement",
+ "training-tab-cache-mode-radio-info": "Permet de régler la mise en cache des données d'apprentissage. Sélectionnez « aucun » pour ne pas mettre en cache, « charger » pour charger à partir d'un fichier et « sauvegarder » pour sauvegarder les données d'entraînement",
+ "training-tab-cache-mode-radio-option-none": "aucun",
+ "training-tab-cache-mode-radio-option-load": "charger",
+ "training-tab-cache-mode-radio-option-save": "sauvegarder",
+ "training-tab-cache-select-directory-button-label": "Sélectionner le répertoire du fichier cache",
+ "training-tab-cache-file-name-textbox-info": "Le nom du fichier cache.",
+ "training-tab-cache-select-file-button-label": "Sélectionner la localisation du fichier cache",
+ "training-tab-start-training-button-label": "Commencer l’apprentissage",
+ "training-tab-early-stoppage-msg": "Arrêt prématuré - la mesure de validation ne s'améliore pas.",
+ "segments-tab-title": "Segments",
+ "segments-tab-select-audio-input-directory-button-label": "Sélectionner le dossier de l’audio (récursif)",
+ "segments-tab-select-results-input-directory-button-label": "Sélectionner le dossier du résultat",
+ "segments-tab-results-input-textbox-placeholder": "Identique au répertoire audio s'il n'est pas sélectionné",
+ "segments-tab-output-selection-button-label": "Sélectionner le répertoire de sortie",
+ "segments-tab-output-selection-textbox-placeholder": "Identique au répertoire audio s'il n'est pas sélectionné",
+ "segments-tab-min-confidence-slider-label": "Confiance minimale",
+ "segments-tab-min-confidence-slider-info": "Ne sélectionner que les segments dont la confiance est supérieure à ce seuil.",
+ "segments-tab-max-seq-number-label": "Nombre maximal de segments",
+ "segments-tab-max-seq-number-info": "Nombre maximal de segments extraits au hasard par espèce.",
+ "segments-tab-seq-length-number-label": "Longueur de la ou les séquence (s)",
+ "segments-tab-seq-length-number-info": "Longueur des segments extraits en secondes.",
+ "segments-tab-threads-number-label": "Cœurs",
+ "segments-tab-threads-number-info": "Nombre de cœurs du CPU.",
+ "segments-tab-extract-button-label": "Extraire des segments",
+ "segments-tab-result-dataframe-column-file-header": "Fichier",
+ "segments-tab-result-dataframe-column-execution-header": "Execution",
+ "review-tab-title": "Revue",
+ "review-tab-input-directory-button-label": "Sélectionner le répertoire d'entrée",
+ "review-tab-species-dropdown-label": "Espèces",
+ "review-tab-file-matrix-todo-header": "Total",
+ "review-tab-file-matrix-pos-header": "Positif",
+ "review-tab-file-matrix-neg-header": "Négatif",
+ "review-tab-spectrogram-plot-label": "Spectrogramme",
+ "review-tab-pos-button-label": "Positif",
+ "review-tab-neg-button-label": "Negatif",
+ "review-tab-no-files-label": "Aucun fichiers trouvés",
+ "review-tab-regression-plot-label": "Regression",
+ "review-tab-no-species-found-error": "Aucune espèce n'a été trouvée dans le répertoire sélectionné.",
+ "review-tab-start-button-label": "Commencer le reviewing",
+ "review-tab-segment-matrix-count-header": "Compte",
+ "review-tab-regression-plot-x-label": "Confiance",
+ "review-tab-regression-plot-y-label-false": "Non",
+ "review-tab-regression-plot-y-label-true": "Oui",
+ "review-tab-autoplay-checkbox-label": "Lecture automatique",
+ "species-tab-title": "Espèces",
+ "species-tab-select-output-directory-button-label": "Sélectionner un dossier de sortie",
+ "species-tab-filename-textbox-label": "Nom du fichier, si non spécifié « species_list.txt » sera utilisé.",
+ "species-tab-sort-radio-label": "Trier par",
+ "species-tab-sort-radio-info": "Trier les espèces par fréquence d'apparition ou par ordre alphabétique.",
+ "species-tab-sort-radio-option-frequency": "fréquence",
+ "species-tab-sort-radio-option-alphabetically": "alphabétiquement",
+ "species-tab-finish-info": "La liste des espèces est sauvegardé à l’adresse:",
+ "species-tab-start-button-label": "Générer la liste des espèces",
+ "settings-tab-title": "Paramètres",
+ "settings-tab-language-dropdown-label": "Langue de l’interface",
+ "settings-tab-language-dropdown-info": "Les modifications ne prendront effet qu'après le redémarrage de l'application.",
+ "settings-tab-error-log-textbox-label": "Erreur fichiers journaux",
+ "settings-tab-error-log-textbox-info-path": "Chemin",
+ "settings-tab-error-log-textbox-placeholder": "Vide",
+ "validation-no-file-selected": "Sélectionnez un fichier",
+ "validation-no-directory-selected": "Veuillez sélectionner un dossier",
+ "validation-no-species-list-selected": "Veuillez sélectionner une liste d’espèces",
+ "validation-no-custom-classifier-selected": "Aucun classificateur personnalisé n'a été sélectionné.",
+ "validation-no-audio-files-found": "Aucun fichier audio trouvés.",
+ "validation-no-training-data-selected": "Veuillez sélectionner des données d’entraînement",
+ "validation-no-directory-for-classifier-selected": "Veuillez sélectionner un dossier pour le classificateur.",
+ "validation-no-valid-classifier-name": "Veuillez saisir un nom valide pour le classificateur.",
+ "validation-no-valid-epoch-number": "Veuillez saisir un nombre valide d'époques.",
+ "validation-no-valid-batch-size": "Veuillez saisir une taille de lot valide.",
+ "validation-no-valid-learning-rate": "Veuillez saisir un taux d'apprentissage valide.",
+ "validation-no-valid-frequency": "Veuillez saisir une fréquence valide dans",
+ "validation-no-audio-directory-selected": "Aucun dossiers audio sélectionnés",
+ "validation-no-negative-samples-in-binary-classification": "Les étiquettes négatives ne peuvent pas être utilisées avec la classification binaire",
+ "validation-non-event-samples-required-in-binary-classification": "Des échantillons sans événement sont nécessaires pour la classification binaire.",
+ "validation-only-repeat-upsampling-for-multi-label": "Seul l'échantillonnage ascendant répété est disponible pour les étiquettes multiples.",
+ "progress-preparing": "Préparation...",
+ "progress-starting": "Démarrage...",
+ "progress-build-classifier": "Chargement des données et construction d'un classificateur",
+ "progress-loading-data": "Chargement des données pour",
+ "progress-saving": "Enregistrement à",
+ "progress-training": "Modèle d’entraînement",
+ "progress-autotune": "Autotune en progression",
+ "progress-search": "Recherche des fichier...",
+ "footer-help": "Pour de la documentation, visiter"
+}
\ No newline at end of file
diff --git a/localization.py b/localization.py
index 7e1789d2..587f00b4 100644
--- a/localization.py
+++ b/localization.py
@@ -7,6 +7,7 @@
LANGUAGE_LOOKUP = {}
TARGET_LANGUAGE = FALLBACK_LANGUAGE
GUI_SETTINGS_PATH = os.path.join(SCRIPT_DIR, "gui-settings.json")
+STATE_SETTINGS_PATH = os.path.join(SCRIPT_DIR, "state.json")
def ensure_settings_file():
@@ -16,7 +17,29 @@ def ensure_settings_file():
f.write(json.dumps(settings, indent=4))
-def load_localization():
+def get_state_dict() -> dict:
+
+ try:
+ with open(STATE_SETTINGS_PATH, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except FileNotFoundError:
+ with open(STATE_SETTINGS_PATH, "w") as f:
+ json.dump({}, f)
+ return {}
+
+
+def get_state(key: str, default=None) -> str:
+ return get_state_dict().get(key, default)
+
+
+def set_state(key: str, value: str):
+ state = get_state_dict()
+ state[key] = value
+ with open(STATE_SETTINGS_PATH, "w") as f:
+ json.dump(state, f, indent=4)
+
+
+def load_local_state():
global LANGUAGE_LOOKUP
global TARGET_LANGUAGE
diff --git a/model.py b/model.py
index e2e87e66..c1b1dd90 100644
--- a/model.py
+++ b/model.py
@@ -22,7 +22,7 @@
# NOTE: we have to use TFLite if we want to use
# the metadata model or want to extract embeddings
try:
- import tflite_runtime.interpreter as tflite
+ import tflite_runtime.interpreter as tflite # type: ignore
except ModuleNotFoundError:
from tensorflow import lite as tflite
if not cfg.MODEL_PATH.endswith(".tflite"):
@@ -49,7 +49,9 @@ def loadModel(class_output=True):
# Do we have to load the tflite or protobuf model?
if cfg.MODEL_PATH.endswith(".tflite"):
# Load TFLite model and allocate tensors.
- INTERPRETER = tflite.Interpreter(model_path=os.path.join(SCRIPT_DIR, cfg.MODEL_PATH), num_threads=cfg.TFLITE_THREADS)
+ INTERPRETER = tflite.Interpreter(
+ model_path=os.path.join(SCRIPT_DIR, cfg.MODEL_PATH), num_threads=cfg.TFLITE_THREADS
+ )
INTERPRETER.allocate_tensors()
# Get input and output tensors.
@@ -114,7 +116,9 @@ def loadMetaModel():
global M_OUTPUT_LAYER_INDEX
# Load TFLite model and allocate tensors.
- M_INTERPRETER = tflite.Interpreter(model_path=os.path.join(SCRIPT_DIR, cfg.MDATA_MODEL_PATH), num_threads=cfg.TFLITE_THREADS)
+ M_INTERPRETER = tflite.Interpreter(
+ model_path=os.path.join(SCRIPT_DIR, cfg.MDATA_MODEL_PATH), num_threads=cfg.TFLITE_THREADS
+ )
M_INTERPRETER.allocate_tensors()
# Get input and output tensors.
diff --git a/requirements.txt b/requirements.txt
index 9076fc03..df6b3d49 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,8 @@ librosa==0.9.2
resampy
tensorflow==2.15.0
gradio
-webview
+pywebview
tqdm
bottle
-requests
\ No newline at end of file
+requests
+keras-tuner
\ No newline at end of file
diff --git a/segments.py b/segments.py
index 4ae560f8..068d4135 100644
--- a/segments.py
+++ b/segments.py
@@ -36,6 +36,21 @@ def detectRType(line: str):
return "csv"
else:
return "audacity"
+
+def getHeaderMapping(line: str) -> dict:
+ rtype = detectRType(line)
+ if rtype == "table" or rtype == "audacity":
+ sep = "\t"
+ else:
+ sep = ","
+
+ cols = line.split(sep)
+
+ mapping = {}
+ for i, col in enumerate(cols):
+ mapping[col] = i
+
+ return mapping
def parseFolders(apath: str, rpath: str, allowed_result_filetypes: list[str] = ["txt", "csv"]) -> list[dict]:
@@ -55,19 +70,34 @@ def parseFolders(apath: str, rpath: str, allowed_result_filetypes: list[str] = [
apath = apath.replace("/", os.sep).replace("\\", os.sep)
rpath = rpath.replace("/", os.sep).replace("\\", os.sep)
- # Get all audio files
- for root, _, files in os.walk(apath):
- for f in files:
- if f.rsplit(".", 1)[-1].lower() in cfg.ALLOWED_FILETYPES:
- data[f.rsplit(".", 1)[0]] = {"audio": os.path.join(root, f), "result": ""}
-
- # Get all result files
- for root, _, files in os.walk(rpath):
- for f in files:
- if f.rsplit(".", 1)[-1] in allowed_result_filetypes and ".BirdNET." in f:
- table_key = f.split(".BirdNET.", 1)[0]
- if table_key in data:
- data[table_key]["result"] = os.path.join(root, f)
+ # Check if combined selection table is present and read that.
+ if os.path.exists(os.path.join(rpath, cfg.OUTPUT_RAVEN_FILENAME)):
+ # Read combined Raven selection table
+ rfile = os.path.join(rpath, cfg.OUTPUT_RAVEN_FILENAME)
+ data["combined"] = {"isCombinedFile": True, "result": rfile}
+ elif os.path.exists(os.path.join(rpath, cfg.OUTPUT_CSV_FILENAME)):
+ rfile = os.path.join(rpath, cfg.OUTPUT_CSV_FILENAME)
+ data["combined"] = {"isCombinedFile": True, "result": rfile}
+ elif os.path.exists(os.path.join(rpath, cfg.OUTPUT_KALEIDOSCOPE_FILENAME)):
+ rfile = os.path.join(rpath, cfg.OUTPUT_KALEIDOSCOPE_FILENAME)
+ data["combined"] = {"isCombinedFile": True, "result": rfile}
+ elif os.path.exists(os.path.join(rpath, cfg.OUTPUT_RTABLE_FILENAME)):
+ rfile = os.path.join(rpath, cfg.OUTPUT_RTABLE_FILENAME)
+ data["combined"] = {"isCombinedFile": True, "result": rfile}
+ else:
+ # Get all audio files
+ for root, _, files in os.walk(apath):
+ for f in files:
+ if f.rsplit(".", 1)[-1].lower() in cfg.ALLOWED_FILETYPES:
+ data[os.path.join(root, f.rsplit(".", 1)[0])] = {"audio": os.path.join(root, f), "result": ""}
+
+ # Get all result files
+ for root, _, files in os.walk(rpath):
+ for f in files:
+ if f.rsplit(".", 1)[-1] in allowed_result_filetypes and ".BirdNET." in f:
+ table_key = os.path.join(root, f.split(".BirdNET.", 1)[0])
+ if table_key in data:
+ data[table_key]["result"] = os.path.join(root, f)
# Convert to list
flist = [f for f in data.values() if f["result"]]
@@ -89,13 +119,11 @@ def parseFiles(flist: list[dict], max_segments=100):
"""
species_segments: dict[str, list] = {}
- for f in flist:
- # Paths
- afile = f["audio"]
- rfile = f["result"]
+ is_combined_rfile = len(flist) == 1 and flist[0].get("isCombinedFile", False)
- # Get all segments for result file
- segments = findSegments(afile, rfile)
+ if is_combined_rfile:
+ rfile = flist[0]["result"]
+ segments = findSegmentsFromCombined(rfile)
# Parse segments by species
for s in segments:
@@ -103,6 +131,21 @@ def parseFiles(flist: list[dict], max_segments=100):
species_segments[s["species"]] = []
species_segments[s["species"]].append(s)
+ else:
+ for f in flist:
+ # Paths
+ afile = f["audio"]
+ rfile = f["result"]
+
+ # Get all segments for result file
+ segments = findSegments(afile, rfile)
+
+ # Parse segments by species
+ for s in segments:
+ if s["species"] not in species_segments:
+ species_segments[s["species"]] = []
+
+ species_segments[s["species"]].append(s)
# Shuffle segments for each species and limit to max_segments
for s in species_segments:
@@ -128,6 +171,79 @@ def parseFiles(flist: list[dict], max_segments=100):
return flist
+def findSegmentsFromCombined(rfile: str):
+ """Extracts the segments from a combined results file
+
+ Args:
+ rfile: Path to the result file.
+
+ Returns:
+ A list of dicts in the form of
+ {"audio": afile, "start": start, "end": end, "species": species, "confidence": confidence}
+ """
+ segments: list[dict] = []
+
+ # Open and parse result file
+ lines = utils.readLines(rfile)
+
+ # Auto-detect result type
+ rtype = detectRType(lines[0])
+
+ if rtype == "audacity":
+ raise Exception("Audacity files are not supported for combined results.")
+
+ # Get mapping from the header column
+ header_mapping = getHeaderMapping(lines[0])
+
+ # Get start and end times based on rtype
+ confidence = 0
+ start = end = 0.0
+ species = ""
+ afile = ""
+
+ for i, line in enumerate(lines):
+ if rtype == "table" and i > 0:
+ d = line.split("\t")
+ file_offset = float(d[header_mapping["File Offset (s)"]])
+ start = file_offset
+ end = file_offset + (float(d[header_mapping["End Time (s)"]]) - float(d[header_mapping["Begin Time (s)"]]))
+ species = d[header_mapping["Species Code"]]
+ confidence = float(d[header_mapping["Confidence"]])
+ afile = d[header_mapping["Begin Path"]].replace("/", os.sep).replace("\\", os.sep)
+
+ elif rtype == "r" and i > 0:
+ d = line.split(",")
+ start = float(d[header_mapping["start"]])
+ end = float(d[header_mapping["end"]])
+ species = d[header_mapping["common_name"]]
+ confidence = float(d[header_mapping["confidence"]])
+ afile = d[header_mapping["filepath"]].replace("/", os.sep).replace("\\", os.sep)
+
+ elif rtype == "kaleidoscope" and i > 0:
+ d = line.split(",")
+ start = float(d[header_mapping["OFFSET"]])
+ end = float(d[header_mapping["DURATION"]]) + start
+ species = d[header_mapping["scientific_name"]]
+ confidence = float(d[header_mapping["confidence"]])
+ in_dir = d[header_mapping["INDIR"]]
+ folder = d[header_mapping["FOLDER"]]
+ in_file = d[header_mapping["IN FILE"]]
+ afile = os.path.join(in_dir, folder, in_file).replace("/", os.sep).replace("\\", os.sep)
+
+ elif rtype == "csv" and i > 0:
+ d = line.split(",")
+ start = float(d[header_mapping["Start (s)"]])
+ end = float(d[header_mapping["End (s)"]])
+ species = d[header_mapping["Common name"]]
+ confidence = float(d[header_mapping["Confidence"]])
+ afile = d[header_mapping["File"]].replace("/", os.sep).replace("\\", os.sep)
+
+ # Check if confidence is high enough and label is not "nocall"
+ if confidence >= cfg.MIN_CONFIDENCE and species.lower() != "nocall" and afile:
+ segments.append({"audio": afile, "start": start, "end": end, "species": species, "confidence": confidence})
+
+ return segments
+
def findSegments(afile: str, rfile: str):
"""Extracts the segments for an audio file from the results file
@@ -148,6 +264,9 @@ def findSegments(afile: str, rfile: str):
# Auto-detect result type
rtype = detectRType(lines[0])
+ # Get mapping from the header column
+ header_mapping = getHeaderMapping(lines[0])
+
# Get start and end times based on rtype
confidence = 0
start = end = 0.0
@@ -155,12 +274,11 @@ def findSegments(afile: str, rfile: str):
for i, line in enumerate(lines):
if rtype == "table" and i > 0:
- # TODO: Use header columns to get the right indices
d = line.split("\t")
- start = float(d[3])
- end = float(d[4])
- species = d[-4]
- confidence = float(d[-3])
+ start = float(d[header_mapping["Begin Time (s)"]])
+ end = float(d[header_mapping["End Time (s)"]])
+ species = d[header_mapping["Species Code"]]
+ confidence = float(d[header_mapping["Confidence"]])
elif rtype == "audacity":
d = line.split("\t")
@@ -171,24 +289,24 @@ def findSegments(afile: str, rfile: str):
elif rtype == "r" and i > 0:
d = line.split(",")
- start = float(d[1])
- end = float(d[2])
- species = d[4]
- confidence = float(d[5])
+ start = float(d[header_mapping["start"]])
+ end = float(d[header_mapping["end"]])
+ species = d[header_mapping["common_name"]]
+ confidence = float(d[header_mapping["confidence"]])
elif rtype == "kaleidoscope" and i > 0:
d = line.split(",")
- start = float(d[3])
- end = float(d[4]) + start
- species = d[5]
- confidence = float(d[7])
+ start = float(d[header_mapping["OFFSET"]])
+ end = float(d[header_mapping["DURATION"]]) + start
+ species = d[header_mapping["scientific_name"]]
+ confidence = float(d[header_mapping["confidence"]])
elif rtype == "csv" and i > 0:
d = line.split(",")
- start = float(d[0])
- end = float(d[1])
- species = d[3]
- confidence = float(d[4])
+ start = float(d[header_mapping["Start (s)"]])
+ end = float(d[header_mapping["End (s)"]])
+ species = d[header_mapping["Common name"]]
+ confidence = float(d[header_mapping["Confidence"]])
# Check if confidence is high enough and label is not "nocall"
if confidence >= cfg.MIN_CONFIDENCE and species.lower() != "nocall":
diff --git a/train.py b/train.py
index 92b1f74e..fc978a62 100644
--- a/train.py
+++ b/train.py
@@ -41,6 +41,7 @@ def _loadAudioFile(f, label_vector, config):
except Exception as e:
# Print Error
print(f"\t Error when loading file {f}", flush=True)
+ print(f"\t {e}", flush=True)
return np.array([]), np.array([])
# Crop training samples
@@ -169,8 +170,12 @@ def _loadTrainingData(cache_mode="none", cache_file="", progress_callback=None):
with tqdm.tqdm(total=len(tasks), desc=f" - loading '{folder}'", unit='f') as progress_bar:
for task in tasks:
result = task.get()
- x_train += result[0]
- y_train += result[1]
+ # Make sure result is not empty
+ # Empty results might be caused by errors when loading the audio file
+ # TODO: We should check for embeddings size in result, otherwise we can't add them to the training data
+ if len(result[0]) > 0:
+ x_train += result[0]
+ y_train += result[1]
num_files_processed += 1
progress_bar.update(1)
if progress_callback:
diff --git a/utils.py b/utils.py
index b5c834bf..c02a113d 100644
--- a/utils.py
+++ b/utils.py
@@ -640,3 +640,18 @@ def save_model_params(file_path):
cfg.TRAIN_WITH_LABEL_SMOOTHING,
)
)
+
+def save_result_file(result_path: str, out_string: str):
+ """Saves the result to a file.
+
+ Args:
+ result_path: The path to the result file.
+ out_string: The string to be written to the file.
+ """
+
+ # Make directory if it doesn't exist
+ os.makedirs(os.path.dirname(result_path), exist_ok=True)
+
+ # Write the result to the file
+ with open(result_path, "w", encoding="utf-8") as rfile:
+ rfile.write(out_string)