diff --git a/birdnet-pi/CHANGELOG.md b/birdnet-pi/CHANGELOG.md
index 3b15fd555..cac0bdc64 100644
--- a/birdnet-pi/CHANGELOG.md
+++ b/birdnet-pi/CHANGELOG.md
@@ -1,3 +1,5 @@
+- Significantly improve the SPECIES_CONVERTER option, and add a webui when the option is enabled
+
## 0.13-28 (17-05-2024)
- Improve code clarity by separating modifications of code to make it work, and new features specific to the addon
- New option SPECIES_CONVERTER: if enabled, you need to put in the file /config/convert_species_list.txt the list of species you want to convert (example : Falco subbuteo_Faucon hobereau;Falco tinnunculus_Faucon Crécerelle). It will convert on the fly the specie when detected. This is not enabled by default as can be a cause for issues
diff --git a/birdnet-pi/config.json b/birdnet-pi/config.json
index 73ab239ab..1147e74d6 100644
--- a/birdnet-pi/config.json
+++ b/birdnet-pi/config.json
@@ -102,6 +102,6 @@
"udev": true,
"url": "https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pi",
"usb": true,
- "version": "0.13-28",
+ "version": "0.13-29",
"video": true
}
diff --git a/birdnet-pi/rootfs/etc/cont-init.d/72-newfeatures.sh b/birdnet-pi/rootfs/etc/cont-init.d/72-newfeatures.sh
index 9034ec4e6..59dfd5a69 100755
--- a/birdnet-pi/rootfs/etc/cont-init.d/72-newfeatures.sh
+++ b/birdnet-pi/rootfs/etc/cont-init.d/72-newfeatures.sh
@@ -55,32 +55,17 @@ fi
# Add species conversion system
if bashio::config.true "SPECIES_CONVERTER"; then
- bashio::log.yellow "... adding feature of SPECIES_CONVERTER, please see README to use"
+ bashio::log.yellow "... adding feature of SPECIES_CONVERTER, a new tab is added to your Tools"
touch /config/convert_species_list.txt
chown pi:pi /config/convert_species_list.txt
sudo -u pi ln -fs /config/convert_species_list.txt "$HOME"/BirdNET-Pi/
# Not useful
sed -i "/exclude_species_list.txt/a sudo -u pi ln -fs /config/convert_species_list.txt $HOME/BirdNET-Pi/scripts/" "$HOME"/BirdNET-Pi/scripts/clear_all_data.sh
sed -i "/exclude_species_list.txt/a sudo -u pi ln -fs /config/convert_species_list.txt $HOME/BirdNET-Pi/scripts/" "$HOME"/BirdNET-Pi/scripts/install_services.sh
- # Change server
- sed -i "/INTERPRETER, M_INTERPRETER, INCLUDE_LIST, EXCLUDE_LIST/c INTERPRETER, M_INTERPRETER, INCLUDE_LIST, EXCLUDE_LIST, CONVERT_LIST = (None, None, None, None, None)" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/global INCLUDE_LIST, EXCLUDE_LIST/c\ global INCLUDE_LIST, EXCLUDE_LIST, CONVERT_LIST, CONVERT_DICT" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/exclude_species_list.txt/a\ CONVERT_DICT = {row.split(';')[0]: row.split(';')[1] for row in CONVERT_LIST}" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/exclude_species_list.txt/a\ CONVERT_LIST = loadCustomSpeciesList(os.path.expanduser(\"~/BirdNET-Pi/convert_species_list.txt\"))" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "s|entry\[0\]|converted_entry|g" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "s|if converted_entry in|if entry\[0\] in|g" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/for entry in entries/a\ converted_entry = entry[0]" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/for entry in entries/a\ else :" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/for entry in entries/a\ log.info('WARNING : ' + entry[0] + ' converted to ' + converted_entry)" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/for entry in entries/a\ converted_entry = CONVERT_DICT.get(entry[0], entry[0])" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/for entry in entries/a\ if entry[0] in CONVERT_DICT:" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/for entry in entries/a\ if entry[1] >= conf.getfloat('CONFIDENCE'):" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "/converted_entry in INCLUDE_LIST or len(INCLUDE_LIST)/c\ if ((converted_entry in INCLUDE_LIST or len(INCLUDE_LIST) == 0)" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "s| d = Detection| d = Detection|g" "$HOME"/BirdNET-Pi/scripts/server.py
- sed -i "s| confident_detections| confident_detections|g" "$HOME"/BirdNET-Pi/scripts/server.py
- curl -o /home/pi/BirdNET-Pi/scripts/convert_list.php https://raw.githubusercontent.com/alexbelgium/BirdNET-Pi/patch-2_species_conversion/scripts/convert_list.php
- chmod 777 /home/pi/BirdNET-Pi/scripts/convert_list.php
- #sed -i '/Excluded Species List/\ Convert Species List ' "$HOME"/BirdNET-Pi/homepage/views.php
+ # Add files
+ mv -f /helpers/convert_list/convert_list.php "$HOME"/BirdNET-Pi/scripts/convert_list.php && chown pi:pi "$HOME"/BirdNET-Pi/scripts/convert_list.php
+ mv -f /helpers/convert_list/server.py "$HOME"/BirdNET-Pi/scripts/server.py && chown pi:pi "$HOME"/BirdNET-Pi/scripts/server.py
+ mv -f /helpers/convert_list/views.php "$HOME"/BirdNET-Pi/homepage/views.php && chown pi:pi "$HOME"/BirdNET-Pi/homepage/views.php
fi
echo " "
diff --git a/birdnet-pi/rootfs/helpers/change_id/bird.svg b/birdnet-pi/rootfs/helpers/change_id/bird.svg
new file mode 100644
index 000000000..2fa6ab0dc
--- /dev/null
+++ b/birdnet-pi/rootfs/helpers/change_id/bird.svg
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/birdnet-pi/rootfs/helpers/changeidentification.sh b/birdnet-pi/rootfs/helpers/change_id/birdnet_changeidentification.sh
old mode 100755
new mode 100644
similarity index 100%
rename from birdnet-pi/rootfs/helpers/changeidentification.sh
rename to birdnet-pi/rootfs/helpers/change_id/birdnet_changeidentification.sh
diff --git a/birdnet-pi/rootfs/helpers/change_id/play.php.txt b/birdnet-pi/rootfs/helpers/change_id/play.php.txt
new file mode 100644
index 000000000..0c1827fe8
--- /dev/null
+++ b/birdnet-pi/rootfs/helpers/change_id/play.php.txt
@@ -0,0 +1,705 @@
+busyTimeout(1000);
+
+if(isset($_GET['deletefile'])) {
+ ensure_authenticated('You must be authenticated to delete files.');
+ if (preg_match('~^.*(\.\.\/).+$~', $_GET['deletefile'])) {
+ echo "Error";
+ die();
+ }
+ $db_writable = new SQLite3('./scripts/birds.db', SQLITE3_OPEN_READWRITE);
+ $db->busyTimeout(1000);
+ $statement1 = $db_writable->prepare('DELETE FROM detections WHERE File_Name = :file_name LIMIT 1');
+ ensure_db_ok($statement1);
+ $statement1->bindValue(':file_name', explode("/", $_GET['deletefile'])[2]);
+ $file_pointer = $home."/BirdSongs/Extracted/By_Date/".$_GET['deletefile'];
+ if (!exec("sudo rm $file_pointer 2>&1 && sudo rm $file_pointer.png 2>&1", $output)) {
+ echo "OK";
+ } else {
+ echo "Error - file deletion failed : " . implode(", ", $output) . " ";
+ }
+ $result1 = $statement1->execute();
+ if ($result1 === false || $db_writable->changes() === 0) {
+ echo "Error - database line deletion failed : " . $db_writable->lastErrorMsg();
+ }
+ $db_writable->close();
+ die();
+}
+
+if(isset($_GET['excludefile'])) {
+ ensure_authenticated('You must be authenticated to change the protection of files.');
+ if(!file_exists($home."/BirdNET-Pi/scripts/disk_check_exclude.txt")) {
+ file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "##start\n##end\n");
+ }
+ if(isset($_GET['exclude_add'])) {
+ $myfile = fopen($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "a") or die("Unable to open file!");
+ $txt = $_GET['excludefile'];
+ fwrite($myfile, $txt."\n");
+ fwrite($myfile, $txt.".png\n");
+ fclose($myfile);
+ echo "OK";
+ die();
+ } else {
+ $lines = file($home."/BirdNET-Pi/scripts/disk_check_exclude.txt");
+ $search = $_GET['excludefile'];
+
+ $result = '';
+ foreach($lines as $line) {
+ if(stripos($line, $search) === false && stripos($line, $search.".png") === false) {
+ $result .= $line;
+ }
+ }
+ file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", $result);
+ echo "OK";
+ die();
+ }
+}
+
+if(isset($_GET['getlabels'])) {
+ $labels = file('/home/pi/BirdNET-Pi/model/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("$home/BirdNET-Pi/scripts/birdnet_changeidentification.sh \"$oldname\" \"$newname\" log_errors 2>&1", $output)) {
+ echo "OK";
+ } else {
+ echo "Error : " . implode(", ", $output) . " ";
+ }
+ die();
+}
+
+$shifted_path = $home."/BirdSongs/Extracted/By_Date/shifted/";
+
+if(isset($_GET['shiftfile'])) {
+ ensure_authenticated('You cannot shift files for this installation');
+
+ $filename = $_GET['shiftfile'];
+ $pp = pathinfo($filename);
+ $dir = $pp['dirname'];
+ $fn = $pp['filename'];
+ $ext = $pp['extension'];
+ $pi = $home."/BirdSongs/Extracted/By_Date/";
+
+ if(isset($_GET['doshift'])) {
+ $freqshift_tool = $config['FREQSHIFT_TOOL'];
+
+ if ($freqshift_tool == "ffmpeg") {
+ $cmd = "sudo /usr/bin/nohup /usr/bin/ffmpeg -y -i ".escapeshellarg($pi.$filename)." -af \"rubberband=pitch=".$config['FREQSHIFT_LO']."/".$config['FREQSHIFT_HI']."\" ".escapeshellarg($shifted_path.$filename)."";
+ shell_exec("sudo mkdir -p ".$shifted_path.$dir." && ".$cmd);
+
+ } else if ($freqshift_tool == "sox") {
+ //linux.die.net/man/1/sox
+ $soxopt = "-q";
+ $soxpitch = $config['FREQSHIFT_PITCH'];
+ $cmd = "sudo /usr/bin/nohup /usr/bin/sox ".escapeshellarg($pi.$filename)." ".escapeshellarg($shifted_path.$filename)." pitch ".$soxopt." ".$soxpitch;
+ shell_exec("sudo mkdir -p ".$shifted_path.$dir." && ".$cmd);
+ }
+ } else {
+ $cmd = "sudo rm -f " . escapeshellarg($shifted_path.$filename);
+ shell_exec($cmd);
+ }
+
+ echo "OK";
+ die();
+}
+
+if(isset($_GET['bydate'])){
+ $statement = $db->prepare('SELECT DISTINCT(Date) FROM detections GROUP BY Date ORDER BY Date DESC');
+ ensure_db_ok($statement);
+ $result = $statement->execute();
+ $view = "bydate";
+
+ #Specific Date
+} elseif(isset($_GET['date'])) {
+ $date = $_GET['date'];
+ session_start();
+ $_SESSION['date'] = $date;
+ if(isset($_GET['sort']) && $_GET['sort'] == "occurrences") {
+ $statement = $db->prepare("SELECT DISTINCT(Com_Name) FROM detections WHERE Date == \"$date\" GROUP BY Com_Name ORDER BY COUNT(*) DESC");
+ } else {
+ $statement = $db->prepare("SELECT DISTINCT(Com_Name) FROM detections WHERE Date == \"$date\" ORDER BY Com_Name");
+ }
+ ensure_db_ok($statement);
+ $result = $statement->execute();
+ $view = "date";
+
+ #By Species
+} elseif(isset($_GET['byspecies'])) {
+ if(isset($_GET['sort']) && $_GET['sort'] == "occurrences") {
+ $statement = $db->prepare('SELECT DISTINCT(Com_Name) FROM detections GROUP BY Com_Name ORDER BY COUNT(*) DESC');
+ } else {
+ $statement = $db->prepare('SELECT DISTINCT(Com_Name) FROM detections ORDER BY Com_Name ASC');
+ }
+ session_start();
+ ensure_db_ok($statement);
+ $result = $statement->execute();
+ $view = "byspecies";
+
+ #Specific Species
+} elseif(isset($_GET['species'])) {
+ $species = htmlspecialchars_decode($_GET['species'], ENT_QUOTES);
+ session_start();
+ $_SESSION['species'] = $species;
+ $statement = $db->prepare("SELECT * FROM detections WHERE Com_Name == \"$species\" ORDER BY Com_Name");
+ ensure_db_ok($statement);
+ $statement3 = $db->prepare("SELECT Date, Time, Sci_Name, MAX(Confidence), File_Name FROM detections WHERE Com_Name == \"$species\" ORDER BY Com_Name");
+ ensure_db_ok($statement3);
+ $result = $statement->execute();
+ $result3 = $statement3->execute();
+ $view = "species";
+} else {
+ unset($_SESSION['species']);
+ unset($_SESSION['date']);
+ $view = "choose";
+}
+
+if (get_included_files()[0] === __FILE__) {
+ echo '
+
+
+
+
+ ';
+}
+
+?>
+
+
+
+
+
+
+
+
+
";
+}
+
+#Specific Species
+if(isset($_GET['species'])){ ?>
+
+prepare("SELECT * FROM detections where Com_Name == \"$name\" AND Date == \"$date\" ORDER BY Confidence DESC");
+ } else {
+ $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" AND Date == \"$date\" ORDER BY Time DESC");
+ }
+} else {
+ if(isset($_GET['sort']) && $_GET['sort'] == "confidence") {
+ $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" ORDER BY Confidence DESC");
+ } else {
+ $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" ORDER BY Date DESC, Time DESC");
+ }
+}
+ensure_db_ok($statement2);
+$result2 = $statement2->execute();
+$num_rows = 0;
+while ($result2->fetchArray(SQLITE3_ASSOC)) {
+ $num_rows++;
+}
+$result2->reset(); // reset the pointer to the beginning of the result set
+echo "
+
+ $name
+ ";
+ $iter=0;
+ while($results=$result2->fetchArray(SQLITE3_ASSOC))
+ {
+ $comname = preg_replace('/ /', '_', $results['Com_Name']);
+ $comname = preg_replace('/\'/', '', $comname);
+ $date = $results['Date'];
+ $filename = "/By_Date/".$date."/".$comname."/".$results['File_Name'];
+ $filename_shifted = "/By_Date/shifted/".$date."/".$comname."/".$results['File_Name'];
+ $filename_png = $filename . ".png";
+ $sciname = preg_replace('/ /', '_', $results['Sci_Name']);
+ $sci_name = $results['Sci_Name'];
+ $time = $results['Time'];
+ $confidence = round((float)round($results['Confidence'],2) * 100 ) . '%';
+ $filename_formatted = $date."/".$comname."/".$results['File_Name'];
+
+ // file was deleted by disk check, no need to show the detection in recordings
+ if(!file_exists($home."/BirdSongs/Extracted/".$filename)) {
+ continue;
+ }
+ if(!in_array($filename_formatted, $disk_check_exclude_arr) && isset($_GET['only_excluded'])) {
+ continue;
+ }
+ $iter++;
+
+ if($num_rows < 100){
+ $imageelem = " ";
+ } else {
+ $imageelem = " ";
+ }
+
+ if($config["FULL_DISK"] == "purge") {
+ if(!in_array($filename_formatted, $disk_check_exclude_arr)) {
+ $imageicon = "images/unlock.svg";
+ $title = "This file will be deleted when disk space needs to be freed (>95% usage).";
+ $type = "add";
+ } else {
+ $imageicon = "images/lock.svg";
+ $title = "This file is excluded from being purged.";
+ $type = "del";
+ }
+
+ if(file_exists($shifted_path.$filename_formatted)) {
+ $shiftImageIcon = "images/unshift.svg";
+ $shiftTitle = "This file has been shifted down in frequency.";
+ $shiftAction = "unshift";
+ $filename = $filename_shifted;
+ } else {
+ $shiftImageIcon = "images/shift.svg";
+ $shiftTitle = "This file is not shifted in frequency.";
+ $shiftAction = "shift";
+ }
+
+ echo "
+
+
+
+
+
+ $date $time $confidence
+
+ ".$imageelem."
+
+ ";
+ } else {
+ echo "
+ $date $time $confidence
+
+ ".$imageelem."
+
+ ";
+ }
+
+ }if($iter == 0){ echo "No recordings were found. They may have been deleted to make space for new recordings. You can prevent this from happening in the future by clicking the icon in the top right of a recording. You can also modify this behavior globally under \"Full Disk Behavior\" here. ";}echo "
";}
+
+ if(isset($_GET['filename'])){
+ $name = $_GET['filename'];
+ $statement2 = $db->prepare("SELECT * FROM detections where File_name == \"$name\" ORDER BY Date DESC, Time DESC");
+ ensure_db_ok($statement2);
+ $result2 = $statement2->execute();
+ echo "
+
+ $name
+ ";
+ while($results=$result2->fetchArray(SQLITE3_ASSOC))
+ {
+ $comname = preg_replace('/ /', '_', $results['Com_Name']);
+ $comname = preg_replace('/\'/', '', $comname);
+ $date = $results['Date'];
+ $filename = "/By_Date/".$date."/".$comname."/".$results['File_Name'];
+ $filename_shifted = "/By_Date/shifted/".$date."/".$comname."/".$results['File_Name'];
+ $filename_png = $filename . ".png";
+ $sciname = preg_replace('/ /', '_', $results['Sci_Name']);
+ $sci_name = $results['Sci_Name'];
+ $time = $results['Time'];
+ $confidence = round((float)round($results['Confidence'],2) * 100 ) . '%';
+ $filename_formatted = $date."/".$comname."/".$results['File_Name'];
+
+ // add disk_check_exclude.txt lines into an array for grepping
+ $fp = @fopen($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", 'r');
+ if ($fp) {
+ $disk_check_exclude_arr = explode("\n", fread($fp, filesize($home."/BirdNET-Pi/scripts/disk_check_exclude.txt")));
+ } else {
+ $disk_check_exclude_arr = [];
+ }
+
+ if($config["FULL_DISK"] == "purge") {
+ if(!in_array($filename_formatted, $disk_check_exclude_arr)) {
+ $imageicon = "images/unlock.svg";
+ $title = "This file will be deleted when disk space needs to be freed (>95% usage).";
+ $type = "add";
+ } else {
+ $imageicon = "images/lock.svg";
+ $title = "This file is excluded from being purged.";
+ $type = "del";
+ }
+
+ if(file_exists($shifted_path.$filename_formatted)) {
+ $shiftImageIcon = "images/unshift.svg";
+ $shiftTitle = "This file has been shifted down in frequency.";
+ $shiftAction = "unshift";
+ $filename = $filename_shifted;
+ } else {
+ $shiftImageIcon = "images/shift.svg";
+ $shiftTitle = "This file is not shifted in frequency.";
+ $shiftAction = "shift";
+ }
+
+ echo "
+
+
+
+
+
+ $date $time $confidence
+
+
+ ";
+ } else {
+ echo "
+ $date $time $confidence
+
+
+ ";
+ }
+
+ }echo "
";}
+ echo "
";
+if (get_included_files()[0] === __FILE__) {
+ echo '';
+}
diff --git a/birdnet-pi/rootfs/helpers/change_id/style.css b/birdnet-pi/rootfs/helpers/change_id/style.css
new file mode 100644
index 000000000..3274e2296
--- /dev/null
+++ b/birdnet-pi/rootfs/helpers/change_id/style.css
@@ -0,0 +1,894 @@
+@font-face {
+ font-family: 'Roboto Flex' ;
+ src: url('static/RobotoFlex-Regular.ttf') format('truetype');
+}
+
+* {
+ font-family: 'Roboto Flex', sans-serif;
+ box-sizing: border-box;
+ font-size: medium;
+}
+
+a {
+ text-decoration: none;
+}
+
+a:hover {
+ font-weight: bold;
+}
+
+h3 {
+ text-align: center;
+ margin-bottom: 12px;
+}
+
+iframe {
+ border: none;
+ height: 85%;
+ width: 100%;
+ position: fixed;
+}
+
+body {
+ margin: 0;
+ background-color: rgb(119, 196, 135);
+}
+
+table {
+ background-color: transparent;
+ border-collapse: collapse;
+ border-spacing: 0;
+ margin-left: auto;
+ margin-right: auto;
+ box-shadow: 0px 0px 17px 1px rgba(0, 0, 0, 0.10);
+ border-radius:3px;
+ overflow: hidden;
+}
+
+td {
+ padding: 10px;
+ vertical-align: top;
+ background-color: rgb(219, 255, 235);
+ font-weight: lighter;
+ text-align: center;
+}
+
+th {
+ padding: 12px;
+ font-weight: bold;
+ height: auto;
+ text-align: center;
+}
+
+audio, video{
+ max-height: 100%;
+ max-width: 100%;
+}
+
+label {
+ font-weight: bold;
+}
+
+hr {
+ border-color:black;
+}
+
+button {
+ background-color: transparent;
+ border: none;
+ color: black;
+ cursor: pointer;
+ transition:background-color 0.2s;
+}
+
+.disabled{
+ cursor: not-allowed;
+ pointer-events: none;
+ opacity:0.5;
+}
+
+button:hover {
+ color: blue;
+}
+
+.row {
+ display: flex;
+}
+
+.centered {
+ text-align: center;
+ display: block;
+ width: auto;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.banner {
+ height: 7%;
+ text-align: center;
+}
+
+.banner h1 {
+ padding: .5em;
+ letter-spacing: 5px;
+}
+
+.banner audio,.banner form {
+ float: right;
+ width: 120px;
+ margin-left: -120px;
+ margin-right: auto;
+}
+
+.banner button {
+ padding-top: 8px;
+ font-weight: bold;
+ letter-spacing: 1px;
+}
+
+.banner a {
+ text-decoration: none;
+ color: black;
+ font-size: x-large;
+}
+
+.logo img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ padding: 10px;
+}
+
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0,0,0,0.4);
+}
+
+.modal-content {
+ background-color: #fefefe;
+ margin: 15% auto;
+ padding: 20px;
+ border: 1px solid #888;
+ width: 80%;
+ text-align: center; /* Center the content */
+}
+
+@media (max-width: 768px) {
+ .modal-content {
+ width: 95%;
+ margin: 10% auto;
+ }
+ #labelDropdown {
+ max-width: 100%;
+ overflow-x: auto;
+ }
+}
+
+.topnav {
+ background-color: rgb(159, 226, 155);
+ display: flex;
+ flex: 65%;
+ width: 65%;
+ min-width: min-content;
+ justify-content: space-between;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 15px;
+ box-shadow: 0px 0px 28px 1px rgba(0, 0, 0, 0.10) !important;
+ border-radius: 4px;
+}
+
+.topimage {
+ width:175px;
+ display:initial !important;
+}
+
+.topnav form {
+ margin: 0;
+ padding: 0;
+}
+
+.topnav button {
+ background-color: transparent;
+ text-align: center;
+ padding: 14px 16px;
+ width: auto;
+ vertical-align: middle;
+}
+
+.topnav button:hover {
+ background-color: rgb(219, 255, 235);
+ color: black;
+}
+
+.topnav button.active {
+ background-color: #04AA6D;
+ color: white;
+}
+
+.topnav .button-hover {
+ background-color: rgb(219, 255, 235) !important;
+ color: black;
+}
+
+.topnav .icon {
+ display: none;
+}
+
+.overview th {
+ background-color: rgb(219, 255, 235);
+ text-align: center;
+ padding: 12px;
+}
+
+.overview td {
+ vertical-align: middle;
+}
+
+.overview div img {
+ max-height: 100%;
+ display: flex;
+ justify-content: center;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+.overview .chart {
+ margin-top: 10px;
+}
+.overview-stats {
+ display: flex;
+ justify-content: center;
+}
+
+.left-column {
+ flex: 10%;
+ padding-left: 10px;
+}
+
+.right-column {
+ flex: 90%;
+ margin-right: 10%;
+}
+
+.stats td {
+ vertical-align: middle;
+}
+
+.stats table {
+ height: auto;
+}
+
+.stats button:hover {
+ color: blue;
+}
+
+.overview button, .center button{
+ font-weight: bold;
+ color: blue;
+}
+
+.history table,.history img {
+ width: auto;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.views {
+ transition: opacity 0.3s;
+ -webkit-transition: opacity 0.3s;
+}
+
+.views .centered button {
+ background-color: rgb(219, 255, 235);
+ padding: 12px;
+ transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
+}
+
+.views .centered button:hover {
+ background-color: rgb(159, 226, 155);
+ color: black;
+ box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
+}
+
+.settings {
+ padding: 12px;
+}
+
+.settings h2 {
+ margin-top: 0px;
+}
+
+.settings p {
+ margin-bottom: 0px;
+}
+
+.settings h3 {
+ text-align: left;
+}
+
+.settings button {
+ background-color: rgb(219, 255, 235);
+ padding: 12px;
+}
+
+.settings button:hover {
+ background-color: rgb(159, 226, 155);
+ color: black;
+}
+
+.float button {
+ margin-top: 6px;
+ margin-bottom: 6px;
+}
+
+.customlabels,.customlabels2 {
+ float:left;
+}
+
+.customlabels table {
+ height: 100%;
+}
+
+.customlabels td,.customlabels2 td {
+ border:none;
+ background-color: transparent;
+ vertical-align: middle;
+}
+
+.customlabels button,.customlabels2 button {
+ padding: 12px;
+ background-color: rgb(219, 255, 235);
+
+ width: 100%;
+}
+
+.column1, .column3 {
+ width: 45%;
+}
+
+.smaller {
+ width: 100%;
+ display: none;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.column2 {
+ text-align: center;
+ width: 10%;
+ height: 80%;
+}
+
+.column4 {
+ text-align: justify;
+ width: 10%;
+ height: 50%;
+}
+
+.column1 form,.column3 form {
+ width: 80%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.column1 select,.column3 select {
+ height: 80%;
+ width: 100%;
+}
+
+.spectrogram {
+ width:50%
+}
+
+.full {
+ width:100%;
+}
+
+.logbutton, .navbuttons {
+ float: left;
+}
+
+.systemcontrols form,.servicecontrols form {
+ /*text-align: center;*/
+}
+
+.servicecontrols button {
+ background-color: rgb(219, 255, 235);
+ padding: 12px;
+ width: 50%;
+}
+
+.systemcontrols button {
+ background-color: rgb(219, 255, 235);
+ display: block;
+ padding: 12px;
+ width: 50%;
+ margin: 16px auto;
+}
+
+.servicecontrols button {
+ width: 20%;
+}
+
+.btn-group-center {
+ text-align:center;
+ /*align-content: center;*/
+ margin: 16px auto;
+ position:relative;
+ /*display:inline-block;*/
+}
+
+.slider {
+ -webkit-appearance: none;
+ width: 33%;
+ height: 15px;
+ border-radius: 5px;
+ outline: none;
+ opacity: 0.7;
+ -webkit-transition: .2s;
+ transition: opacity .2s;
+}
+
+.slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 25px;
+ height: 25px;
+ border-radius: 50%;
+ background: #04AA6D;
+ cursor: pointer;
+}
+
+.slider::-moz-range-thumb {
+ width: 25px;
+ height: 25px;
+ border-radius: 50%;
+ background: #04AA6D;
+ cursor: pointer;
+}
+
+#body::-webkit-scrollbar {
+ /*display:none*/
+}
+
+@media screen and (max-width: 1290px) {
+ .column1,.column2,.column3,.column4 {
+ height: 90%
+ }
+ .left-column {
+ display: none;
+ }
+ .right-column {
+ flex: 100%;
+ margin: 0;
+ }
+ img {
+ max-width: 100%;
+ }
+ .overview {
+ overflow-x: hidden;
+ }
+ .overview .right-column .chart img {
+ margin-left: 5%;
+ margin-right: auto;
+ margin-top: 10px;
+ }
+}
+@media screen and (max-width: 1000px) {
+ .customlabels form,.customlabels2 form {
+ width: 95%;
+ }
+ .column1, .column3 {
+ width: 50%;
+ height: 100%;
+ }
+ .column1 select,.column3 select {
+ height: 70%;
+ }
+ .column2,.column4 {
+ display: none;
+ }
+ .smaller{
+ display: block;
+ }
+ .systemcontrols button,.servicecontrols button {
+ width: 60%;
+ padding: 12px;
+ background-color: rgb(219, 255, 235);
+ }
+ .topnav {
+ flex: 100%;
+ width: 100%;
+ }
+ .topnav button {display: none;}
+ .topnav button.icon {
+ padding: 0;
+ margin: 0;
+ display: block;
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ .banner {
+ height: auto;
+ }
+ .banner img {
+ display: none;
+ }
+ .logo img {
+ display: block;
+ width: 60px;
+ height: 60px;
+ }
+ .topnav.responsive {position: relative;}
+ .topnav.responsive button {
+ display: block;
+ text-align: center;
+ }
+}
+
+@media screen and (max-width: 800px) {
+ .column1, .column3 {
+ width: 100%;
+ }
+ .systemcontrols button,.servicecontrols button {
+ width: 80%;
+ padding: 12px;
+ background-color: rgb(219, 255, 235);
+ }
+ .stats img {
+ width: 100%;
+ margin-left:auto;
+ margin-right:auto;
+ }
+ .overview img {
+ width: 100%
+ }
+ .banner {
+ height: auto;
+ margin-left: 60px;
+ }
+ .banner img {
+ display: none;
+ }
+ .stream {
+ float: right;
+ display: block;
+ width: 100px;
+ }
+ .logo img {
+ display: block;
+ width: 60px;
+ height: 60px;
+ }
+ .play table,.overview table,.stats table {
+ width: 100%;
+ }
+ .topnav {
+ flex: 100%;
+ width: 100%;
+ flex-direction: column;
+ }
+ .topnav button {
+ font-size: large;
+ width: 100%
+ }
+ .topnav button {display: none;}
+ .topnav button.icon {
+ margin: 0;
+ padding: 0;
+ display: block;
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ .topnav.responsive {position: relative;}
+ .topnav.responsive button {
+ display: block;
+ }
+ .left-column {
+ display: none;
+ }
+ .left {
+ display:none;
+ }
+}
+
+.copyimage {
+ position:absolute;
+ top:7px;
+ right:7px;
+ width:25px !important;
+ height:25px !important;
+}
+
+.copyimage-mobile {
+ width: 16px !important;
+ height: 16px !important;
+}
+
+.relative {
+ position:relative;
+}
+
+.sortbutton {
+ margin-top:10px;
+ font-size:x-large;
+ background:#dbffeb;
+ padding:5px;
+ box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.10);
+}
+
+button.legacyview {
+ display: none;
+ color:gray;
+ margin:5px;
+ float:right;
+ z-index:100;
+ position:relative;
+ font-size:small;
+ background:#dbffeb;
+ padding:5px;
+ transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
+}
+
+button.legacyview:hover {
+ box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
+}
+
+button.loadmore {
+ margin-top:10px;
+ font-size:x-large;
+ background:#dbffeb;
+ padding:10px;
+ transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
+
+}
+
+button.loadmore:hover {
+ box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
+}
+
+#searchterm {
+ transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+#searchterm:hover {
+ box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
+}
+
+tr {
+ background-color:#9fe29b;
+}
+
+.history.centered form {
+ display:flex;
+ justify-content: center;
+}
+
+.history.centered input {
+ margin-right:5px;
+ border:0px;
+}
+
+.centered form#views button {
+ box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.10);
+ margin:2px;
+ transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
+}
+
+.centered form#views button:hover {
+ box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
+}
+
+dl {
+ margin: 1em 0 0 1em;
+}
+dt {
+ float: left;
+ clear: left;
+ width: auto;
+ text-align: left;
+ font-weight: bold;
+ color: black;
+}
+dd::before {
+ content: ": ";
+}
+
+input {
+ box-shadow: 0px 0px 17px 1px rgba(0, 0, 0, 0.10);
+}
+
+dialog {
+ border:none;
+}
+
+dialog::backdrop {
+ background: repeating-linear-gradient(
+ 30deg,
+ rgba(24, 194, 236, 0.2),
+ rgba(24, 194, 236, 0.2) 1px,
+ rgba(24, 194, 236, 0.3) 1px,
+ rgba(24, 194, 236, 0.3) 20px
+ );
+ backdrop-filter: blur(1px)
+}
+
+.centered_image_container {
+ font-size:19px !important;
+ display:inline-block;
+ position:relative;
+ margin-bottom:3px;
+}
+
+.centered_image_container img.img1 {
+ transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
+ cursor:pointer;
+ height:95%;
+ position:absolute;
+ right:110%;
+ top:0px;
+ border-radius: 5px;
+ width:unset;
+}
+
+.centered_image_container img.img1:hover{
+ opacity:0.8;
+ box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
+}
+
+#birdimage {
+ transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
+ cursor:pointer;
+ border-radius: 5px;
+}
+
+#birdimage:hover {
+ opacity:0.8;
+ box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%);
+}
+
+.centered_image_container * {
+ font-size:19px !important;
+}
+
+.centered_image_container form {
+ margin-bottom:0px;
+}
+
+.brbanner {
+ padding:15px;
+ background-color:rgb(159, 226, 155);
+ text-align:center;
+ font-size:large;
+}
+
+#gain.centered {
+ margin-bottom:10px;
+}
+
+.updatenumber {
+ margin-left:5px;
+ position:absolute;
+ display:inline-block;
+ background-color:#c8191a;
+ color:white;
+ width:20px;
+ line-height:20px;
+ border-radius:12px;
+ text-align:center;
+ font-size:small;
+}
+
+form#views button .updatenumber {
+ position:initial;
+ margin-left:0px;
+}
+
+#detections_table_overview table {
+ width:944px;
+}
+
+#recent_detection_middle_td{
+ width:33%;
+}
+@media screen and (max-width:500px) {
+ #recent_detection_middle_td{
+ width:66%;
+ }
+}
+#recent_detection_middle_td img{
+ width:unset !important;
+ height:75px;
+ float:left;
+}
+
+.settingstable {
+ margin-left:unset;
+ margin-right:unset;
+}
+.settingstable td {
+ text-align:unset;
+}
+.settingstable textarea {
+ width:100%;
+ margin-top:10px;
+}
+.settingstable h2 {
+ font-size:x-large;
+}
+.plaintable {
+ box-shadow: unset;
+}
+.plaintable td {
+ padding: unset;
+}
+
+.brbanner h1 {
+ margin:0px;
+ font-size: xx-large;
+}
+
+.testbtn {
+ background:#77c487 !important;
+}
+
+pre.bash {
+ background-color: black;
+ color: white;
+ font-size: medium ;
+ font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
+ width: 100%;
+ display: inline-block;
+}
+pre#timer.bash {
+ display:unset;
+ width:unset;
+}
+
+#toolsbtn {
+ min-width: max-content;
+}
+
+#showpassword {
+ cursor:pointer;
+ margin-left:2px;
+ height:5px;
+ line-height:5px;
+ padding:3px;
+ background-color:#9fe29b
+}
+
+#newrtspstream{
+ cursor: pointer;
+ margin-left: 2px;
+ height: 5px;
+ line-height: 5px;
+ padding: 3px;
+ background-color: #9fe29b;
+}
+
+.exclude_species_list_option_highlight {
+ color: black;
+ background-color: rgb(119, 196, 135);
+ font-weight: bolder;
+}
+
+#ddnewline::before {
+ content: none;
+}
diff --git a/birdnet-pi/rootfs/helpers/convert_list.php b/birdnet-pi/rootfs/helpers/convert_list/convert_list.php
similarity index 56%
rename from birdnet-pi/rootfs/helpers/convert_list.php
rename to birdnet-pi/rootfs/helpers/convert_list/convert_list.php
index 8ab072634..6e4bee985 100644
--- a/birdnet-pi/rootfs/helpers/convert_list.php
+++ b/birdnet-pi/rootfs/helpers/convert_list/convert_list.php
@@ -2,27 +2,33 @@
+This tool will allow to convert on-the-fly species to compensate for model errors. It SHOULD NOT BE USED except if you know what you are doing, instead the model errors should be reported to the owner. However, it is still convenient for systematic biases that are confirmed through careful listening of samples, while waiting for the models to be updated.
+