mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-01-13 11:21:02 +01:00
Update
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 " "
|
||||
|
||||
22
birdnet-pi/rootfs/helpers/change_id/bird.svg
Normal file
22
birdnet-pi/rootfs/helpers/change_id/bird.svg
Normal 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 |
0
birdnet-pi/rootfs/helpers/changeidentification.sh → birdnet-pi/rootfs/helpers/change_id/birdnet_changeidentification.sh
Executable file → Normal file
0
birdnet-pi/rootfs/helpers/changeidentification.sh → birdnet-pi/rootfs/helpers/change_id/birdnet_changeidentification.sh
Executable file → Normal file
705
birdnet-pi/rootfs/helpers/change_id/play.php.txt
Normal file
705
birdnet-pi/rootfs/helpers/change_id/play.php.txt
Normal 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>';
|
||||
}
|
||||
894
birdnet-pi/rootfs/helpers/change_id/style.css
Normal file
894
birdnet-pi/rootfs/helpers/change_id/style.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
342
birdnet-pi/rootfs/helpers/convert_list/server.py
Normal file
342
birdnet-pi/rootfs/helpers/convert_list/server.py
Normal 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
|
||||
Reference in New Issue
Block a user