From 0d30f5e85ba1da5f86dfa5219a8e583743489e4c Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Wed, 14 Aug 2024 14:32:53 +0200 Subject: [PATCH 01/23] Mac signed installer (#407) * remove build files * ignore optional import warning * small review update --------- Co-authored-by: Josef Haupt --- .gitignore | 5 +- BirdNET-Analyzer-mac.spec | 55 ----------------- BirdNET-Analyzer-win.spec | 122 -------------------------------------- deploy.py | 30 ---------- gui.py | 19 ++++-- model.py | 10 +++- requirements.txt | 2 +- 7 files changed, 23 insertions(+), 220 deletions(-) delete mode 100644 BirdNET-Analyzer-mac.spec delete mode 100644 BirdNET-Analyzer-win.spec delete mode 100644 deploy.py diff --git a/.gitignore b/.gitignore index e8ace913..bfb66f51 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ foo* installers/ desktop.ini *.iss +*deploy* # Custom classifier checkpoints/custom/ @@ -34,7 +35,7 @@ __pycache__/ # Distribution / packaging .Python -build/ +build* develop-eggs/ dist/ downloads/ @@ -58,8 +59,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 diff --git a/BirdNET-Analyzer-mac.spec b/BirdNET-Analyzer-mac.spec deleted file mode 100644 index 3d242f12..00000000 --- a/BirdNET-Analyzer-mac.spec +++ /dev/null @@ -1,55 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -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=None, - entitlements_file=None, - 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/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/gui.py b/gui.py index aba5bf04..e6b3d771 100644 --- a/gui.py +++ b/gui.py @@ -5,6 +5,9 @@ from pathlib import Path from functools import partial +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() @@ -34,7 +37,6 @@ import webview import analyze -import config as cfg import segments import species import utils @@ -1559,7 +1561,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 @@ -1580,13 +1582,18 @@ 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 = linear_model.LogisticRegression(C=55) log_model.fit([[x] for x in x_vals], y_val) Xs = np.linspace(0, 10, 200) Ys = expit(Xs * log_model.coef_ + log_model.intercept_).ravel() @@ -1615,7 +1622,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) @@ -1796,7 +1803,7 @@ 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 toggle_autoplay(value): return gr.Audio(autoplay=value) 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..a15ec764 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ librosa==0.9.2 resampy tensorflow==2.15.0 gradio -webview +pywebview tqdm bottle requests \ No newline at end of file From 132b6f8b76468f18a909313f11dc847d656f017d Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Wed, 14 Aug 2024 14:36:19 +0200 Subject: [PATCH 02/23] ops (#408) Co-authored-by: Josef Haupt --- gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui.py b/gui.py index e6b3d771..e3f761d9 100644 --- a/gui.py +++ b/gui.py @@ -1584,7 +1584,7 @@ def create_log_plot(positives, negatives, fig_num=None): try: x_val = float(os.path.basename(fl).split("_", 1)[0]) - if 0 < x_val > 1: + if 0 > x_val > 1: continue x_vals.append(x_val) From 6154906cb62e16f070eb8a1d82aa05bf352e6402 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Wed, 14 Aug 2024 16:17:25 +0200 Subject: [PATCH 03/23] Clean up (#409) * remove build files, move GUI-Version to build process --------- Co-authored-by: Josef Haupt --- .gitignore | 1 + config.py | 3 -- extra-hooks/hook-gradio.py | 3 -- extra-hooks/hook-gradio_client.py | 3 -- extra-hooks/hook-librosa.py | 3 -- gui.py | 6 +-- gui/gui.js | 74 ++++++++++++++++--------------- 7 files changed, 43 insertions(+), 50 deletions(-) delete mode 100644 extra-hooks/hook-gradio.py delete mode 100644 extra-hooks/hook-gradio_client.py delete mode 100644 extra-hooks/hook-librosa.py diff --git a/.gitignore b/.gitignore index bfb66f51..f74e890a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ installers/ desktop.ini *.iss *deploy* +*hook* # Custom classifier checkpoints/custom/ diff --git a/config.py b/config.py index 8cd95a0a..713385db 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 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 e3f761d9..b2f3e9ad 100644 --- a/gui.py +++ b/gui.py @@ -937,7 +937,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
@@ -1587,14 +1587,14 @@ def create_log_plot(positives, negatives, fig_num=None): if 0 > x_val > 1: continue - x_vals.append(x_val) + 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 = linear_model.LogisticRegression(C=55) - log_model.fit([[x] for x in x_vals], y_val) + 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] 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() { From ea751b7e0f846dc30eabbd3adce48daf79366ab8 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Thu, 15 Aug 2024 11:25:13 +0200 Subject: [PATCH 04/23] Update README.adoc (#411) --- README.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.adoc b/README.adoc index 55bbff73..99f05bfd 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 From 41d0047a9a930796b091bf1c2d6a9a518fb11a66 Mon Sep 17 00:00:00 2001 From: max-mauermann <40059289+max-mauermann@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:00:10 +0200 Subject: [PATCH 05/23] Segments with combined selection table (#413) * segments can now work with combined selection tables headervalues are now used to access columns segments does not skip files with the same name in different folders (though this should be avoided anyway) * combined r and kaleidoscope-files are now .csv r table doesnt have an empty row after the header * combined files are not read twice * segments use header_mapping for reading the files * removed ntpath import * use get + default value for dictionary --- analyze.py | 2 +- config.py | 4 +- segments.py | 190 ++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 157 insertions(+), 39 deletions(-) diff --git a/analyze.py b/analyze.py index 53a881fb..eb98f24f 100644 --- a/analyze.py +++ b/analyze.py @@ -111,7 +111,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, diff --git a/config.py b/config.py index 713385db..cdad2f95 100644 --- a/config.py +++ b/config.py @@ -109,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/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": From 8b4c4f5fee69443b8df264db294c712a57c76e74 Mon Sep 17 00:00:00 2001 From: max-mauermann <40059289+max-mauermann@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:35:38 +0200 Subject: [PATCH 06/23] name of the result file for the single analysis tab is constructed and returned correctly. (#414) --- gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui.py b/gui.py index b2f3e9ad..3e50dccf 100644 --- a/gui.py +++ b/gui.py @@ -318,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: @@ -389,7 +389,7 @@ 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") From 1e6028e7da7e683ce31323c6f66b71fc2a2cf7a4 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Tue, 20 Aug 2024 11:17:25 +0200 Subject: [PATCH 07/23] skip button --- gui.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/gui.py b/gui.py index b2f3e9ad..fc6f8e2d 100644 --- a/gui.py +++ b/gui.py @@ -1629,15 +1629,17 @@ def create_log_plot(positives, negatives, fig_num=None): return gr.Plot(value=f, visible=bool(y_val)) with gr.Tab(loc.localize("review-tab-title")): + from collections import defaultdict review_state = gr.State( { "input_directory": "", - "spcies_list": [], + "species_list": [], "current_species": "", "files": [], "current": 0, POSITIVE_LABEL_DIR: [], NEGATIVE_LABEL_DIR: [], + "history": defaultdict(list) } ) @@ -1653,6 +1655,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: @@ -1660,6 +1663,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( @@ -1691,6 +1697,7 @@ 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) + next_review_state["history"][next_review_state["current_species"]].append((current_file, target_dir)) update_dict |= { species_regression_plot: create_log_plot( @@ -1723,6 +1730,7 @@ def next_review(next_review_state: dict, target_dir: str = None): 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"]] + next_review_state["history"][next_review_state["current_species"]].append((current_file, None)) update_dict |= { review_audio: gr.Audio(next_file, label=os.path.basename(next_file)), @@ -1803,6 +1811,18 @@ 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["current"] > 0: + next_review_state["current"] -= 1 + + return { + review_audio: gr.Audio(next_review_state["files"][next_review_state["current"]]), + spectrogram_image: utils.spectrogram_from_file(next_review_state["files"][next_review_state["current"]], 1), + review_state: next_review_state, + } + + return {review_state: next_review_state} def toggle_autoplay(value): return gr.Audio(autoplay=value) @@ -1852,6 +1872,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, From c96480dff61baa869130cb9a0b375c9205888aa1 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Tue, 20 Aug 2024 13:45:31 +0200 Subject: [PATCH 08/23] undo button --- gui.py | 107 ++++++++++++++++++++++++++++++++------------------- lang/de.json | 2 + lang/en.json | 2 + 3 files changed, 72 insertions(+), 39 deletions(-) diff --git a/gui.py b/gui.py index bd2b8380..733879c2 100644 --- a/gui.py +++ b/gui.py @@ -389,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 result_list[0][1]["csv"] + 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") @@ -1629,7 +1631,6 @@ def create_log_plot(positives, negatives, fig_num=None): return gr.Plot(value=f, visible=bool(y_val)) with gr.Tab(loc.localize("review-tab-title")): - from collections import defaultdict review_state = gr.State( { "input_directory": "", @@ -1639,7 +1640,7 @@ def create_log_plot(positives, negatives, fig_num=None): "current": 0, POSITIVE_LABEL_DIR: [], NEGATIVE_LABEL_DIR: [], - "history": defaultdict(list) + "history": [], } ) @@ -1678,6 +1679,40 @@ 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 update_values(next_review_state, skip_plot=False): + update_dict = {} + + 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 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]), + ], + ], + } + + return update_dict + def next_review(next_review_state: dict, target_dir: str = None): current_file = next_review_state["files"][next_review_state["current"]] @@ -1697,40 +1732,16 @@ 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) - next_review_state["history"][next_review_state["current_species"]].append((current_file, target_dir)) - 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), - } + next_review_state["history"].append((current_file, target_dir)) - update_dict |= { - file_count_matrix: [ - [ - len(next_review_state["files"]), - len(next_review_state[POSITIVE_LABEL_DIR]), - len(next_review_state[NEGATIVE_LABEL_DIR]), - ], - ], - } + update_dict |= update_values(next_review_state) 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"]] - next_review_state["history"][next_review_state["current_species"]].append((current_file, None)) + + next_review_state["history"].append((current_file, None)) update_dict |= { review_audio: gr.Audio(next_file, label=os.path.basename(next_file)), @@ -1811,17 +1822,35 @@ 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["current"] > 0: - next_review_state["current"] -= 1 + 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), + ), + ) - return { - review_audio: gr.Audio(next_review_state["files"][next_review_state["current"]]), - spectrogram_image: utils.spectrogram_from_file(next_review_state["files"][next_review_state["current"]], 1), - review_state: next_review_state, - } + next_review_state[last_dir].remove(last_file) + next_review_state["files"].append(last_file) + next_review_state["current"] += 1 + + return {review_state: next_review_state} | update_values(next_review_state) + else: + next_review_state["current"] -= 1 + return {review_state: next_review_state} | update_values(next_review_state, skip_plot=True) return {review_state: next_review_state} def toggle_autoplay(value): 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.", From 7946d2a6168a812e292fe11237d618c0c6bb7e68 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Tue, 20 Aug 2024 15:37:00 +0200 Subject: [PATCH 09/23] save last directory selection --- .gitignore | 3 ++- gui.py | 68 ++++++++++++++++++++++++++++++++++--------------- localization.py | 25 +++++++++++++++++- 3 files changed, 74 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index f74e890a..c4b4312e 100644 --- a/.gitignore +++ b/.gitignore @@ -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/gui.py b/gui.py index 3e50dccf..8306525c 100644 --- a/gui.py +++ b/gui.py @@ -43,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__)) @@ -389,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 result_list[0][1]["csv"] + 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") @@ -442,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 = [] @@ -466,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: @@ -475,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): @@ -497,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. @@ -510,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: @@ -881,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, @@ -1027,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: @@ -1050,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, @@ -1162,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(): @@ -1183,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), @@ -1361,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), @@ -1382,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) @@ -1442,7 +1466,7 @@ def build_segments_tab(): output_directory_state = gr.State() def select_directory_to_state_and_tb(): - return (select_directory(collect_files=False),) * 2 + return (select_directory(collect_files=False, state_key="segments-data-dir"),) * 2 with gr.Row(): select_audio_directory_btn = gr.Button( @@ -1738,9 +1762,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()] @@ -1870,9 +1896,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), 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 From 2ca2b3ac975e610050fcafdc1251037495537d45 Mon Sep 17 00:00:00 2001 From: Stefan Kahl Date: Mon, 26 Aug 2024 11:03:03 +0200 Subject: [PATCH 10/23] Target dirs and imports (#419) * Prevent exception when emeddings result is empty * Make target dir for result file * add keras-tuner * move save results to utils --- analyze.py | 17 ++++++----------- requirements.txt | 3 ++- train.py | 9 +++++++-- utils.py | 15 +++++++++++++++ 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/analyze.py b/analyze.py index eb98f24f..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: @@ -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/requirements.txt b/requirements.txt index a15ec764..df6b3d49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ gradio pywebview tqdm bottle -requests \ No newline at end of file +requests +keras-tuner \ No newline at end of file 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) From 344a5705cdcb751eecf505cfcca1b275b28bb36a Mon Sep 17 00:00:00 2001 From: Max Mauermann Date: Mon, 26 Aug 2024 11:34:43 +0200 Subject: [PATCH 11/23] folders in segments tab each have their own statekey now --- gui.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui.py b/gui.py index 8306525c..ebd4659b 100644 --- a/gui.py +++ b/gui.py @@ -1465,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, state_key="segments-data-dir"),) * 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( @@ -1474,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, ) @@ -1489,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, ) @@ -1502,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, ) From ccbf1c2ed2d1a765de37e8fc4ab2679c6b837408 Mon Sep 17 00:00:00 2001 From: Max Mauermann Date: Mon, 26 Aug 2024 13:29:35 +0200 Subject: [PATCH 12/23] language changes always when checkbox is changed. removed the button output --- gui.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gui.py b/gui.py index ebd4659b..11130145 100644 --- a/gui.py +++ b/gui.py @@ -1976,11 +1976,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): From 59e66adaf89774de70392bd7710f204b263f710f Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 26 Aug 2024 13:56:22 +0200 Subject: [PATCH 13/23] clear history on species chane --- gui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui.py b/gui.py index 733879c2..721b45b2 100644 --- a/gui.py +++ b/gui.py @@ -1752,6 +1752,8 @@ def next_review(next_review_state: dict, target_dir: str = None): def select_subdir(new_value: str, next_review_state: dict): if new_value != next_review_state["current_species"]: + next_review_state["history"] = [] + return update_review(next_review_state, selected_species=new_value) else: return {review_state: next_review_state} From 7dc15a9df277be66954d1cf296ab44a3719bdd51 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 26 Aug 2024 14:06:06 +0200 Subject: [PATCH 14/23] disable undo btn if possible --- gui.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gui.py b/gui.py index 721b45b2..0e95be25 100644 --- a/gui.py +++ b/gui.py @@ -1748,12 +1748,14 @@ def next_review(next_review_state: dict, target_dir: str = None): spectrogram_image: utils.spectrogram_from_file(next_file), } + update_dict |= { + undo_btn: gr.Button(interactive=bool(next_review_state["history"])), + } + return update_dict def select_subdir(new_value: str, next_review_state: dict): if new_value != next_review_state["current_species"]: - next_review_state["history"] = [] - return update_review(next_review_state, selected_species=new_value) else: return {review_state: next_review_state} @@ -1776,6 +1778,8 @@ 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"] = [] + if selected_species: next_review_state["current_species"] = selected_species else: @@ -1794,6 +1798,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"]), @@ -1887,6 +1892,7 @@ def toggle_autoplay(value): no_samles_label, file_count_matrix, species_regression_plot, + undo_btn, ] positive_btn.click( From 8736e80a3d95dd9b296213b84a25140ed6e054ff Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 26 Aug 2024 14:16:29 +0200 Subject: [PATCH 15/23] fix --- gui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui.py b/gui.py index 0e95be25..2d9f5e2d 100644 --- a/gui.py +++ b/gui.py @@ -1875,6 +1875,7 @@ def toggle_autoplay(value): review_state, file_count_matrix, species_regression_plot, + undo_btn ] species_dropdown.change( From d1d76956cb62ba02180a3f05bc3ce013029ca192 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 26 Aug 2024 14:20:52 +0200 Subject: [PATCH 16/23] check on undo if button --- gui.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gui.py b/gui.py index 2d9f5e2d..983c7fca 100644 --- a/gui.py +++ b/gui.py @@ -1709,6 +1709,7 @@ def update_values(next_review_state, skip_plot=False): len(next_review_state[NEGATIVE_LABEL_DIR]), ], ], + undo_btn: gr.Button(interactive=bool(next_review_state["history"])), } return update_dict @@ -1858,7 +1859,10 @@ def undo_review(next_review_state): next_review_state["current"] -= 1 return {review_state: next_review_state} | update_values(next_review_state, skip_plot=True) - return {review_state: next_review_state} + 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) @@ -1875,7 +1879,7 @@ def toggle_autoplay(value): review_state, file_count_matrix, species_regression_plot, - undo_btn + undo_btn, ] species_dropdown.change( From bc39abe6e05154cfc41aeae670fab44a171fa578 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 26 Aug 2024 15:45:56 +0200 Subject: [PATCH 17/23] added skip state --- gui.py | 53 ++++++++++++++++++----------------------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/gui.py b/gui.py index 983c7fca..bd2ffbd7 100644 --- a/gui.py +++ b/gui.py @@ -1637,9 +1637,9 @@ def create_log_plot(positives, negatives, fig_num=None): "species_list": [], "current_species": "", "files": [], - "current": 0, POSITIVE_LABEL_DIR: [], NEGATIVE_LABEL_DIR: [], + "skipped": [], "history": [], } ) @@ -1680,7 +1680,7 @@ def create_log_plot(positives, negatives, fig_num=None): species_regression_plot = gr.Plot(label=loc.localize("review-tab-regression-plot-label")) def update_values(next_review_state, skip_plot=False): - update_dict = {} + update_dict = {next_review_state: next_review_state} if not skip_plot: update_dict |= { @@ -1689,22 +1689,22 @@ def update_values(next_review_state, skip_plot=False): ), } - if not next_review_state["files"]: + if next_review_state["files"]: + next_file = next_review_state["files"][0] update_dict |= { - no_samles_label: gr.Label(visible=True), - review_item_col: gr.Column(visible=False), + review_audio: gr.Audio(next_file, label=os.path.basename(next_file)), + spectrogram_image: utils.spectrogram_from_file(next_file), } 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), + 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["files"]) + len(next_review_state["skipped"]), len(next_review_state[POSITIVE_LABEL_DIR]), len(next_review_state[NEGATIVE_LABEL_DIR]), ], @@ -1715,9 +1715,7 @@ def update_values(next_review_state, skip_plot=False): return update_dict def next_review(next_review_state: dict, target_dir: str = None): - current_file = next_review_state["files"][next_review_state["current"]] - - update_dict = {review_state: next_review_state} + current_file = next_review_state["files"][0] if target_dir: selected_dir = os.path.join( @@ -1735,25 +1733,12 @@ def next_review(next_review_state: dict, target_dir: str = None): next_review_state["files"].remove(current_file) next_review_state["history"].append((current_file, target_dir)) - - update_dict |= update_values(next_review_state) 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"]] - - next_review_state["history"].append((current_file, None)) - - update_dict |= { - review_audio: gr.Audio(next_file, label=os.path.basename(next_file)), - spectrogram_image: utils.spectrogram_from_file(next_file), - } - - update_dict |= { - undo_btn: gr.Button(interactive=bool(next_review_state["history"])), - } + 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"]: @@ -1851,14 +1836,12 @@ def undo_review(next_review_state): ) next_review_state[last_dir].remove(last_file) - next_review_state["files"].append(last_file) - next_review_state["current"] += 1 - - return {review_state: next_review_state} | update_values(next_review_state) else: - next_review_state["current"] -= 1 + next_review_state["skipped"].remove(last_file) + + next_review_state["files"].insert(0, last_file) - return {review_state: next_review_state} | update_values(next_review_state, skip_plot=True) + 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"])), From 24ce88190d66acba0a0cee7792244d20e6446ab4 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 26 Aug 2024 16:31:21 +0200 Subject: [PATCH 18/23] . --- gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui.py b/gui.py index bd2ffbd7..e88daaf1 100644 --- a/gui.py +++ b/gui.py @@ -1680,7 +1680,7 @@ def create_log_plot(positives, negatives, fig_num=None): species_regression_plot = gr.Plot(label=loc.localize("review-tab-regression-plot-label")) def update_values(next_review_state, skip_plot=False): - update_dict = {next_review_state: next_review_state} + update_dict = {review_state: next_review_state} if not skip_plot: update_dict |= { From 3a071a6d1ba0a95861cbaa94c7e46dd1d22a9dbc Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 26 Aug 2024 16:43:30 +0200 Subject: [PATCH 19/23] reset skipped files on selectin change --- gui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui.py b/gui.py index e88daaf1..d3c6caa7 100644 --- a/gui.py +++ b/gui.py @@ -1765,6 +1765,7 @@ def start_review(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 From acc25a75ededcf9104ba1688616243e9952616c3 Mon Sep 17 00:00:00 2001 From: Josef Haupt Date: Mon, 26 Aug 2024 17:31:31 +0200 Subject: [PATCH 20/23] Update README.adoc (#423) --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 99f05bfd..7184a0e5 100644 --- a/README.adoc +++ b/README.adoc @@ -777,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. From 9dcdd1dd074fc67a0f2cfeabfc35957ead7c897f Mon Sep 17 00:00:00 2001 From: Max Mauermann Date: Tue, 27 Aug 2024 14:47:43 +0200 Subject: [PATCH 21/23] fix threshold typo in regression graph --- gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui.py b/gui.py index d3c6caa7..1073d28b 100644 --- a/gui.py +++ b/gui.py @@ -1614,7 +1614,7 @@ def create_log_plot(positives, negatives, fig_num=None): color=p_color, linestyle="--", linewidth=0.5, - label=f"p={target_p:.2f} treshold>={threshold:.2f}", + label=f"p={target_p:.2f} threshold>={threshold:.2f}", ) ax.hlines(target_p, 0, threshold, color=p_color, linestyle="--", linewidth=0.5) From 5762857a8a37e8d5ddb4a2deb88e5a53e8acf341 Mon Sep 17 00:00:00 2001 From: Max Mauermann Date: Tue, 27 Aug 2024 15:42:59 +0200 Subject: [PATCH 22/23] dont show thresholds above 1.0 --- gui.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/gui.py b/gui.py index 1073d28b..5904a7c2 100644 --- a/gui.py +++ b/gui.py @@ -1607,16 +1607,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} threshold>={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") From 62245fc1c8681e7b7e63d71e10bb11fe74aec783 Mon Sep 17 00:00:00 2001 From: Max Mauermann Date: Tue, 27 Aug 2024 16:21:04 +0200 Subject: [PATCH 23/23] Included french translation by FranciumSoftware --- lang/fr.json | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 lang/fr.json 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