This commit is contained in:
Alexandre
2024-05-18 17:36:56 +02:00
parent ffc0d4f2f0
commit b8722babdd
10 changed files with 2003 additions and 31 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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/\ <button type=\"submit\" name=\"view\" value=\"Converted\" form=\"views\">Convert Species List</button>' "$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 " "

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 341.874 341.874" xml:space="preserve">
<path d="M341.338,56.544c-0.865-1.748-2.654-2.834-4.669-2.834c-0.243,0-0.471,0.016-0.662,0.035l-13.201,0.573l10.858-10.302
l0.248-0.132l2.895-3.426l-1.536-3.02c-0.887-1.743-2.67-2.826-4.654-2.826c-0.489,0-0.928,0.066-1.289,0.15l-35.572,6.485
c-0.648-0.844-1.308-1.672-1.984-2.479c-12.529-14.959-29.409-22.865-48.814-22.865c-4.852,0-9.917,0.497-15.056,1.476
c-36.111,6.882-53.736,34.2-72.396,63.122c-24.924,38.632-50.697,78.579-124.683,78.579c-6.337,0-13.028-0.301-19.886-0.896
c-0.695-0.061-1.358-0.091-1.97-0.091c-3.851,0-6.544,1.251-8.006,3.717c-2.208,3.727-0.062,7.647,0.969,9.531l0.142,0.261
c13.907,25.674,34.957,47.705,60.9,63.712c23.211,14.321,49.99,23.099,76.99,25.806v52.677c0,6.747,5.517,12.171,12.265,12.171
h28.832c0.799,0,1.593-0.092,2.373-0.243c0.783,0.158,1.593,0.243,2.422,0.243h28.832c3.66,0,7.256-1.609,9.62-4.359
c2.116-2.461,3.024-5.496,2.556-8.58c-1.294-8.509-12.61-11.532-22.768-11.532c-2.314,0-6.642,0.184-11.032,1.307l3.213-44.469
c3.401-0.65,6.804-1.365,10.205-2.186c46.987-11.342,72.971-42.049,86.494-65.814c16.654-29.266,23.972-64.827,20.076-97.568
c-0.326-2.739-0.727-5.427-1.202-8.063l27.382-21.343c1.025-0.608,1.824-1.513,2.262-2.59
C342.051,59.4,341.991,57.852,341.338,56.544z M173.964,301.607c-1-0.067-2.282-0.101-3.326-0.101c-2.314,0-6.727,0.18-11.117,1.303
l3.079-40.844c3.728-0.08,7.365-0.271,11.365-0.568V301.607z M137.724,201.387c-15.404,10.814-31.967,11.775-41.318-4.436
c-10.372,3.407-21.528,2.202-26.284-7.327c-1.491-2.988,0.775-5.469,2.189-5.541c52.375-2.654,99.886-43.521,118.922-86.605
c1.398-3.165,5.691-3.562,6.524-0.52C212.89,152.204,194.946,219.858,137.724,201.387z M242.354,87.651
c-9.213,0-16.682-7.469-16.682-16.682s7.469-16.682,16.682-16.682c9.213,0,16.682,7.469,16.682,16.682
S251.567,87.651,242.354,87.651z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,705 @@
<?php
/* Prevent XSS input */
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
error_reporting(E_ERROR);
ini_set('display_errors',1);
require_once 'scripts/common.php';
$home = get_home();
$config = get_config();
$db = new SQLite3('./scripts/birds.db', SQLITE3_OPEN_READONLY);
$db->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) . "<br>";
}
$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) . "<br>";
}
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 '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>';
}
?>
<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();
xhttp.onload = function() {
if(this.responseText == "OK"){
if(copylink == true) {
window.top.close();
} else {
location.reload();
}
} else {
alert(this.responseText);
}
}
xhttp.open("GET", "play.php?deletefile="+filename, true);
xhttp.send();
}
}
function toggleLock(filename, type, elem) {
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
if(this.responseText == "OK"){
if(type == "add") {
elem.setAttribute("src","images/lock.svg");
elem.setAttribute("title", "This file is excluded from being purged.");
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("add","del"));
} else {
elem.setAttribute("src","images/unlock.svg");
elem.setAttribute("title", "This file will be deleted when disk space needs to be freed.");
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("del","add"));
}
}
}
if(type == "add") {
xhttp.open("GET", "play.php?excludefile="+filename+"&exclude_add=true", true);
} else {
xhttp.open("GET", "play.php?excludefile="+filename+"&exclude_del=true", true);
}
xhttp.send();
elem.setAttribute("src","images/spinner.gif");
}
function toggleShiftFreq(filename, shiftAction, elem) {
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
if(this.responseText == "OK"){
if(shiftAction == "shift") {
elem.setAttribute("src","images/unshift.svg");
elem.setAttribute("title", "This file has been shifted down in frequency.");
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("shift","unshift"));
console.log("shifted freqs of " + filename);
video=elem.parentNode.getElementsByTagName("video");
if (video.length > 0) {
video[0].setAttribute("title", video[0].getAttribute("title").replace("/By_Date/","/By_Date/shifted/"));
source = video[0].getElementsByTagName("source")[0];
source.setAttribute("src", source.getAttribute("src").replace("/By_Date/","/By_Date/shifted/"));
video[0].load();
} else {
atag=elem.parentNode.getElementsByTagName("a")[0];
atag.setAttribute("href", atag.getAttribute("href").replace("/By_Date/","/By_Date/shifted/"));
}
} else {
elem.setAttribute("src","images/shift.svg");
elem.setAttribute("title", "This file is not shifted in frequency.");
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("unshift","shift"));
console.log("unshifted freqs of " + filename);
video=elem.parentNode.getElementsByTagName("video");
if (video.length > 0) {
video[0].setAttribute("title", video[0].getAttribute("title").replace("/By_Date/shifted/","/By_Date/"));
source = video[0].getElementsByTagName("source")[0];
source.setAttribute("src", source.getAttribute("src").replace("/By_Date/shifted/","/By_Date/"));
video[0].load();
} else {
atag=elem.parentNode.getElementsByTagName("a")[0];
atag.setAttribute("href", atag.getAttribute("href").replace("/By_Date/shifted/","/By_Date/"));
}
}
}
}
if(shiftAction == "shift") {
console.log("shifting freqs of " + filename);
xhttp.open("GET", "play.php?shiftfile="+filename+"&doshift=true", true);
} else {
console.log("unshifting freqs of " + filename);
xhttp.open("GET", "play.php?shiftfile="+filename, true);
}
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..."><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);
}
});
});
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) {
window.top.close();
} else {
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
#If no specific species
if(!isset($_GET['species']) && !isset($_GET['filename'])){
?>
<div class="play">
<?php if($view == "byspecies" || $view == "date") { ?>
<div style="width: auto;
text-align: center">
<form action="views.php" method="GET">
<input type="hidden" name="view" value="Recordings">
<input type="hidden" name="<?php echo $view; ?>" value="<?php echo $_GET['date']; ?>">
<button <?php if(!isset($_GET['sort']) || $_GET['sort'] == "alphabetical"){ echo "style='background:#9fe29b !important;'"; }?> class="sortbutton" type="submit" name="sort" value="alphabetical">
<img src="images/sort_abc.svg" title="Sort by alphabetical" alt="Sort by alphabetical">
</button>
<button <?php if(isset($_GET['sort']) && $_GET['sort'] == "occurrences"){ echo "style='background:#9fe29b !important;'"; }?> class="sortbutton" type="submit" name="sort" value="occurrences">
<img src="images/sort_occ.svg" title="Sort by occurrences" alt="Sort by occurrences">
</button>
</form>
</div>
<br>
<?php } ?>
<form action="views.php" method="GET">
<input type="hidden" name="view" value="Recordings">
<table>
<?php
#By Date
if($view == "bydate") {
while($results=$result->fetchArray(SQLITE3_ASSOC)){
$date = $results['Date'];
if(realpath($home."/BirdSongs/Extracted/By_Date/".$date) !== false){
echo "<td>
<button action=\"submit\" name=\"date\" value=\"$date\">".($date == date('Y-m-d') ? "Today" : $date)."</button></td></tr>";}}
#By Species
} elseif($view == "byspecies") {
$birds = array();
while($results=$result->fetchArray(SQLITE3_ASSOC))
{
$name = $results['Com_Name'];
$birds[] = $name;
}
if(count($birds) > 45) {
$num_cols = 3;
} else {
$num_cols = 1;
}
$num_rows = ceil(count($birds) / $num_cols);
for ($row = 0; $row < $num_rows; $row++) {
echo "<tr>";
for ($col = 0; $col < $num_cols; $col++) {
$index = $row + $col * $num_rows;
if ($index < count($birds)) {
?>
<td class="spec">
<button type="submit" name="species" value="<?php echo $birds[$index];?>"><?php echo $birds[$index];?></button>
</td>
<?php
} else {
echo "<td></td>";
}
}
echo "</tr>";
}
} elseif($view == "date") {
$birds = array();
while($results=$result->fetchArray(SQLITE3_ASSOC))
{
$name = $results['Com_Name'];
$dir_name = str_replace("'", '', $name);
if(realpath($home."/BirdSongs/Extracted/By_Date/".$date."/".str_replace(" ", "_", $dir_name)) !== false){
$birds[] = $name;
}
}
if(count($birds) > 45) {
$num_cols = 3;
} else {
$num_cols = 1;
}
$num_rows = ceil(count($birds) / $num_cols);
for ($row = 0; $row < $num_rows; $row++) {
echo "<tr>";
for ($col = 0; $col < $num_cols; $col++) {
$index = $row + $col * $num_rows;
if ($index < count($birds)) {
?>
<td class="spec">
<button type="submit" name="species" value="<?php echo $birds[$index];?>"><?php echo $birds[$index];?></button>
</td>
<?php
} else {
echo "<td></td>";
}
}
echo "</tr>";
}
#Choose
} else {
echo "<td>
<button action=\"submit\" name=\"byspecies\" value=\"byspecies\">By Species</button></td></tr>
<tr><td><button action=\"submit\" name=\"bydate\" value=\"bydate\">By Date</button></td>";
}
echo "</table></form>";
}
#Specific Species
if(isset($_GET['species'])){ ?>
<div style="width: auto;
text-align: center">
<form action="views.php" method="GET">
<input type="hidden" name="view" value="Recordings">
<input type="hidden" name="species" value="<?php echo $_GET['species']; ?>">
<input type="hidden" name="sort" value="<?php echo $_GET['sort']; ?>">
<button <?php if(!isset($_GET['sort']) || $_GET['sort'] == "" || $_GET['sort'] == "date"){ echo "style='background:#9fe29b !important;'"; }?> class="sortbutton" type="submit" name="sort" value="date">
<img width=35px src="images/sort_date.svg" title="Sort by date" alt="Sort by date">
</button>
<button <?php if(isset($_GET['sort']) && $_GET['sort'] == "confidence"){ echo "style='background:#9fe29b !important;'"; }?> class="sortbutton" type="submit" name="sort" value="confidence">
<img src="images/sort_occ.svg" title="Sort by confidence" alt="Sort by confidence">
</button><br>
<input style="margin-top:10px" <?php if(isset($_GET['only_excluded'])){ echo "checked"; }?> type="checkbox" name="only_excluded" onChange="submit()">
<label for="onlyverified">Only Show Purge Excluded</label>
</form>
</div>
<?php
// 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 = [];
}
$name = htmlspecialchars_decode($_GET['species'], ENT_QUOTES);
if(isset($_SESSION['date'])) {
$date = $_SESSION['date'];
if(isset($_GET['sort']) && $_GET['sort'] == "confidence") {
$statement2 = $db->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 "<table>
<tr>
<th>$name</th>
</tr>";
$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 = "<video onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster=\"$filename_png\" preload=\"none\" title=\"$filename\"><source src=\"$filename\"></video>";
} else {
$imageelem = "<a href=\"$filename\"><img src=\"$filename_png\"></a>";
}
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 "<tr>
<td class=\"relative\">
<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>
".$imageelem."
</td>
</tr>";
} else {
echo "<tr>
<td class=\"relative\">$date $time<br>$confidence
<img style='cursor:pointer' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\")' class=\"copyimage\" width=25 title='Delete Detection'><br>
".$imageelem."
</td>
</tr>";
}
}if($iter == 0){ echo "<tr><td><b>No recordings were found.</b><br><br><span style='font-size:medium'>They may have been deleted to make space for new recordings. You can prevent this from happening in the future by clicking the <img src='images/unlock.svg' style='width:20px'> icon in the top right of a recording.<br>You can also modify this behavior globally under \"Full Disk Behavior\" <a href='views.php?view=Advanced'>here.</a></span></td></tr>";}echo "</table>";}
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 "<table>
<tr>
<th>$name</th>
</tr>";
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 "<tr>
<td class=\"relative\">
<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>
<video onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster=\"$filename_png\" preload=\"none\" title=\"$filename\"><source src=\"$filename\"></video></td>
</tr>";
} else {
echo "<tr>
<td class=\"relative\">$date $time<br>$confidence
<img style='cursor:pointer' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\", true)' class=\"copyimage\" width=25 title='Delete Detection'><br>
<video onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster=\"$filename_png\" preload=\"none\" title=\"$filename\"><source src=\"$filename\"></video></td>
</tr>";
}
}echo "</table>";}
echo "</div>";
if (get_included_files()[0] === __FILE__) {
echo '</html>';
}

View File

@@ -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;
}

View File

@@ -2,27 +2,33 @@
<style>
</style>
<p><strong>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.</strong></p>
<div class="customlabels column1">
<form action="" method="GET" id="add">
<input type="hidden" id="species" name="species">
<h3>Specie to convert from :</h3>
<!-- Input box to filter options in the first table -->
<input type="text" id="species1Search" onkeyup="filterOptions('species1')" placeholder="Search for species...">
<select name="species1" id="species1" size="25">
<?php
error_reporting(E_ALL);
ini_set('display_errors',1);
$filename = './scripts/labels.txt';
$eachline = file($filename, FILE_IGNORE_NEW_LINES);
foreach($eachline as $lines){echo
foreach($eachline as $lines){echo
"<option value=\"".$lines."\">$lines</option>";}
?>
</select>
<br><br> <!-- Added a space between the two tables -->
<h3>Specie to convert to :</h3>
<!-- Input box to filter options in the second table -->
<input type="text" id="species2Search" onkeyup="filterOptions('species2')" placeholder="Search for species...">
<select name="species2" id="species2" size="25">
<?php
foreach($eachline as $lines){echo
foreach($eachline as $lines){echo
"<option value=\"".$lines."\">$lines</option>";}
?>
</select>
@@ -49,7 +55,7 @@
$filename = './scripts/convert_species_list.txt'; // Changed the file path
$eachline = file($filename, FILE_IGNORE_NEW_LINES);
foreach($eachline as $lines){
echo
echo
"<option value=\"".$lines."\">$lines</option>";
}?>
</select>
@@ -66,14 +72,30 @@
document.getElementById("add").addEventListener("submit", function(event) {
var speciesSelect1 = document.getElementById("species1");
var speciesSelect2 = document.getElementById("species2");
var selectedSpecies1 = speciesSelect1.options[speciesSelect1.selectedIndex].value;
var selectedSpecies2 = speciesSelect2.options[speciesSelect2.selectedIndex].value;
document.getElementById("species").value = selectedSpecies1 + ";" + selectedSpecies2;
if (speciesSelect1.selectedIndex < 1 || speciesSelect2.selectedIndex < 1) {
if (speciesSelect1.selectedIndex < 0 || speciesSelect2.selectedIndex < 0) {
alert("Please select a species from both lists.");
document.querySelector('.views').style.opacity = 1;
event.preventDefault();
} else {
var selectedSpecies1 = speciesSelect1.options[speciesSelect1.selectedIndex].value;
var selectedSpecies2 = speciesSelect2.options[speciesSelect2.selectedIndex].value;
document.getElementById("species").value = selectedSpecies1 + ";" + selectedSpecies2;
}
});
// Function to filter options in a select element
function filterOptions(id) {
var input = document.getElementById(id + "Search");
var filter = input.value.toUpperCase();
var select = document.getElementById(id);
var options = select.getElementsByTagName("option");
for (var i = 0; i < options.length; i++) {
var txtValue = options[i].textContent || options[i].innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
options[i].style.display = "";
} else {
options[i].style.display = "none";
}
}
}
</script>

View File

@@ -0,0 +1,342 @@
import datetime
import logging
import math
import operator
import os
import time
import librosa
import numpy as np
from utils.helpers import get_settings, Detection
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
os.environ['CUDA_VISIBLE_DEVICES'] = ''
try:
import tflite_runtime.interpreter as tflite
except BaseException:
from tensorflow import lite as tflite
log = logging.getLogger(__name__)
userDir = os.path.expanduser('~')
INTERPRETER, M_INTERPRETER, INCLUDE_LIST, EXCLUDE_LIST, CONVERT_LIST = (None, None, None, None, None)
PREDICTED_SPECIES_LIST = []
model, priv_thresh, sf_thresh = (None, None, None)
mdata, mdata_params = (None, None)
def loadModel():
global INPUT_LAYER_INDEX
global OUTPUT_LAYER_INDEX
global MDATA_INPUT_INDEX
global CLASSES
log.info('LOADING TF LITE MODEL...')
# Load TFLite model and allocate tensors.
# model will either be BirdNET_GLOBAL_6K_V2.4_Model_FP16 (new) or BirdNET_6K_GLOBAL_MODEL (old)
modelpath = userDir + '/BirdNET-Pi/model/'+model+'.tflite'
myinterpreter = tflite.Interpreter(model_path=modelpath, num_threads=2)
myinterpreter.allocate_tensors()
# Get input and output tensors.
input_details = myinterpreter.get_input_details()
output_details = myinterpreter.get_output_details()
# Get input tensor index
INPUT_LAYER_INDEX = input_details[0]['index']
if model == "BirdNET_6K_GLOBAL_MODEL":
MDATA_INPUT_INDEX = input_details[1]['index']
OUTPUT_LAYER_INDEX = output_details[0]['index']
# Load labels
CLASSES = []
labelspath = userDir + '/BirdNET-Pi/model/labels.txt'
with open(labelspath, 'r') as lfile:
for line in lfile.readlines():
CLASSES.append(line.replace('\n', ''))
log.info('LOADING DONE!')
return myinterpreter
def loadMetaModel():
global M_INTERPRETER
global M_INPUT_LAYER_INDEX
global M_OUTPUT_LAYER_INDEX
if get_settings().getint('DATA_MODEL_VERSION') == 2:
data_model = 'BirdNET_GLOBAL_6K_V2.4_MData_Model_V2_FP16.tflite'
else:
data_model = 'BirdNET_GLOBAL_6K_V2.4_MData_Model_FP16.tflite'
# Load TFLite model and allocate tensors.
M_INTERPRETER = tflite.Interpreter(model_path=os.path.join(userDir, 'BirdNET-Pi/model', data_model))
M_INTERPRETER.allocate_tensors()
# Get input and output tensors.
input_details = M_INTERPRETER.get_input_details()
output_details = M_INTERPRETER.get_output_details()
# Get input tensor index
M_INPUT_LAYER_INDEX = input_details[0]['index']
M_OUTPUT_LAYER_INDEX = output_details[0]['index']
log.info("loaded META model")
def predictFilter(lat, lon, week):
global M_INTERPRETER
# Does interpreter exist?
if M_INTERPRETER is None:
loadMetaModel()
# Prepare mdata as sample
sample = np.expand_dims(np.array([lat, lon, week], dtype='float32'), 0)
# Run inference
M_INTERPRETER.set_tensor(M_INPUT_LAYER_INDEX, sample)
M_INTERPRETER.invoke()
return M_INTERPRETER.get_tensor(M_OUTPUT_LAYER_INDEX)[0]
def explore(lat, lon, week):
# Make filter prediction
l_filter = predictFilter(lat, lon, week)
# Apply threshold
l_filter = np.where(l_filter >= float(sf_thresh), l_filter, 0)
# Zip with labels
l_filter = list(zip(l_filter, CLASSES))
# Sort by filter value
l_filter = sorted(l_filter, key=lambda x: x[0], reverse=True)
return l_filter
def predictSpeciesList(lat, lon, week):
l_filter = explore(lat, lon, week)
for s in l_filter:
if s[0] >= float(sf_thresh):
# if there's a custom user-made include list, we only want to use the species in that
if (len(INCLUDE_LIST) == 0):
PREDICTED_SPECIES_LIST.append(s[1])
def loadCustomSpeciesList(path):
slist = []
if os.path.isfile(path):
with open(path, 'r') as csfile:
for line in csfile.readlines():
slist.append(line.replace('\r', '').replace('\n', ''))
return slist
def splitSignal(sig, rate, overlap, seconds=3.0, minlen=1.5):
# Split signal with overlap
sig_splits = []
for i in range(0, len(sig), int((seconds - overlap) * rate)):
split = sig[i:i + int(seconds * rate)]
# End of signal?
if len(split) < int(minlen * rate):
break
# Signal chunk too short? Fill with zeros.
if len(split) < int(rate * seconds):
temp = np.zeros((int(rate * seconds)))
temp[:len(split)] = split
split = temp
sig_splits.append(split)
return sig_splits
def readAudioData(path, overlap, sample_rate=48000):
log.info('READING AUDIO DATA...')
# Open file with librosa (uses ffmpeg or libav)
sig, rate = librosa.load(path, sr=sample_rate, mono=True, res_type='kaiser_fast')
# Split audio into 3-second chunks
chunks = splitSignal(sig, rate, overlap)
log.info('READING DONE! READ %d CHUNKS.', len(chunks))
return chunks
def convertMetadata(m):
# Convert week to cosine
if m[2] >= 1 and m[2] <= 48:
m[2] = math.cos(math.radians(m[2] * 7.5)) + 1
else:
m[2] = -1
# Add binary mask
mask = np.ones((3,))
if m[0] == -1 or m[1] == -1:
mask = np.zeros((3,))
if m[2] == -1:
mask[2] = 0.0
return np.concatenate([m, mask])
def custom_sigmoid(x, sensitivity=1.0):
return 1 / (1.0 + np.exp(-sensitivity * x))
def predict(sample, sensitivity):
global INTERPRETER
# Make a prediction
INTERPRETER.set_tensor(INPUT_LAYER_INDEX, np.array(sample[0], dtype='float32'))
if model == "BirdNET_6K_GLOBAL_MODEL":
INTERPRETER.set_tensor(MDATA_INPUT_INDEX, np.array(sample[1], dtype='float32'))
INTERPRETER.invoke()
prediction = INTERPRETER.get_tensor(OUTPUT_LAYER_INDEX)[0]
# Apply custom sigmoid
p_sigmoid = custom_sigmoid(prediction, sensitivity)
# Get label and scores for pooled predictions
p_labels = dict(zip(CLASSES, p_sigmoid))
# Sort by score
p_sorted = sorted(p_labels.items(), key=operator.itemgetter(1), reverse=True)
human_cutoff = max(10, int(len(p_sorted) * priv_thresh / 100.0))
log.debug("DATABASE SIZE: %d", len(p_sorted))
log.debug("HUMAN-CUTOFF AT: %d", human_cutoff)
for i in range(min(10, len(p_sorted))):
if p_sorted[i][0] == 'Human_Human':
with open(userDir + '/BirdNET-Pi/HUMAN.txt', 'a') as rfile:
rfile.write(str(datetime.datetime.now()) + str(p_sorted[i]) + ' ' + str(human_cutoff) + '\n')
return p_sorted[:human_cutoff]
def analyzeAudioData(chunks, lat, lon, week, sens, overlap,):
global INTERPRETER
sensitivity = max(0.5, min(1.0 - (sens - 1.0), 1.5))
detections = {}
start = time.time()
log.info('ANALYZING AUDIO...')
if model == "BirdNET_GLOBAL_6K_V2.4_Model_FP16":
if len(PREDICTED_SPECIES_LIST) == 0 or len(INCLUDE_LIST) != 0:
predictSpeciesList(lat, lon, week)
mdata = get_metadata(lat, lon, week)
# Parse every chunk
pred_start = 0.0
for c in chunks:
# Prepare as input signal
sig = np.expand_dims(c, 0)
# Make prediction
p = predict([sig, mdata], sensitivity)
# print("PPPPP",p)
HUMAN_DETECTED = False
# Catch if Human is recognized
for x in range(len(p)):
if "Human" in p[x][0]:
HUMAN_DETECTED = True
# Save result and timestamp
pred_end = pred_start + 3.0
# If human detected set all detections to human to make sure voices are not saved
if HUMAN_DETECTED is True:
p = [('Human_Human', 0.0)] * 10
detections[str(pred_start) + ';' + str(pred_end)] = p
pred_start = pred_end - overlap
log.info('DONE! Time %.2f SECONDS', time.time() - start)
return detections
def get_metadata(lat, lon, week):
global mdata, mdata_params
if mdata_params != [lat, lon, week]:
mdata_params = [lat, lon, week]
# Convert and prepare metadata
mdata = convertMetadata(np.array([lat, lon, week]))
mdata = np.expand_dims(mdata, 0)
return mdata
def load_global_model():
global INTERPRETER
global model, priv_thresh, sf_thresh
conf = get_settings()
model = conf['MODEL']
priv_thresh = conf.getfloat('PRIVACY_THRESHOLD')
sf_thresh = conf.getfloat('SF_THRESH')
INTERPRETER = loadModel()
def run_analysis(file):
global INCLUDE_LIST, EXCLUDE_LIST, CONVERT_LIST, CONVERT_DICT
INCLUDE_LIST = loadCustomSpeciesList(os.path.expanduser("~/BirdNET-Pi/include_species_list.txt"))
EXCLUDE_LIST = loadCustomSpeciesList(os.path.expanduser("~/BirdNET-Pi/exclude_species_list.txt"))
CONVERT_LIST = loadCustomSpeciesList(os.path.expanduser("~/BirdNET-Pi/convert_species_list.txt"))
CONVERT_DICT = {row.split(';')[0]: row.split(';')[1] for row in CONVERT_LIST}
conf = get_settings()
# Read audio data & handle errors
try:
audio_data = readAudioData(file.file_name, conf.getfloat('OVERLAP'))
except (NameError, TypeError) as e:
log.error("Error with the following info: %s", e)
return []
# Process audio data and get detections
raw_detections = analyzeAudioData(audio_data, conf.getfloat('LATITUDE'), conf.getfloat('LONGITUDE'), file.week,
conf.getfloat('SENSITIVITY'), conf.getfloat('OVERLAP'))
confident_detections = []
for time_slot, entries in raw_detections.items():
log.info('%s-%s', time_slot, entries[0])
for entry in entries:
if entry[1] >= conf.getfloat('CONFIDENCE'):
if entry[0] in CONVERT_DICT:
converted_entry = CONVERT_DICT.get(entry[0], entry[0])
else :
converted_entry = entry[0]
if (converted_entry in INCLUDE_LIST or len(INCLUDE_LIST) == 0) and \
(converted_entry not in EXCLUDE_LIST or len(EXCLUDE_LIST) == 0) and \
(converted_entry in PREDICTED_SPECIES_LIST or len(PREDICTED_SPECIES_LIST) == 0):
d = Detection(time_slot.split(';')[0], time_slot.split(';')[1], converted_entry, entry[1])
confident_detections.append(d)
return confident_detections