mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-05-30 20:34:04 +02:00
Update
This commit is contained in:
101
birdnet-pi/rootfs/helpers/convert_list/convert_list.php
Normal file
101
birdnet-pi/rootfs/helpers/convert_list/convert_list.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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
|
||||
"<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
|
||||
"<option value=\"".$lines."\">$lines</option>";}
|
||||
?>
|
||||
</select>
|
||||
<input type="hidden" name="add" value="add">
|
||||
</form>
|
||||
<div class="customlabels smaller">
|
||||
<button type="submit" name="view" value="Converted" form="add">>>ADD>></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="customlabels column2">
|
||||
<table><td>
|
||||
<button type="submit" name="view" value="Converted" form="add">>>ADD>></button>
|
||||
<br><br>
|
||||
<button type="submit" name="view" value="Converted" form="del">REMOVE</button>
|
||||
</td></table>
|
||||
</div>
|
||||
|
||||
<div class="customlabels column3" style="margin-top: 0;"> <!-- Removed the blank space above the table -->
|
||||
<form action="" method="GET" id="del">
|
||||
<h3>Converted Species List</h3>
|
||||
<select name="species[]" id="value2" multiple size="25">
|
||||
<?php
|
||||
$filename = './scripts/convert_species_list.txt'; // Changed the file path
|
||||
$eachline = file($filename, FILE_IGNORE_NEW_LINES);
|
||||
foreach($eachline as $lines){
|
||||
echo
|
||||
"<option value=\"".$lines."\">$lines</option>";
|
||||
}?>
|
||||
</select>
|
||||
<input type="hidden" name="del" value="del">
|
||||
</form>
|
||||
<div class="customlabels smaller">
|
||||
<button type="submit" name="view" value="Converted" form="del">REMOVE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="hiddenSpecies" name="hiddenSpecies">
|
||||
|
||||
<script>
|
||||
document.getElementById("add").addEventListener("submit", function(event) {
|
||||
var speciesSelect1 = document.getElementById("species1");
|
||||
var speciesSelect2 = document.getElementById("species2");
|
||||
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
|
||||
457
birdnet-pi/rootfs/helpers/convert_list/views.php
Normal file
457
birdnet-pi/rootfs/helpers/convert_list/views.php
Normal file
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
|
||||
/* Prevent XSS input */
|
||||
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
|
||||
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
|
||||
|
||||
session_start();
|
||||
|
||||
require_once 'scripts/common.php';
|
||||
$user = get_user();
|
||||
$home = get_home();
|
||||
$config = get_config();
|
||||
set_timezone();
|
||||
|
||||
if(is_authenticated() && (!isset($_SESSION['behind']) || !isset($_SESSION['behind_time']) || time() > $_SESSION['behind_time'] + 86400)) {
|
||||
shell_exec("sudo -u".$user." git -C ".$home."/BirdNET-Pi fetch > /dev/null 2>/dev/null &");
|
||||
$str = trim(shell_exec("sudo -u".$user." git -C ".$home."/BirdNET-Pi status"));
|
||||
if (preg_match("/behind '.*?' by (\d+) commit(s?)\b/", $str, $matches)) {
|
||||
$num_commits_behind = $matches[1];
|
||||
}
|
||||
if (preg_match('/\b(\d+)\b and \b(\d+)\b different commits each/', $str, $matches)) {
|
||||
$num1 = (int) $matches[1];
|
||||
$num2 = (int) $matches[2];
|
||||
$num_commits_behind = $num1 + $num2;
|
||||
}
|
||||
if (stripos($str, "Your branch is up to date") !== false) {
|
||||
$num_commits_behind = '0';
|
||||
}
|
||||
$_SESSION['behind'] = $num_commits_behind;
|
||||
$_SESSION['behind_time'] = time();
|
||||
}
|
||||
if(isset($_SESSION['behind'])&&intval($_SESSION['behind']) >= 99) {?>
|
||||
<style>
|
||||
.updatenumber {
|
||||
width:30px !important;
|
||||
}
|
||||
</style>
|
||||
<?php }
|
||||
if ($config["LATITUDE"] == "0.000" && $config["LONGITUDE"] == "0.000") {
|
||||
echo "<center style='color:red'><b>WARNING: Your latitude and longitude are not set properly. Please do so now in Tools -> Settings.</center></b>";
|
||||
}
|
||||
elseif ($config["LATITUDE"] == "0.000") {
|
||||
echo "<center style='color:red'><b>WARNING: Your latitude is not set properly. Please do so now in Tools -> Settings.</center></b>";
|
||||
}
|
||||
elseif ($config["LONGITUDE"] == "0.000") {
|
||||
echo "<center style='color:red'><b>WARNING: Your longitude is not set properly. Please do so now in Tools -> Settings.</center></b>";
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BirdNET-Pi DB</title>
|
||||
<link rel="stylesheet" href="style.css?v=<?php echo date ('n.d.y', filemtime('style.css')); ?>">
|
||||
</head>
|
||||
<body>
|
||||
<form action="views.php" method="GET" id="views">
|
||||
<div class="topnav" id="myTopnav">
|
||||
<button type="submit" name="view" value="Overview" form="views">Overview</button>
|
||||
<button type="submit" name="view" value="Todays Detections" form="views">Today's Detections</button>
|
||||
<button type="submit" name="view" value="Spectrogram" form="views">Spectrogram</button>
|
||||
<button type="submit" name="view" value="Species Stats" form="views">Best Recordings</button>
|
||||
<button type="submit" name="view" value="Streamlit" form="views">Species Stats</button>
|
||||
<button type="submit" name="view" value="Daily Charts" form="views">Daily Charts</button>
|
||||
<button type="submit" name="view" value="Recordings" form="views">Recordings</button>
|
||||
<button type="submit" name="view" value="View Log" form="views">View Log</button>
|
||||
<button type="submit" name="view" value="Tools" form="views">Tools<?php if(isset($_SESSION['behind']) && intval($_SESSION['behind']) >= 50 && ($config['SILENCE_UPDATE_INDICATOR'] != 1)){ $updatediv = ' <div class="updatenumber">'.$_SESSION["behind"].'</div>'; } else { $updatediv = ""; } echo $updatediv; ?></button>
|
||||
<button type="button" href="javascript:void(0);" class="icon" onclick="myFunction()"><img src="images/menu.png"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
var elements = document.querySelectorAll("button[name=view]");
|
||||
|
||||
var setViewsOpacity = function() {
|
||||
document.getElementsByClassName("views")[0].style.opacity = "0.5";
|
||||
};
|
||||
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
elements[i].addEventListener('click', setViewsOpacity, false);
|
||||
}
|
||||
};
|
||||
var topbuttons = document.querySelectorAll("button[form='views']");
|
||||
if(window.location.search.substr(1) != '') {
|
||||
for (var i = 0; i < topbuttons.length; i++) {
|
||||
if(topbuttons[i].value == decodeURIComponent(window.location.search.substr(1)).replace(/\+/g,' ').split('=').pop()) {
|
||||
topbuttons[i].classList.add("button-hover");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
topbuttons[0].classList.add("button-hover");
|
||||
}
|
||||
function copyOutput(elem) {
|
||||
elem.innerHTML = 'Copied!';
|
||||
const copyText = document.getElementsByTagName("pre")[0].textContent;
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.style.position = 'absolute';
|
||||
textArea.style.left = '-100%';
|
||||
textArea.textContent = copyText;
|
||||
document.body.append(textArea);
|
||||
textArea.select();
|
||||
document.execCommand("copy");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="views">
|
||||
<?php
|
||||
if(isset($_GET['view'])){
|
||||
if($_GET['view'] == "System Info"){echo "<iframe src='phpsysinfo/index.php'></iframe>";}
|
||||
if($_GET['view'] == "System Controls"){
|
||||
ensure_authenticated();
|
||||
include('scripts/system_controls.php');
|
||||
}
|
||||
if($_GET['view'] == "Services"){
|
||||
ensure_authenticated();
|
||||
include('scripts/service_controls.php');
|
||||
}
|
||||
if($_GET['view'] == "Spectrogram"){include('spectrogram.php');}
|
||||
if($_GET['view'] == "View Log"){echo "<body style=\"scroll:no;overflow-x:hidden;\"><iframe style=\"width:calc( 100% + 1em);\" src=\"/log\"></iframe></body>";}
|
||||
if($_GET['view'] == "Overview"){include('overview.php');}
|
||||
if($_GET['view'] == "Todays Detections"){include('todays_detections.php');}
|
||||
if($_GET['view'] == "Kiosk"){$kiosk = true;include('todays_detections.php');}
|
||||
if($_GET['view'] == "Species Stats"){include('stats.php');}
|
||||
if($_GET['view'] == "Weekly Report"){include('weekly_report.php');}
|
||||
if($_GET['view'] == "Streamlit"){echo "<iframe src=\"/stats\"></iframe>";}
|
||||
if($_GET['view'] == "Daily Charts"){include('history.php');}
|
||||
if($_GET['view'] == "Tools"){
|
||||
ensure_authenticated();
|
||||
$url = $_SERVER['SERVER_NAME']."/scripts/adminer.php";
|
||||
echo "<div class=\"centered\">
|
||||
<form action=\"views.php\" method=\"GET\" id=\"views\">
|
||||
<button type=\"submit\" name=\"view\" value=\"Settings\" form=\"views\">Settings</button>
|
||||
<button type=\"submit\" name=\"view\" value=\"System Info\" form=\"views\">System Info</button>
|
||||
<button type=\"submit\" name=\"view\" value=\"System Controls\" form=\"views\">System Controls".$updatediv."</button>
|
||||
<button type=\"submit\" name=\"view\" value=\"Services\" form=\"views\">Services</button>
|
||||
<button type=\"submit\" name=\"view\" value=\"File\" form=\"views\">File Manager</button>
|
||||
<a href=\"scripts/adminer.php\" target=\"_blank\"><button type=\"submit\" form=\"\">Database Maintenance</button></a>
|
||||
<button type=\"submit\" name=\"view\" value=\"Webterm\" form=\"views\">Web Terminal</button>
|
||||
<button type=\"submit\" name=\"view\" value=\"Included\" form=\"views\">Custom Species List</button>
|
||||
<button type=\"submit\" name=\"view\" value=\"Excluded\" form=\"views\">Excluded Species List</button>
|
||||
<button type=\"submit\" name=\"view\" value=\"Converted\" form=\"views\">Converted Species List</button>
|
||||
</form>
|
||||
</div>";
|
||||
}
|
||||
if($_GET['view'] == "Recordings"){include('play.php');}
|
||||
if($_GET['view'] == "Settings"){include('scripts/config.php');}
|
||||
if($_GET['view'] == "Advanced"){include('scripts/advanced.php');}
|
||||
if($_GET['view'] == "Included"){
|
||||
ensure_authenticated();
|
||||
if(isset($_GET['species']) && isset($_GET['add'])){
|
||||
$file = './scripts/include_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $str);
|
||||
file_put_contents("$file", "$str");
|
||||
if(isset($_GET['species'])){
|
||||
foreach ($_GET['species'] as $selectedOption)
|
||||
file_put_contents("./scripts/include_species_list.txt", htmlspecialchars_decode($selectedOption, ENT_QUOTES)."\n", FILE_APPEND);
|
||||
}
|
||||
} elseif(isset($_GET['species']) && isset($_GET['del'])){
|
||||
$file = './scripts/include_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace('/^\h*\v+/m', '', $str);
|
||||
file_put_contents("$file", "$str");
|
||||
foreach($_GET['species'] as $selectedOption) {
|
||||
$content = file_get_contents("../BirdNET-Pi/include_species_list.txt");
|
||||
$newcontent = str_replace($selectedOption, "", "$content");
|
||||
$newcontent = str_replace(htmlspecialchars_decode($selectedOption, ENT_QUOTES), "", "$newcontent");
|
||||
file_put_contents("./scripts/include_species_list.txt", "$newcontent");
|
||||
}
|
||||
$file = './scripts/include_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace('/^\h*\v+/m', '', $str);
|
||||
file_put_contents("$file", "$str");
|
||||
}
|
||||
include('./scripts/include_list.php');
|
||||
}
|
||||
if($_GET['view'] == "Excluded"){
|
||||
ensure_authenticated();
|
||||
if(isset($_GET['species']) && isset($_GET['add'])){
|
||||
$file = './scripts/exclude_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $str);
|
||||
file_put_contents("$file", "$str");
|
||||
foreach ($_GET['species'] as $selectedOption)
|
||||
file_put_contents("./scripts/exclude_species_list.txt", htmlspecialchars_decode($selectedOption, ENT_QUOTES)."\n", FILE_APPEND);
|
||||
} elseif (isset($_GET['species']) && isset($_GET['del'])){
|
||||
$file = './scripts/exclude_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace('/^\h*\v+/m', '', $str);
|
||||
file_put_contents("$file", "$str");
|
||||
foreach($_GET['species'] as $selectedOption) {
|
||||
$content = file_get_contents("./scripts/exclude_species_list.txt");
|
||||
$newcontent = str_replace($selectedOption, "", "$content");
|
||||
$newcontent = str_replace(htmlspecialchars_decode($selectedOption, ENT_QUOTES), "", "$content");
|
||||
file_put_contents("./scripts/exclude_species_list.txt", "$newcontent");
|
||||
}
|
||||
$file = './scripts/exclude_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace('/^\h*\v+/m', '', $str);
|
||||
file_put_contents("$file", "$str");
|
||||
}
|
||||
include('./scripts/exclude_list.php');
|
||||
}
|
||||
if($_GET['view'] == "Converted"){
|
||||
ensure_authenticated();
|
||||
if(isset($_GET['species']) && isset($_GET['add'])){
|
||||
$file = './scripts/convert_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $str);
|
||||
file_put_contents("$file", "$str");
|
||||
// Write $_GET['species'] to the file
|
||||
file_put_contents("./scripts/convert_species_list.txt", htmlspecialchars_decode($_GET['species'], ENT_QUOTES)."\n", FILE_APPEND);
|
||||
} elseif (isset($_GET['species']) && isset($_GET['del'])){
|
||||
$file = './scripts/convert_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace('/^\h*\v+/m', '', $str);
|
||||
file_put_contents("$file", "$str");
|
||||
foreach($_GET['species'] as $selectedOption) {
|
||||
$content = file_get_contents("./scripts/convert_species_list.txt");
|
||||
$newcontent = str_replace($selectedOption, "", "$content");
|
||||
$newcontent = str_replace(htmlspecialchars_decode($selectedOption, ENT_QUOTES), "", "$content");
|
||||
file_put_contents("./scripts/convert_species_list.txt", "$newcontent");
|
||||
}
|
||||
$file = './scripts/convert_species_list.txt';
|
||||
$str = file_get_contents("$file");
|
||||
$str = preg_replace('/^\h*\v+/m', '', $str);
|
||||
file_put_contents("$file", "$str");
|
||||
}
|
||||
include('./scripts/convert_list.php');
|
||||
}
|
||||
if($_GET['view'] == "File"){
|
||||
echo "<iframe src='scripts/filemanager/filemanager.php'></iframe>";
|
||||
}
|
||||
if($_GET['view'] == "Webterm"){
|
||||
ensure_authenticated('You cannot access the web terminal');
|
||||
echo "<iframe src='/terminal'></iframe>";
|
||||
}
|
||||
} elseif(isset($_GET['submit'])) {
|
||||
ensure_authenticated();
|
||||
$allowedCommands = array('sudo systemctl stop livestream.service && sudo systemctl stop icecast2.service',
|
||||
'sudo systemctl restart livestream.service && sudo systemctl restart icecast2.service',
|
||||
'sudo systemctl disable --now livestream.service && sudo systemctl disable icecast2 && sudo systemctl stop icecast2.service',
|
||||
'sudo systemctl enable icecast2 && sudo systemctl start icecast2.service && sudo systemctl enable --now livestream.service',
|
||||
'sudo systemctl stop web_terminal.service',
|
||||
'sudo systemctl restart web_terminal.service',
|
||||
'sudo systemctl disable --now web_terminal.service',
|
||||
'sudo systemctl enable --now web_terminal.service',
|
||||
'sudo systemctl stop birdnet_log.service',
|
||||
'sudo systemctl restart birdnet_log.service',
|
||||
'sudo systemctl disable --now birdnet_log.service',
|
||||
'sudo systemctl enable --now birdnet_log.service',
|
||||
'sudo systemctl stop birdnet_analysis.service',
|
||||
'sudo systemctl restart birdnet_analysis.service',
|
||||
'sudo systemctl disable --now birdnet_analysis.service',
|
||||
'sudo systemctl enable --now birdnet_analysis.service',
|
||||
'sudo systemctl stop birdnet_stats.service',
|
||||
'sudo systemctl restart birdnet_stats.service',
|
||||
'sudo systemctl disable --now birdnet_stats.service',
|
||||
'sudo systemctl enable --now birdnet_stats.service',
|
||||
'sudo systemctl stop birdnet_recording.service',
|
||||
'sudo systemctl restart birdnet_recording.service',
|
||||
'sudo systemctl disable --now birdnet_recording.service',
|
||||
'sudo systemctl enable --now birdnet_recording.service',
|
||||
'sudo systemctl stop chart_viewer.service',
|
||||
'sudo systemctl restart chart_viewer.service',
|
||||
'sudo systemctl disable --now chart_viewer.service',
|
||||
'sudo systemctl enable --now chart_viewer.service',
|
||||
'sudo systemctl stop spectrogram_viewer.service',
|
||||
'sudo systemctl restart spectrogram_viewer.service',
|
||||
'sudo systemctl disable --now spectrogram_viewer.service',
|
||||
'sudo systemctl enable --now spectrogram_viewer.service',
|
||||
'sudo systemctl enable '.get_service_mount_name().' && sudo reboot',
|
||||
'sudo systemctl disable '.get_service_mount_name().' && sudo reboot',
|
||||
'stop_core_services.sh',
|
||||
'restart_services.sh',
|
||||
'sudo reboot',
|
||||
'update_birdnet.sh',
|
||||
'sudo shutdown now',
|
||||
'sudo clear_all_data.sh');
|
||||
$command = $_GET['submit'];
|
||||
if(in_array($command,$allowedCommands)){
|
||||
if(isset($command)){
|
||||
$initcommand = $command;
|
||||
if (strpos($command, "systemctl") !== false) {
|
||||
//If there more than one command to execute, processes then separately
|
||||
//currently only livestream service uses multiple commands to interact with the required services
|
||||
if (strpos($command, " && ") !== false) {
|
||||
$separate_commands = explode("&&", trim($command));
|
||||
$new_multiservice_status_command = "";
|
||||
foreach ($separate_commands as $indiv_service_command) {
|
||||
//explode the string by " " space so we can get each individual component of the command
|
||||
//and eventually the service name at the end
|
||||
$separate_command_tmp = explode(" ", trim($indiv_service_command));
|
||||
//get the service names
|
||||
$new_multiservice_status_command .= " " . trim(end($separate_command_tmp));
|
||||
}
|
||||
|
||||
$service_names = $new_multiservice_status_command;
|
||||
} else {
|
||||
//only one service needs restarting so we only need to query the status of one service
|
||||
$tmp = explode(" ", trim($command));
|
||||
$service_names = end($tmp);
|
||||
}
|
||||
|
||||
$command .= " & sleep 3;sudo systemctl status " . $service_names;
|
||||
}
|
||||
if($initcommand == "update_birdnet.sh") {
|
||||
session_unset();
|
||||
}
|
||||
$results = shell_exec("$command 2>&1");
|
||||
$results = str_replace("FAILURE", "<span style='color:red'>FAILURE</span>", $results);
|
||||
$results = str_replace("failed", "<span style='color:red'>failed</span>",$results);
|
||||
$results = str_replace("active (running)", "<span style='color:green'><b>active (running)</b></span>",$results);
|
||||
$results = str_replace("Your branch is up to date", "<span style='color:limegreen'><b>Your branch is up to date</b></span>",$results);
|
||||
|
||||
$results = str_replace("(+)", "(<span style='color:lime;font-weight:bold'>+</span>)",$results);
|
||||
$results = str_replace("(-)", "(<span style='color:red;font-weight:bold'>-</span>)",$results);
|
||||
|
||||
// split the input string into lines
|
||||
$lines = explode("\n", $results);
|
||||
|
||||
// iterate over each line
|
||||
foreach ($lines as &$line) {
|
||||
// check if the line matches the pattern
|
||||
if (preg_match('/^(.+?)\s*\|\s*(\d+)\s*([\+\- ]+)(\d+)?$/', $line, $matches)) {
|
||||
// extract the filename, count, and indicator letters
|
||||
$filename = $matches[1];
|
||||
$count = $matches[2];
|
||||
$diff = $matches[3];
|
||||
$delta = $matches[4] ?? '';
|
||||
// determine the indicator letters
|
||||
$diff_array = str_split($diff);
|
||||
$indicators = array_map(function ($d) use ($delta) {
|
||||
if ($d === '+') {
|
||||
return "<span style='color:lime;'><b>+</b></span>";
|
||||
} elseif ($d === '-') {
|
||||
return "<span style='color:red;'><b>-</b></span>";
|
||||
} elseif ($d === ' ') {
|
||||
if ($delta !== '') {
|
||||
return 'A';
|
||||
} else {
|
||||
return ' ';
|
||||
}
|
||||
}
|
||||
}, $diff_array);
|
||||
// modify the line with the new indicator letters
|
||||
$line = sprintf('%-35s|%3d %s%s', $filename, $count, implode('', $indicators), $delta);
|
||||
}
|
||||
}
|
||||
|
||||
// rejoin the modified lines into a string
|
||||
$output = implode("\n", $lines);
|
||||
$results = $output;
|
||||
|
||||
// remove script tags (xss)
|
||||
$results = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $results);
|
||||
if(strlen($results) == 0) {
|
||||
$results = "This command has no output.";
|
||||
}
|
||||
echo "<table style='min-width:70%;'><tr class='relative'><th>Output of command:`".$initcommand."`<button class='copyimage' style='right:40px' onclick='copyOutput(this);'>Copy</button></th></tr><tr><td style='padding-left: 0px;padding-right: 0px;padding-bottom: 0px;padding-top: 0px;'><pre class='bash' style='text-align:left;margin:0px'>$results</pre></td></tr></table>";
|
||||
}
|
||||
}
|
||||
ob_end_flush();
|
||||
} else {include('overview.php');}
|
||||
?>
|
||||
<script>
|
||||
function myFunction() {
|
||||
var x = document.getElementById("myTopnav");
|
||||
if (x.className === "topnav") {
|
||||
x.className += " responsive";
|
||||
} else {
|
||||
x.className = "topnav";
|
||||
}
|
||||
}
|
||||
function setLiveStreamVolume(vol) {
|
||||
var audioelement = window.parent.document.getElementsByTagName("audio")[0];
|
||||
if (typeof(audioelement) != 'undefined' && audioelement != null)
|
||||
{
|
||||
audioelement.volume = vol
|
||||
}
|
||||
}
|
||||
window.onbeforeunload = function(event) {
|
||||
// if the user is playing a video and then navigates away mid-play, the live stream audio should be unmuted again
|
||||
var audioelement = window.parent.document.getElementsByTagName("audio")[0];
|
||||
if (typeof(audioelement) != 'undefined' && audioelement != null)
|
||||
{
|
||||
audioelement.volume = 1
|
||||
}
|
||||
}
|
||||
|
||||
function getTheDate(increment) {
|
||||
var theDate = "<?php if (isset($theDate)) echo $theDate;?>";
|
||||
|
||||
d = new Date(theDate);
|
||||
d.setDate(d.getDate(theDate) + increment);
|
||||
yyyy = d.getFullYear();
|
||||
mm = d.getMonth() + 1; if (mm < 10) mm = "0" + mm;
|
||||
dd = d.getDate(); if (dd < 10) dd = "0" + dd;
|
||||
|
||||
document.getElementById("SwipeSpinner").hidden = false;
|
||||
|
||||
window.location = "/views.php?date="+yyyy+"-"+mm+"-"+dd+"&view=Daily+Charts";
|
||||
}
|
||||
|
||||
function installKeyAndSwipeEventHandler() {
|
||||
for (var i = 0; i < topbuttons.length; i++) {
|
||||
if (topbuttons[i].textContent == "Daily Charts" &&
|
||||
topbuttons[i].className == "button-hover") {
|
||||
|
||||
document.onkeydown = function(event) {
|
||||
switch (event.keyCode) {
|
||||
case 37: //Left key
|
||||
getTheDate(-1);
|
||||
break;
|
||||
case 39: //Right key
|
||||
getTheDate(+1);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/questions/2264072/detect-a-finger-swipe-through-javascript-on-the-iphone-and-android
|
||||
let touchstartX = 0;
|
||||
let diffX = 0;
|
||||
let touchstartY = 0;
|
||||
let diffY = 0;
|
||||
let startTime = 0;
|
||||
let diffTime = 0;
|
||||
|
||||
function checkDirection() {
|
||||
if (Math.abs(diffX) > Math.abs(diffY) && diffTime < 500) {
|
||||
if (diffX > 20) getTheDate(+1);
|
||||
if (diffX < -20) getTheDate(-1);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', e => {
|
||||
touchstartX = e.changedTouches[0].screenX;
|
||||
touchstartY = e.changedTouches[0].screenY;
|
||||
startTime = Date.now();
|
||||
});
|
||||
|
||||
document.addEventListener('touchend', e => {
|
||||
diffX = touchstartX - e.changedTouches[0].screenX;
|
||||
diffY = touchstartY - e.changedTouches[0].screenY;
|
||||
diffTime = Date.now() - startTime;
|
||||
checkDirection();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installKeyAndSwipeEventHandler();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
Reference in New Issue
Block a user