Feat : change specie detected (#62)

* Add changedetection function

* Create birdnet_changeidentification.sh

* Add files via upload

* Use a modal box for display of the species

* Allow filtering on safari ios

* Reset the dropdown if cancelled

* Fix : click outside modal box to cancel it in safari ios

* Switch modal style to style.css

* Switch modal style to style.css

* Improves modal rendition on small screens

* Use ./scripts/labels.txt instead of hardcoded home

* Added physical cancel button

* ensure user pi executes the scripts

https://github.com/Nachtzuster/BirdNET-Pi/pull/62

* Specify exec if not explicit

https://github.com/Nachtzuster/BirdNET-Pi/pull/62

* Try another way to define the variables

https://github.com/Nachtzuster/BirdNET-Pi/pull/62

* Align shebang with other scripts

https://github.com/Nachtzuster/BirdNET-Pi/pull/62

* Allow different HOME

https://github.com/Nachtzuster/BirdNET-Pi/pull/62

* Update birdnet_changeidentification.sh

* Align way to call sudo -u $user with other birdnet-pi code

https://github.com/Nachtzuster/BirdNET-Pi/pull/62

* Align with proposal

https://github.com/Nachtzuster/BirdNET-Pi/pull/62

* Successfully converted message

https://github.com/Nachtzuster/BirdNET-Pi/pull/62

* Align "safe" variables logic to helpers.py

* Make birdnet_changeidentification.sh executable

* Force confidence to 1.001

* Set confidence to 0

https://github.com/Nachtzuster/BirdNET-Pi/pull/62
This commit is contained in:
Alexandre
2024-05-30 11:30:09 +02:00
committed by GitHub
parent b07c28bbfb
commit 2fd75ccafc
4 changed files with 316 additions and 2 deletions
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# This scripts allows to change the identification of a Birdnet-pi detection
#################
# SET VARIABLES #
#################
# Define HOME in case environment is not correctly set
HOME="${HOME:-/home/pi}"
# shellcheck disable=sc1091
source /etc/birdnet/birdnet.conf &>/dev/null
# Get arguments
OLDNAME="$1" #OLDNAME="Mésange_charbonnière-78-2024-05-02-birdnet-RTSP_1-18:14:08.mp3"
NEWNAME="$2" #NEWNAME="Lapinus atricapilla_Lapinu à tête noire"
# Set log level
OUTPUT_TYPE="${3:-debug}" # Set 3rd argument to debug to have all outputs
# Ask for user input if no arguments
if [ -z "$OLDNAME" ]; then read -r -p 'OLDNAME (finishing by mp3): ' OLDNAME; fi
if [ -z "$NEWNAME" ]; then read -r -p 'NEWNAME (sciname_commoname): ' NEWNAME; fi
# Fixed values
LABELS_FILE="$HOME/BirdNET-Pi/model/labels.txt"
DB_FILE="$HOME/BirdNET-Pi/scripts/birds.db"
DETECTIONS_TABLE="detections"
###################
# VALIDITY CHECKS #
###################
# Check if files exist
if [ ! -f "$LABELS_FILE" ]; then echo "$LABELS_FILE doesn't exist, exiting" && exit 1; fi
if [ ! -f "$DB_FILE" ]; then echo "$DB_FILE doesn't exist, exiting" && exit 1; fi
# Check if inputs are valid
if [[ "$1" != *".mp3" ]]; then
echo "The first argument should be a filename starting with the common name of the bird and finishing by mp3!"
echo "Instead, it is : $1"
exit 1
elif [[ "$2" != *"_"* ]]; then
echo "The second argument should be in the format : \"scientific name_common name\""
echo "Instead, it is : $2"
exit 1
fi
# Check if $NEWNAME is found in the file $LABELS_FILE
if ! grep -q "$NEWNAME" "$LABELS_FILE"; then
echo "Error: $NEWNAME not found in $LABELS_FILE"
exit 1
fi
# Check if the common name as the same as the first
OLDNAME_space="${OLDNAME//_/ }"
if [[ "${OLDNAME_space%%-*}" == "${NEWNAME#*_}" ]]; then
echo "Error: $OLDNAME has the same common name as $NEWNAME"
exit 1
fi
##################
# EXECUTE SCRIPT #
##################
# Intro
[[ "$OUTPUT_TYPE" == "debug" ]] && echo "Starting to modify $OLDNAME to $NEWNAME"
# Get the line where the column "File_Name" matches exactly $OLDNAME
IFS='|' read -r OLDNAME_sciname OLDNAME_comname OLDNAME_date < <(sqlite3 "$DB_FILE" "SELECT Sci_Name, Com_Name, Date FROM $DETECTIONS_TABLE WHERE File_Name = '$OLDNAME' LIMIT 1;")
if [[ -z "$OLDNAME_sciname" ]]; then
echo "Error: No line matching $OLDNAME in $DB_FILE"
exit 1
fi
# Extract the part before the _ from $NEWNAME
NEWNAME_comname="${NEWNAME#*_}"
NEWNAME_sciname="${NEWNAME%%_*}"
# Replace spaces with underscores, and ' with "" (same logic as helpers.py)
NEWNAME_comname_safe="$(echo "$NEWNAME_comname" | tr -d "'" | tr ' ' '_')"
OLDNAME_comname_safe="$(echo "$OLDNAME_comname" | tr -d "'" | tr ' ' '_')"
# Replace OLDNAME_comname_safe with NEWNAME_comname_safe in OLDNAME
NEWNAME_filename="${OLDNAME//$OLDNAME_comname_safe/$NEWNAME_comname_safe}"
[[ "$OUTPUT_TYPE" == "debug" ]] && echo "This script will change the identification $OLDNAME from $OLDNAME_comname to ${NEWNAME#*_}"
########################
# EXECUTE : MOVE FILES #
########################
# Check if the file exists
FILE_PATH="$HOME/BirdSongs/Extracted/By_Date/$OLDNAME_date/$OLDNAME_comname_safe/$OLDNAME"
if [[ -f $FILE_PATH ]]; then
# Ensure the new directory exists
NEW_DIR="$HOME/BirdSongs/Extracted/By_Date/$OLDNAME_date/$NEWNAME_comname_safe"
mkdir -p "$NEW_DIR"
# Move and rename the file
mv "$FILE_PATH" "$NEW_DIR/$NEWNAME_filename"
mv "$FILE_PATH".png "$NEW_DIR/$NEWNAME_filename".png
[[ "$OUTPUT_TYPE" == "debug" ]] && echo "Files moved!"
else
echo "Error: File $FILE_PATH does not exist"
fi
###################################
# EXECUTE : UPDATE DATABASE FILES #
###################################
# Update the database
sqlite3 "$DB_FILE" "UPDATE $DETECTIONS_TABLE SET Sci_Name = '$NEWNAME_sciname', Com_Name = '$NEWNAME_comname', Confidence = '0', File_Name = '$NEWNAME_filename' WHERE File_Name = '$OLDNAME';"
[[ "$OUTPUT_TYPE" == "debug" ]] && echo "Database entry removed"
[[ "$OUTPUT_TYPE" == "debug" ]] && echo "All done!"
+142 -2
View File
@@ -9,6 +9,7 @@ ini_set('display_errors',1);
require_once 'scripts/common.php';
$home = get_home();
$config = get_config();
$user = get_user();
$db = new SQLite3('./scripts/birds.db', SQLITE3_OPEN_READONLY);
$db->busyTimeout(1000);
@@ -67,6 +68,28 @@ if(isset($_GET['excludefile'])) {
}
}
if(isset($_GET['getlabels'])) {
$labels = file('./scripts/labels.txt', FILE_IGNORE_NEW_LINES);
echo json_encode($labels);
die();
}
if(isset($_GET['changefile']) && isset($_GET['newname'])) {
ensure_authenticated('You must be authenticated to delete files.');
if (preg_match('~^.*(\.\.\/).+$~', $_GET['changefile'])) {
echo "Error";
die();
}
$oldname = basename(urldecode($_GET['changefile']));
$newname = urldecode($_GET['newname']);
if (!exec("sudo -u ".$user." ".$home."/BirdNET-Pi/scripts/birdnet_changeidentification.sh \"$oldname\" \"$newname\" log_errors 2>&1", $output)) {
echo "OK";
} else {
echo "Error : " . implode(", ", $output) . "<br>";
}
die();
}
$shifted_path = $home."/BirdSongs/Extracted/By_Date/shifted/";
if(isset($_GET['shiftfile'])) {
@@ -163,6 +186,7 @@ if (get_included_files()[0] === __FILE__) {
?>
<script>
function deleteDetection(filename,copylink=false) {
if (confirm("Are you sure you want to delete this detection from the database?") == true) {
const xhttp = new XMLHttpRequest();
@@ -253,6 +277,120 @@ function toggleShiftFreq(filename, shiftAction, elem) {
xhttp.send();
elem.setAttribute("src","images/spinner.gif");
}
function changeDetection(filename,copylink=false) {
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
const labels = JSON.parse(this.responseText);
let dropdown = '<input type="text" id="filterInput" placeholder="Type to filter..."> <button id="cancelButton">Cancel</button> <br><select id="labelDropdown" size="5" style="display: block; margin: 0 auto;"></select>';
// Check if the modal already exists
let modal = document.getElementById('myModal');
if (!modal) {
// Create a modal box
modal = document.createElement('div');
modal.setAttribute('id', 'myModal');
modal.setAttribute('class', 'modal');
// Create a content box
let content = document.createElement('div');
content.setAttribute('class', 'modal-content');
// Add a title to the modal box
let title = document.createElement('h2');
title.textContent = 'Please select the correct specie here:';
content.appendChild(title);
// Add the dropdown to the content
let selectElement = document.createElement('div');
selectElement.innerHTML = dropdown;
content.appendChild(selectElement);
// Append the content to the modal
modal.appendChild(content);
// Append the modal to the body
document.body.appendChild(modal);
}
// Display the modal
modal.style.display = "block";
// Populate the dropdown list
let dropdownList = document.getElementById('labelDropdown');
labels.forEach(label => {
let option = document.createElement('option');
option.value = label;
option.text = label;
dropdownList.appendChild(option);
});
// Add an event listener to the modal box to hide it when clicked outside
document.addEventListener('click', function(event) {
if (event.target == modal) {
modal.style.display = "none";
dropdownList.selectedIndex = -1; // Reset the dropdown selection
}
});
// Add an event listener to the input box to filter the dropdown list
document.getElementById('filterInput').addEventListener('keyup', function() {
let filter = this.value.toUpperCase();
let options = dropdownList.options;
// Clear the dropdown list
while (dropdownList.firstChild) {
dropdownList.removeChild(dropdownList.firstChild);
}
// Populate the dropdown list with the filtered labels
labels.forEach(label => {
if (label.toUpperCase().indexOf(filter) > -1) {
let option = document.createElement('option');
option.value = label;
option.text = label;
dropdownList.appendChild(option);
}
});
});
// Add an event listener to the cancel button to hide the modal box
document.getElementById('cancelButton').addEventListener('click', function() {
modal.style.display = "none";
dropdownList.selectedIndex = -1; // Reset the dropdown selection
});
dropdownList.addEventListener('change', function() {
const newname = this.value;
// Check if the default option is selected
if (newname === '') {
return; // Exit the function early
}
if (confirm("Are you sure you want to change the specie identified in this detection to " + newname + "?") == true) {
const xhttp2 = new XMLHttpRequest();
xhttp2.onload = function() {
if(this.responseText == "OK"){
if(copylink == true) {
alert("Successfully converted");
window.top.close();
} else {
alert("Successfully converted");
location.reload();
}
} else {
alert(this.responseText);
}
}
xhttp2.open("GET", "play.php?changefile="+filename+"&newname="+newname, true);
xhttp2.send();
}
// Hide the modal box and reset the dropdown selection
modal.style.display = "none";
this.selectedIndex = -1;
});
}
xhttp.open("GET", "play.php?getlabels=true", true);
xhttp.send();
}
</script>
<?php
@@ -479,7 +617,8 @@ echo "<table>
echo "<tr>
<td class=\"relative\">
<img style='cursor:pointer;right:90px' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\")' class=\"copyimage\" width=25 title='Delete Detection'>
<img style='cursor:pointer;right:120px' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\")' class=\"copyimage\" width=25 title='Delete Detection'>
<img style='cursor:pointer;right:85px' src='images/bird.svg' onclick='changeDetection(\"".$filename_formatted."\")' class=\"copyimage\" width=25 title='Change Detection'>
<img style='cursor:pointer;right:45px' onclick='toggleLock(\"".$filename_formatted."\",\"".$type."\", this)' class=\"copyimage\" width=25 title=\"".$title."\" src=\"".$imageicon."\">
<img style='cursor:pointer' onclick='toggleShiftFreq(\"".$filename_formatted."\",\"".$shiftAction."\", this)' class=\"copyimage\" width=25 title=\"".$shiftTitle."\" src=\"".$shiftImageIcon."\"> $date $time<br>$confidence<br>
@@ -553,7 +692,8 @@ echo "<table>
echo "<tr>
<td class=\"relative\">
<img style='cursor:pointer;right:90px' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\", true)' class=\"copyimage\" width=25 title='Delete Detection'>
<img style='cursor:pointer;right:120px' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\", true)' class=\"copyimage\" width=25 title='Delete Detection'>
<img style='cursor:pointer;right:85px' src='images/bird.svg' onclick='changeDetection(\"".$filename_formatted."\")' class=\"copyimage\" width=25 title='Change Detection'>
<img style='cursor:pointer;right:45px' onclick='toggleLock(\"".$filename_formatted."\",\"".$type."\", this)' class=\"copyimage\" width=25 title=\"".$title."\" src=\"".$imageicon."\">
<img style='cursor:pointer' onclick='toggleShiftFreq(\"".$filename_formatted."\",\"".$shiftAction."\", this)' class=\"copyimage\" width=25 title=\"".$shiftTitle."\" src=\"".$shiftImageIcon."\">$date $time<br>$confidence<br>