Merge branch 'main' into feature/add-confidencepct-variable-to-apprise-notification

This commit is contained in:
ehpersonal38
2023-05-30 12:30:14 -04:00
committed by GitHub
46 changed files with 23176 additions and 407 deletions
+26
View File
@@ -1,3 +1,29 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
.idea
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
*.pyc
*.flac
.vscode
+13 -20
View File
@@ -14,23 +14,10 @@ Icon made by <a href="https://www.freepik.com" title="Freepik">Freepik</a> from
</p>
## Introduction
BirdNET-Pi is built on the (deprecated) [TFLite version of BirdNET](https://github.com/kahst/BirdNET-Lite) by [**@kahst**](https://github.com/kahst) <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg"></a> using [pre-built TFLite binaries](https://github.com/PINTO0309/TensorflowLite-bin) by [**@PINTO0309**](https://github.com/PINTO0309) . It is able to recognize bird sounds from a USB microphone or sound card in realtime and share its data with the rest of the world.
BirdNET-Pi is built on the [BirdNET framework](https://github.com/kahst/BirdNET-Analyzer) by [**@kahst**](https://github.com/kahst) <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg"></a> using [pre-built TFLite binaries](https://github.com/PINTO0309/TensorflowLite-bin) by [**@PINTO0309**](https://github.com/PINTO0309) . It is able to recognize bird sounds from a USB microphone or sound card in realtime and share its data with the rest of the world.
Check out birds from around the world
- [BirdWeather](https://app.birdweather.com)<br>
- [Johannesburg, South Africa](https://joburg.birdnetpi.com)<br>
- [Öringe, Tyresö, Sweden](https://tyreso.birdnetpi.com)<br>
- [Berowra, New South Wales, Australia](https://berowra.birdnetpi.com)
- [Dundas, Ontario, Canada](https://dundasontario.birdnetpi.com)
- [Bungendore, New South Wales, Australia](https://bungendorensw.birdnetpi.com)
- [Rivers Bend, Ohio, United States](https://riversbendoh.birdnetpi.com)
- [Vienna, Virginia, United States](https://viennava.birdnetpi.com)
- [Grevenbroich, Elsen, Germany](https://grevenbroich-elsen.birdnetpi.com)
- [Occoquan, Virginia, United States](https://occoquanva.birdnetpi.com)
- [Cambridge, Massachusetts, United States](https://cambridgema.birdnetpi.com)
[Share your installation!!](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi)
Have a public installation not in the list above? Let me know!! I'd be happy to add it.
Currently listening in these countries . . . that I know of . . .
- The United States
@@ -62,13 +49,19 @@ Currently listening in these countries . . . that I know of . . .
- South Sudan
- Argentina
- Brazil
- Thailand
- Colombia
- Estonia
- Tasmania
- Luxembourgh
- Crete
## Features
* 24/7 recording and automatic identification of bird songs using BirdNET machine learning
* Bird sounds are automatically extracted and catalogued
* Visualize your recorded bird data and analyze trends
* Live audio stream & spectrogram
* Automatic disk space management, where old audio files are periodically purged
* **24/7 recording and automatic identification** of bird songs, chirps, and peeps using BirdNET machine learning
* **Automatic extraction and cataloguing** of bird clips from full-length recordings
* **Tools to visualize your recorded bird data** and analyze trends
* **Live audio stream and spectrogram**
* **Automatic disk space management** that periodically purges old audio files
* [BirdWeather](https://app.birdweather.com) integration -- you can request a BirdWeather ID from BirdNET-Pi's "Tools" > "Settings" page
* Web interface access to all data and logs provided by [Caddy](https://caddyserver.com)
* [GoTTY](https://github.com/yudai/gotty) Web Terminal
@@ -101,7 +94,7 @@ The installer takes care of any and all necessary updates, so you can run that a
The installation creates a log in `$HOME/installation-$(date "+%F").txt`.
## Access
The BirdNET-Pi can be accessed from any web browser on the same network:
- http://birdnetpi.local
- http://birdnetpi.local OR your Pi's IP address
- Default Basic Authentication Username: birdnet
- Password is empty by default. Set this in "Tools" > "Settings" > "Advanced Settings"
+15 -1
View File
@@ -71,12 +71,14 @@ RTSP_STREAM=
#----------------------- Apprise Miscellanous Configuration -------------------#
APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection"
APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence"
APPRISE_NOTIFICATION_BODY="A \$comname (\$sciname) was just detected with a confidence of \$confidence"
APPRISE_NOTIFY_EACH_DETECTION=0
APPRISE_NOTIFY_NEW_SPECIES=0
APPRISE_WEEKLY_REPORT=1
APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY=0
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=0
APPRISE_ONLY_NOTIFY_SPECIES_NAMES=""
APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=""
#---------------------- Flickr Images API Configuration -----------------------#
## If FLICKR_API_KEY is set, the web interface will try and display bird images
@@ -195,12 +197,24 @@ DATABASE_LANG=en
SILENCE_UPDATE_INDICATOR=0
## RAW_SPECTROGRAM is for removing the axes and labels of the spectrograms
## that are generated by Sox for each detection for a cleaner appearance.
RAW_SPECTROGRAM=0
## HEARTBEAT_URL is a location to ping every time some analysis is done
## no information is sent to the the URL, it is a heart beat to show that the
## analysis is continuing
HEARTBEAT_URL=
## CUSTOM_IMAGE and CUSTOM_IMAGE_TITLE allow you to show a custom image on the
## Overview page of your BirdNET-Pi. This can be used to show a dynamically
## updating picture of your garden, for example.
CUSTOM_IMAGE=
CUSTOM_IMAGE_TITLE=""
## These are just for debugging
LAST_RUN=
THIS_RUN=
+1
View File
@@ -0,0 +1 @@
<svg class="img-fluid" id="outputsvg" xmlns="http://www.w3.org/2000/svg" style="transform: matrix(1.3, 0, 0, 1.3, 0, 0); transform-origin: 50% 50%; cursor: move; transition: transform 200ms ease-in-out 0s;" width="120" height="120" viewBox="0 0 1200 1200"><g id="led6HDXxLpKB3rJBInqSAJ" fill="rgb(253,174,179)" style="transform: none; transform-origin: 50% 50%;"><g style="transform: none;"><path id="ppWkGCoaW" d="M143 1036 l-28 -24 -6 -169 -7 -168 52 -49 c28 -26 100 -90 159 -142 100 -87 111 -94 151 -94 39 0 50 6 127 74 72 64 86 72 101 62 9 -7 91 -78 182 -159 129 -115 171 -147 193 -147 l28 0 -5 396 -5 396 -28 24 -28 24 -429 0 -429 0 -28 -24z"></path></g></g><g id="l1PReg5IXY9KUxbReAvzebZ" fill="rgb(239,68,82)" style="transform: none; transform-origin: 50% 50%;"><g style="transform: none;"><path id="pRauFKI19" d="M100 649 l0 -73 48 -45 c137 -132 293 -261 314 -261 13 0 37 12 54 28 117 102 143 122 161 122 19 0 37 -15 241 -193 109 -94 139 -106 167 -64 8 13 13 35 11 48 -3 13 -5 27 -5 32 -1 4 -6 7 -11 7 -6 0 -84 65 -173 144 -210 187 -213 189 -240 182 -12 -3 -60 -40 -106 -81 -46 -41 -90 -75 -97 -75 -12 0 -87 62 -255 210 -119 106 -109 104 -109 19z"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+27 -2
View File
@@ -1,4 +1,29 @@
<?php
/* Prevent XSS input */
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
$sys_timezone = "";
// If we can get the timezome from the systems timezone file ust that
if (file_exists('/etc/timezone')) {
$tz_data = file_get_contents('/etc/timezone');
if ($tz_data !== false) {
$sys_timezone = trim($tz_data);
}
} else {
// Else get timezone from the timedatectl command
$tz_data = shell_exec('timedatectl show');
$tz_data_array = parse_ini_string($tz_data);
if (is_array($tz_data_array) && array_key_exists('Timezone', $tz_data_array)) {
$sys_timezone = $tz_data_array['Timezone'];
}
}
//Finally if we have a valod timezone, set it as the one PHP uses
if ($sys_timezone !== "") {
date_default_timezone_set($sys_timezone);
}
if (file_exists('./scripts/thisrun.txt')) {
$config = parse_ini_file('./scripts/thisrun.txt');
} elseif (file_exists('./scripts/firstrun.ini')) {
@@ -17,7 +42,7 @@ body::-webkit-scrollbar {
display:none
}
</style>
<link rel="stylesheet" href="style.css?v=1.20.23">
<link rel="stylesheet" href="style.css?v=<?php echo date ('n.d.y', filemtime('style.css')); ?>">
<link rel="stylesheet" type="text/css" href="static/dialog-polyfill.css" />
<body>
<div class="banner">
@@ -79,4 +104,4 @@ echo "
<iframe src=\"/views.php\"></iframe>
</div>";
}
?>
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
var pluginTrendlineLinear={id:"trendlineLinear",afterDraw:function(a){var t,s;for(var e in a.scales)if("x"==e[0]?s=a.scales[e]:t=a.scales[e],s&&t)break;var n=a.chart.ctx;a.data.datasets.forEach(function(t,e){if(t.trendlineLinear&&a.isDatasetVisible(e)&&0!=t.data.length){var i=a.getDatasetMeta(e);addFitter(i,n,t,s,a.scales[i.yAxisID])}}),n.setLineDash([])}};function addFitter(t,e,i,a,s){var n=i.trendlineLinear.style||i.borderColor,r=i.trendlineLinear.width||i.borderWidth,o=i.trendlineLinear.lineStyle||"solid";n=void 0!==n?n:"rgba(169,169,169, .6)",r=void 0!==r?r:3;var l=new LineFitter,d=i.data.length-1,h=t.data[0]._model.x,u=t.data[d]._model.x,m=!1;i.data&&"object"==typeof i.data[0]&&(m=!0),i.data.forEach(function(t,e){if(null!=t)if("time"===a.options.type){var i=null!=t.x?t.x:t.t;l.add(new Date(i).getTime(),t.y)}else m?l.add(t.x,t.y):l.add(e,t)});var x=a.getPixelForValue(l.minx),c=a.getPixelForValue(l.maxx),f=s.getPixelForValue(l.f(l.minx)),g=s.getPixelForValue(l.f(l.maxx));m||(x=h,c=u);var p=t.controller.chart.chartArea.bottom,v=t.controller.chart.width;if(p<f){var X=f-p,L=f-g;f=p,x+=v*(X/L)}else if(p<g){X=g-p,L=g-f;g=p,c=v-(c-(v-v*(X/L)))}e.lineWidth=r,"dotted"===o&&e.setLineDash([2,3]),e.beginPath(),e.moveTo(x,f),e.lineTo(c,g),e.strokeStyle=n,e.stroke()}function LineFitter(){this.count=0,this.sumX=0,this.sumX2=0,this.sumXY=0,this.sumY=0,this.minx=1e100,this.maxx=-1e100}LineFitter.prototype={add:function(t,e){t=parseFloat(t),e=parseFloat(e),this.count++,this.sumX+=t,this.sumX2+=t*t,this.sumXY+=t*e,this.sumY+=e,t<this.minx&&(this.minx=t),t>this.maxx&&(this.maxx=t)},f:function(t){t=parseFloat(t);var e=this.count*this.sumX2-this.sumX*this.sumX;return(this.sumX2*this.sumY-this.sumX*this.sumXY)/e+t*((this.count*this.sumXY-this.sumX*this.sumY)/e)}},"undefined"!=typeof window&&window.Chart&&window.Chart.plugins.register(pluginTrendlineLinear);try{module.exports=exports=pluginTrendlineLinear}catch(t){}
+23 -3
View File
@@ -26,7 +26,7 @@ iframe {
padding: 0;
margin: 0;
border: none;
height: 93%;
height: 85%;
width: 100%;
}
@@ -409,8 +409,8 @@ button:hover {
}
#body::-webkit-scrollbar {
# display:none
#}
/*display:none*/
}
@media screen and (max-width: 1290px) {
.column1,.column2,.column3,.column4 {
@@ -563,6 +563,11 @@ button:hover {
height:25px !important;
}
.copyimage-mobile {
width: 16px !important;
height: 16px !important;
}
.relative {
position:relative;
}
@@ -814,6 +819,21 @@ pre#timer.bash {
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;
}
+116 -13
View File
@@ -1,4 +1,29 @@
<?php
/* Prevent XSS input */
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
$sys_timezone = "";
// If we can get the timezome from the systems timezone file ust that
if (file_exists('/etc/timezone')) {
$tz_data = file_get_contents('/etc/timezone');
if ($tz_data !== false) {
$sys_timezone = trim($tz_data);
}
} else {
// else get timezone from the timedatectl command
$tz_data = shell_exec('timedatectl show');
$tz_data_array = parse_ini_string($tz_data);
if (is_array($tz_data_array) && array_key_exists('Timezone', $tz_data_array)) {
$sys_timezone = $tz_data_array['Timezone'];
}
}
// finally if we have a valod timezone, set it as the one PHP uses
if ($sys_timezone !== "") {
date_default_timezone_set($sys_timezone);
}
session_start();
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
$user = trim($user);
@@ -11,21 +36,38 @@ if(!isset($_SESSION['behind'])) {
$num_commits_behind = $matches[1];
$_SESSION['behind'] = $num_commits_behind;
}
if(isset($_SESSION['behind'])&&intval($_SESSION['behind']) >= 99) {?>
if (preg_match('/\b(\d+)\b and \b(\d+)\b different commits each/', $str, $matches)) {
$num1 = (int) $matches[1];
$num2 = (int) $matches[2];
$sum = $num1 + $num2;
$_SESSION['behind'] = $sum;
}
}
if(isset($_SESSION['behind'])&&intval($_SESSION['behind']) >= 99) {?>
<style>
.updatenumber {
width:30px !important;
}
</style>
<?php }}
<?php }
if (file_exists('./scripts/thisrun.txt')) {
$config = parse_ini_file('./scripts/thisrun.txt');
} elseif (file_exists('./scripts/firstrun.ini')) {
$config = parse_ini_file('./scripts/firstrun.ini');
}
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>";
}
?>
<link rel="stylesheet" href="style.css?v=1.20.23">
<link rel="stylesheet" href="style.css?v=<?php echo date ('n.d.y', filemtime('style.css')); ?>">
<style>
body::-webkit-scrollbar {
display:none
@@ -37,7 +79,7 @@ body::-webkit-scrollbar {
<button type="submit" name="view" value="Overview" form="views">Overview</button>
</form>
<form action="" method="GET" id="views">
<button type="submit" name="view" value="Today's Detections" form="views">Today's Detections</button>
<button type="submit" name="view" value="Todays Detections" form="views">Today's Detections</button>
</form>
<form action="" method="GET" id="views">
<button type="submit" name="view" value="Spectrogram" form="views">Spectrogram</button>
@@ -107,7 +149,7 @@ if(isset($_GET['view'])){
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'] == "Today's Detections"){include('todays_detections.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');}
@@ -245,10 +287,10 @@ if(isset($_GET['view'])){
} else {
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
$submitteduser = $_SERVER['PHP_AUTH_USER'];
$allowedCommands = array('sudo systemctl stop livestream.service && sudo /etc/init.d/icecast2 stop',
'sudo systemctl restart livestream.service && sudo /etc/init.d/icecast2 restart',
'sudo systemctl disable --now livestream.service && sudo systemctl disable icecast2 && sudo /etc/init.d/icecast2 stop',
'sudo systemctl enable icecast2 && sudo /etc/init.d/icecast2 start && sudo systemctl enable --now livestream.service',
$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',
@@ -295,9 +337,28 @@ if(isset($_GET['view'])){
if($submittedpwd == $caddypwd && $submitteduser == 'birdnet' && in_array($command,$allowedCommands)){
if(isset($command)){
$initcommand = $command;
if (strpos($command, "systemctl") !== false) {
$tmp = explode(" ",trim($command));
$command .= "& sleep 3;sudo systemctl status ".end($tmp);
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") {
unset($_SESSION['behind']);
@@ -307,10 +368,52 @@ if(isset($_GET['view'])){
$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><pre style='text-align:left'>$results</pre></td></tr></table>";
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>";
} else {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9
View File
@@ -5,6 +5,15 @@ if [ "$EUID" == 0 ]
exit
fi
if [ "$(uname -m)" != "aarch64" ];then
echo "BirdNET-Pi requires a 64-bit OS.
It looks like your operating system is using $(uname -m),
but would need to be aarch64.
Please take a look at https://birdnetwiki.pmcgui.xyz for more
information"
exit 1
fi
# Simple new installer
HOME=$HOME
USER=$USER
+4 -2
View File
@@ -8,9 +8,11 @@ pytz
tzlocal
pandas
seaborn
streamlit
streamlit==1.19.0
plotly
apprise
apprise==1.2.1
paho-mqtt
pytest==7.1.2
pytest-mock==3.7.0
suntime
altair<5
+399 -30
View File
@@ -52,6 +52,8 @@ if(isset($_GET['submit'])) {
if(isset($_GET["birdnetpi_url"])) {
$birdnetpi_url = $_GET["birdnetpi_url"];
// remove trailing slash to prevent conf from becoming broken
$birdnetpi_url = rtrim($birdnetpi_url, '/');
if(strcmp($birdnetpi_url,$config['BIRDNETPI_URL']) !== 0) {
$contents = preg_replace("/BIRDNETPI_URL=.*/", "BIRDNETPI_URL=$birdnetpi_url", $contents);
$contents2 = preg_replace("/BIRDNETPI_URL=.*/", "BIRDNETPI_URL=$birdnetpi_url", $contents2);
@@ -77,6 +79,39 @@ if(isset($_GET['submit'])) {
}
}
if (isset($_GET["rtsp_stream_to_livestream"])) {
$rtsp_stream_selected = trim($_GET["rtsp_stream_to_livestream"]);
//Setting exists already, see if the value changed
if (strcmp($rtsp_stream_selected, $config['RTSP_STREAM_TO_LIVESTREAM']) !== 0) {
$contents = preg_replace("/RTSP_STREAM_TO_LIVESTREAM=.*/", "RTSP_STREAM_TO_LIVESTREAM=\"$rtsp_stream_selected\"", $contents);
$contents2 = preg_replace("/RTSP_STREAM_TO_LIVESTREAM=.*/", "RTSP_STREAM_TO_LIVESTREAM=\"$rtsp_stream_selected\"", $contents2);
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
sleep(1);
exec("sudo systemctl restart livestream.service");
}
}
if (isset($_GET["activate_freqshift_in_livestream"])) {
$activate_freqshift_in_livestream = trim($_GET["activate_freqshift_in_livestream"]);
//Setting exists already, see if the value changed
if (strcmp($activate_freqshift_in_livestream, $config['ACTIVATE_FREQSHIFT_IN_LIVESTREAM']) !== 0) {
$contents = preg_replace("/ACTIVATE_FREQSHIFT_IN_LIVESTREAM=.*/", "ACTIVATE_FREQSHIFT_IN_LIVESTREAM=\"$activate_freqshift_in_livestream\"", $contents);
$contents2 = preg_replace("/ACTIVATE_FREQSHIFT_IN_LIVESTREAM=.*/", "ACTIVATE_FREQSHIFT_IN_LIVESTREAM=\"$activate_freqshift_in_livestream\"", $contents2);
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
sleep(1);
exec("sudo systemctl restart livestream.service");
}
}
if(isset($_GET["overlap"])) {
$overlap = $_GET["overlap"];
if(strcmp($overlap,$config['OVERLAP']) !== 0) {
@@ -101,7 +136,7 @@ if(isset($_GET['submit'])) {
}
}
if(isset($_GET["freqshift_hi"])) {
if(isset($_GET["freqshift_hi"]) && is_numeric($_GET['freqshift_hi'])) {
$freqshift_hi = $_GET["freqshift_hi"];
if(strcmp($freqshift_hi,$config['FREQSHIFT_HI']) !== 0) {
$contents = preg_replace("/FREQSHIFT_HI=.*/", "FREQSHIFT_HI=$freqshift_hi", $contents);
@@ -109,7 +144,7 @@ if(isset($_GET['submit'])) {
}
}
if(isset($_GET["freqshift_lo"])) {
if(isset($_GET["freqshift_lo"]) && is_numeric($_GET['freqshift_lo'])) {
$freqshift_lo = $_GET["freqshift_lo"];
if(strcmp($freqshift_lo,$config['FREQSHIFT_LO']) !== 0) {
$contents = preg_replace("/FREQSHIFT_LO=.*/", "FREQSHIFT_LO=$freqshift_lo", $contents);
@@ -117,7 +152,7 @@ if(isset($_GET['submit'])) {
}
}
if(isset($_GET["freqshift_pitch"])) {
if(isset($_GET["freqshift_pitch"]) && is_numeric($_GET['freqshift_pitch'])) {
$freqshift_pitch = $_GET["freqshift_pitch"];
if(strcmp($freqshift_pitch,$config['FREQSHIFT_PITCH']) !== 0) {
$contents = preg_replace("/FREQSHIFT_PITCH=.*/", "FREQSHIFT_PITCH=$freqshift_pitch", $contents);
@@ -133,6 +168,14 @@ if(isset($_GET['submit'])) {
}
}
if(isset($_GET["freqshift_reconnect_delay"]) && is_numeric($_GET['freqshift_reconnect_delay'])) {
$freqshift_reconnect_delay = $_GET["freqshift_reconnect_delay"];
if(strcmp($freqshift_hi,$config['FREQSHIFT_RECONNECT_DELAY']) !== 0) {
$contents = preg_replace("/FREQSHIFT_RECONNECT_DELAY=.*/", "FREQSHIFT_RECONNECT_DELAY=$freqshift_reconnect_delay", $contents);
$contents2 = preg_replace("/FREQSHIFT_RECONNECT_DELAY=.*/", "FREQSHIFT_RECONNECT_DELAY=$freqshift_reconnect_delay", $contents2);
}
}
if(isset($_GET["full_disk"])) {
$full_disk = $_GET["full_disk"];
if(strcmp($full_disk,$config['FULL_DISK']) !== 0) {
@@ -200,10 +243,133 @@ if(isset($_GET['submit'])) {
$contents2 = preg_replace("/SILENCE_UPDATE_INDICATOR=.*/", "SILENCE_UPDATE_INDICATOR=0", $contents2);
}
$fh = fopen('/etc/birdnet/birdnet.conf', "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
if(isset($_GET["raw_spectrogram"])) {
$raw_spectrogram = 1;
if(strcmp($RAW_SPECTROGRAM,$config['RAW_SPECTROGRAM']) !== 0) {
$contents = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=$raw_spectrogram", $contents);
$contents2 = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=$raw_spectrogram", $contents2);
}
} else {
$contents = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=0", $contents);
$contents2 = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=0", $contents2);
}
if(isset($_GET["custom_image"])) {
$custom_image = $_GET["custom_image"];
if(strcmp($custom_image,$config['CUSTOM_IMAGE']) !== 0) {
$contents = preg_replace("/CUSTOM_IMAGE=.*/", "CUSTOM_IMAGE=$custom_image", $contents);
$contents2 = preg_replace("/CUSTOM_IMAGE=.*/", "CUSTOM_IMAGE=$custom_image", $contents2);
}
}
if(isset($_GET["custom_image_label"])) {
$custom_image_label = $_GET["custom_image_label"];
if(strcmp($custom_image_label,$config['CUSTOM_IMAGE_TITLE']) !== 0) {
$contents = preg_replace("/CUSTOM_IMAGE_TITLE=.*/", "CUSTOM_IMAGE_TITLE=\"$custom_image_label\"", $contents);
$contents2 = preg_replace("/CUSTOM_IMAGE_TITLE=.*/", "CUSTOM_IMAGE_TITLE=\"$custom_image_label\"", $contents2);
}
}
if (isset($_GET["LogLevel_BirdnetRecordingService"])) {
$birdnet_recording_service_log_level = trim($_GET["LogLevel_BirdnetRecordingService"]);
//If setting exists change it's value
if (array_key_exists('LogLevel_BirdnetRecordingService', $config)) {
//Setting exists already, see if the value changed
if (strcmp($birdnet_recording_service_log_level, $config['LogLevel_BirdnetRecordingService']) !== 0) {
$contents = preg_replace("/LogLevel_BirdnetRecordingService=.*/", "LogLevel_BirdnetRecordingService=\"$birdnet_recording_service_log_level\"", $contents);
$contents2 = preg_replace("/LogLevel_BirdnetRecordingService=.*/", "LogLevel_BirdnetRecordingService=\"$birdnet_recording_service_log_level\"", $contents2);
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
sleep(1);
exec("sudo systemctl restart birdnet_recording.service");
}
} else {
//Create the setting in the setting file - same as what update_birdnet_snippets.sh does but will take the users selected log level as the value
shell_exec('sudo echo "LogLevel_BirdnetRecordingService=\"' . $birdnet_recording_service_log_level . '\"" >> /etc/birdnet/birdnet.conf');
//also update this run txt file
shell_exec('sudo echo "LogLevel_BirdnetRecordingService=\"' . $birdnet_recording_service_log_level . '\"" >> ./scripts/thisrun.txt');
//Reload the config files as we've changed the contents, we need to make sure the contents of the existing variables reflects contents of the config file
sleep(1);
$contents = file_get_contents('/etc/birdnet/birdnet.conf');
$contents2 = file_get_contents('./scripts/thisrun.txt');
exec("sudo systemctl restart birdnet_recording.service");
}
}
if (isset($_GET["LogLevel_SpectrogramViewerService"])) {
$spectrogram_viewer_service_log_level = trim($_GET["LogLevel_SpectrogramViewerService"]);
//If setting exists change it's value
if (array_key_exists('LogLevel_SpectrogramViewerService', $config)) {
//Setting exists already, see if the value changed
if (strcmp($spectrogram_viewer_service_log_level, $config['LogLevel_SpectrogramViewerService']) !== 0) {
$contents = preg_replace("/LogLevel_SpectrogramViewerService=.*/", "LogLevel_SpectrogramViewerService=\"$spectrogram_viewer_service_log_level\"", $contents);
$contents2 = preg_replace("/LogLevel_SpectrogramViewerService=.*/", "LogLevel_SpectrogramViewerService=\"$spectrogram_viewer_service_log_level\"", $contents2);
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
sleep(1);
exec("sudo systemctl restart spectrogram_viewer.service");
}
} else {
//Create the setting in the setting file - same as what update_birdnet_snippets.sh does but will take the users selected log level as the value
shell_exec('sudo echo "LogLevel_SpectrogramViewerService=\"' . $spectrogram_viewer_service_log_level . '\"" >> /etc/birdnet/birdnet.conf');
//also update this run txt file
shell_exec('sudo echo "LogLevel_SpectrogramViewerService=\"' . $spectrogram_viewer_service_log_level . '\"" >> ./scripts/thisrun.txt');
//Reload the config files as we've changed the contents, we need to make sure the contents of the existing variables reflects contents of the config file
sleep(1);
$contents = file_get_contents('/etc/birdnet/birdnet.conf');
$contents2 = file_get_contents('./scripts/thisrun.txt');
exec("sudo systemctl restart spectrogram_viewer.service");
}
}
if (isset($_GET["LogLevel_LiveAudioStreamService"])) {
$livestream_audio_service_log_level = trim($_GET["LogLevel_LiveAudioStreamService"]);
//If setting exists change it's value
if (array_key_exists('LogLevel_LiveAudioStreamService', $config)) {
//Setting exists already, see if the value changed
if (strcmp($livestream_audio_service_log_level, $config['LogLevel_LiveAudioStreamService']) !== 0) {
$contents = preg_replace("/LogLevel_LiveAudioStreamService=.*/", "LogLevel_LiveAudioStreamService=\"$livestream_audio_service_log_level\"", $contents);
$contents2 = preg_replace("/LogLevel_LiveAudioStreamService=.*/", "LogLevel_LiveAudioStreamService=\"$livestream_audio_service_log_level\"", $contents2);
//Write the settings to the config files, so we can restart the relevant services
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
sleep(1);
exec("sudo systemctl restart livestream.service && sudo systemctl restart icecast2.service");
}
} else {
//Create the setting in the setting file - same as what update_birdnet_snippets.sh does but will take the users selected log level as the value
shell_exec('sudo echo "LogLevel_LiveAudioStreamService=\"' . $livestream_audio_service_log_level . '\"" >> /etc/birdnet/birdnet.conf');
//also update this run txt file
shell_exec('sudo echo "LogLevel_LiveAudioStreamService=\"' . $livestream_audio_service_log_level . '\"" >> ./scripts/thisrun.txt');
//Reload the config files as we've changed the contents, we need to make sure the contents of the existing variables reflects contents of the config file
sleep(1);
$contents = file_get_contents('/etc/birdnet/birdnet.conf');
$contents2 = file_get_contents('./scripts/thisrun.txt');
exec("sudo systemctl restart livestream.service && sudo systemctl restart icecast2.service");
}
}
//Finally write the data out. some sections do this themselves in order to have the new settings ready for the services that will be restarted
//but will doubly ensure the settings are saved after any modification
$fh = fopen('/etc/birdnet/birdnet.conf', "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
}
$user = trim(shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"));
@@ -225,9 +391,10 @@ if (file_exists('./scripts/thisrun.txt')) {
$newconfig = parse_ini_file('./scripts/firstrun.ini');
}
?>
<h2>Advanced Settings</h2>
<div class="brbanner"><h1>Advanced Settings</h1></div><br>
<form action="" method="GET">
<label>Privacy Threshold: </label><br>
<table class="settingstable"><tr><td>
<h2>Privacy Threshold</h2>
<div class="slidecontainer">
<input name="privacy_threshold" type="range" min="0" max="3" value="<?php print($newconfig['PRIVACY_THRESHOLD']);?>" class="slider" id="privacy_threshold">
<p>Value: <span id="threshold_value"></span>%</p>
@@ -244,21 +411,25 @@ if (file_exists('./scripts/thisrun.txt')) {
}
</script>
<p>If a Human is predicted anywhere among the top <span id="predictionCount"><?php echo $newconfig['PRIVACY_THRESHOLD'] == 0 ? "threshold % of" : intval(($newconfig['PRIVACY_THRESHOLD'] * $count)/100); ?></span> predictions, the sample will be considered of human origin and no data will be collected. Start with 1% and move up as needed.</p>
<label>Full Disk Behavior: </label>
</td></tr></table><br>
<table class="settingstable"><tr><td>
<h2>Full Disk Behaviour</h2>
<label for="purge">
<input name="full_disk" type="radio" id="purge" value="purge" <?php if (strcmp($newconfig['FULL_DISK'], "purge") == 0) { echo "checked"; }?>>Purge</label>
<label for="keep">
<input name="full_disk" type="radio" id="keep" value="keep" <?php if (strcmp($newconfig['FULL_DISK'], "keep") == 0) { echo "checked"; }?>>Keep</label>
<p>When the disk becomes full, you can choose to 'purge' old files to make room for new ones or 'keep' your data and stop all services instead.<br>Note: you can exclude specific files from 'purge' on the Recordings page.</p>
</td></tr></table><br>
<table class="settingstable"><tr><td>
<h2>Audio Settings</h2>
<label for="rec_card">Audio Card: </label>
<input name="rec_card" type="text" value="<?php print($newconfig['REC_CARD']);?>" required/><br>
<p>Set Audio Card to 'default' to use PulseAudio (always recommended), or an ALSA recognized sound card device from the output of `arecord -L`. Choose the `dsnoop` device if it is available</p>
<label for="channels">Audio Channels: </label>
<input name="channels" type="number" min="1" max="32" step="1" value="<?php print($newconfig['CHANNELS']);?>" required/><br>
<p>Set Channels to the number of channels supported by your sound card. 32 max.</p>
<label for="rtsp_stream">RTSP Stream: </label>
<input name="rtsp_stream" type="url" value="<?php echo $newconfig['RTSP_STREAM'];?>"</input><br>
<p>If you place an RTSP stream URL here, BirdNET-Pi will use that as its audio source.</p>
<label for="recording_length">Recording Length: </label>
<input name="recording_length" oninput="document.getElementsByName('extraction_length')[0].setAttribute('max', this.value);" type="number" min="3" max="60" step="1" value="<?php print($newconfig['RECORDING_LENGTH']);?>" required/><br>
<p>Set Recording Length in seconds between 6 and 60. Multiples of 3 are recommended, as BirdNET analyzes in 3-second chunks.</p>
@@ -275,19 +446,133 @@ foreach($formats as $format){
}
?>
</select>
<h3>BirdNET-Pi Password</h3>
<br><br>
<label id="rtsp_stream_input_label" for="rtsp_stream">RTSP Stream: </label>
<br>
<input style="display: none;" name="rtsp_stream" type="url" value="">
<input style="display: none;" id="rtsp_stream_url_placeholder" name="rtsp_stream_placeholder" type="url" size="60" value="">
<?php
//Print out the rtsp urls in their own input fields
//Explode the stream into an array at the comma
$rtsp_streams = explode(",", $newconfig['RTSP_STREAM']);
//Print out existing streams
foreach ($rtsp_streams as $stream_idx => $stream_url) {
//For the first input keep the element mostly the same as the original but without styling to align it
if ($stream_idx === 0) {
?>
<input id="rtsp_stream_url_0" name="rtsp_stream_0" type="url" size="60" value="<?php echo $stream_url; ?>">
<br>
<?php
} else {
//For every other input field, change the id to reflect the URL's index in the array
?>
<input id="rtsp_stream_url_<?php echo $stream_idx; ?>" name="rtsp_stream_<?php echo $stream_idx; ?>" type="url" size="60"
value="<?php echo $stream_url; ?>">
<br>
<?php
}
}
?>
<div id="newrtspstream_button_container">
<br>
<span id="newrtspstream" onclick="addNewrtspInput();">Add</span><br>
</div>
<script>
//Keep track of how many new input fields were added
var number_of_new_rtsp_urls_added = 0;
//Function to insert new input fields
function addNewrtspInput() {
//Find the placeholder input field
var url_template_element = document.getElementById('rtsp_stream_url_placeholder');
var new_url_input_template = url_template_element.cloneNode();
var br_seperator = document.createElement("BR");
//Fix up the new element so it's visible, set the style so it's sligned correctly
new_url_input_template.setAttribute("id", "rtsp_stream_url_new_" + number_of_new_rtsp_urls_added);
new_url_input_template.setAttribute("name", "rtsp_stream_new_" + number_of_new_rtsp_urls_added);
new_url_input_template.removeAttribute("style");
//Insert the new input field before the button to add new urls
var newrtspstream_button = document.getElementById('newrtspstream_button_container');
//Insert the new input element before the newrtspstream button
newrtspstream_button.parentNode.insertBefore(new_url_input_template, newrtspstream_button);
//Add a separator before the button
newrtspstream_button.parentNode.insertBefore(br_seperator, newrtspstream_button);
//Increment the counter
number_of_new_rtsp_urls_added++;
}
var rtsp_stream_string = "";
var rtsp_stream_string_array = [];
//Collect all the rtsp urls that have been set, concat them into a single string and set it into the rtsp_stream input field so it gets saved
function collectrtspUrls() {
//Reset the array and string so we don't get duplicates
rtsp_stream_string = "";
rtsp_stream_string_array = [];
//Get the inputs by name (which is similar across
var existing_rtsp_stream_urls = document.querySelectorAll('[name^="rtsp_stream_"]');
//Loop over the result and get the values
for (let i = 0; i < existing_rtsp_stream_urls.length; i++) {
//Only collect results that re not empty and add them to the array
if (existing_rtsp_stream_urls[i].value !== 'undefined' && existing_rtsp_stream_urls[i].value !== "") {
rtsp_stream_string_array.push(existing_rtsp_stream_urls[i].value.trim());
}
}
//if the array is not empty, then implode the array joining all the values by a comma
if (rtsp_stream_string_array.length !== 0) {
rtsp_stream_string = rtsp_stream_string_array.join(',');
//Locate the hidden rtsp_stream input field that we'll populate with the full string which will get saved to the config file
var rtsp_stream_input = document.querySelector('[name=rtsp_stream]');
rtsp_stream_input.setAttribute('value', rtsp_stream_string);
}
}
</script>
<p>If you place an RTSP stream URL here, BirdNET-Pi will use that as its audio source.<br>Multiple streams are allowed but may have a impact on rPi performance.<br>Analyze ffmpeg CPU/Memory usage with <b>top</b> or <b>htop</b> if necessary.<br>To remove all and use the soundcard again, just delete the RTSP entries and click Save at the bottom.</p>
</td></tr></table><br>
<table class="settingstable"><tr><td>
<h2>BirdNET-Pi Password</h2>
<p>This password will protect your "Tools" page and "Live Audio" stream.</p>
<p>Do NOT use special characters. Accepted characters: [A-Z0-9a-z]</p>
<label for="caddy_pwd">Password: </label>
<input style="width:40ch" name="caddy_pwd" id="caddy_pwd" type="password" pattern="[A-Za-z0-9]+" title="Password must be alphanumeric (A-Z, 0-9)" value="<?php print($newconfig['CADDY_PWD']);?>" /><span id="showpassword" onmouseover="document.getElementById('caddy_pwd').type='text';" onmouseout="document.getElementById('caddy_pwd').type='password';">show</span><br>
<h3>Custom URL</h3>
</td></tr></table><br>
<table class="settingstable"><tr><td>
<h2>Custom URL</h2>
<p>When you update the URL below, the web server will reload, so be sure to wait at least 30 seconds and then go to your new URL.</p>
<label for="birdnetpi_url">BirdNET-Pi URL: </label>
<input style="width:40ch;" name="birdnetpi_url" type="url" value="<?php print($newconfig['BIRDNETPI_URL']);?>" /><br>
<p>The BirdNET-Pi URL is how the main page will be reached. If you want your installation to respond to an IP address, place that here, but be sure to indicate "<i>http://</i>".<br>Example for IP: <i>http://192.168.0.109</i><br>Example if you own your own domain: <i>https://virginia.birdnetpi.com</i></p>
</td></tr></table><br>
<table class="settingstable"><tr><td>
<h2>Options</h2>
<label for="silence_update_indicator">Silence Update Indicator: </label>
<input type="checkbox" name="silence_update_indicator" <?php if($newconfig['SILENCE_UPDATE_INDICATOR'] == 1) { echo "checked"; };?> ><br>
<p>This allows you to quiet the display of how many commits your installation is behind by relative to the Github repo. This number appears next to "Tools" when you're 50 or more commits behind.</p>
<h3>BirdNET-Lite Settings</h3>
<label for="raw_spectrogram">Minimalist Spectrograms: </label>
<input type="checkbox" name="raw_spectrogram" <?php if($newconfig['RAW_SPECTROGRAM'] == 1) { echo "checked"; };?> ><br>
<p>This allows you to remove the axes and labels of the spectrograms that are generated by Sox for each detection for a cleaner appearance.</p>
</td></tr></table><br>
<table class="settingstable"><tr><td>
<h2>Custom Image</h2>
<label for="custom_image">Custom Image Absolute Path: </label>
<input name="custom_image" type="text" value="<?php print($newconfig['CUSTOM_IMAGE']);?>"/><br>
<label for="custom_image_label">Custom Image Label: </label>
<input name="custom_image_label" type="text" value="<?php print($newconfig['CUSTOM_IMAGE_TITLE']);?>"/><br>
<p>These allow you to show a custom image on the Overview page of your BirdNET-Pi. This can be used to show a dynamically updating picture of your garden, for example.</p>
</td></tr></table><br>
<table class="settingstable"><tr><td>
<h2>BirdNET-Lite Settings</h2>
<p>
<label for="overlap">Overlap: </label>
@@ -304,15 +589,17 @@ foreach($formats as $format){
<input name="sensitivity" type="number" min="0.5" max="1.5" step="0.01" value="<?php print($newconfig['SENSITIVITY']);?>" required/><br>
&nbsp;&nbsp;&nbsp;&nbsp;Min=0.5, Max=1.5
</p>
</td></tr></table><br>
<h3>Accessibility Settings</h3>
<table class="settingstable"><tr><td>
<h2>Accessibility Settings</h2>
<p>Birdsongs Frequency shifting configuration:<br>
this can be useful for hearing impaired people.<br>
This can be useful for hearing impaired people. <br>Note: audio files will only be pitch shifted when the "FREQ SHIFT" button is manually clicked for a detection on the "Recordings" page. <br>The frequency shifting can also be activated for the realtime audio livestream, accessible in the SPECTROGRAM tab of BirdNET-Pi. Once it has been activated, it will be made available for the Live Audio feature as well.<br>Livestream is using ffmpeg for streaming its audio data, so the pitch shifter in that case will use this tool too. If you choose sox as the tool for freq shifting recorded audio files, then you must configure both sox and ffmpeg parameters: sox for recordings, and ffmpeg for livestream.<br>
<p style="margin-left: 40px">
<label for="freqshift_tool">shifting tool: </label>
<label for="freqshift_tool">Shifting tool: </label>
<select name="freqshift_tool">
<option selected="<?php print($newconfig['FREQSHIFT_TOOL']);?>"><?php print($newconfig['FREQSHIFT_TOOL']);?></option>
<?php
@@ -325,29 +612,111 @@ foreach($formats as $format){
?>
</select>
Choose here the shifting tool.<br>
Choose the shifting tool here.<br>
</p>
<p style="margin-left: 40px">
using ffmpeg:
Using ffmpeg:
e.g. origin=6000, target=4000, performs a shift of 2000 Hz down.<br>
<label for="freqshift_hi">origin [Hz]: </label>
<label for="freqshift_hi">Origin [Hz]: </label>
<input name="freqshift_hi" type="number" min="0" max="20000" step="1" value="<?php print($newconfig['FREQSHIFT_HI']);?>" required/><br>
<label for="freqshift_lo">target [Hz]: </label>
<label for="freqshift_lo">Target [Hz]: </label>
<input name="freqshift_lo" type="number" min="0" max="20000" step="1" value="<?php print($newconfig['FREQSHIFT_LO']);?>" required/>
</p>
<p style="margin-left: 40px">
using sox:
e.g. shiftPitch=-1200 performs a shift of 1 octave down. This value is in 100ths of a semitone.<br>
<label for="freqshift_pitch">pitch shift: </label>
<input name="freqshift_pitch" type="number" min="-4000" max="4000" step="1" value="<?php print($newconfig['FREQSHIFT_PITCH']);?>" required/><br>
<label for="freqshift_reconnect_delay">Livestream reconnection delay (in ms): </label>
<input name="freqshift_reconnect_delay" type="number" min="1000" max="10000" step="100" value="<?php print($newconfig['FREQSHIFT_RECONNECT_DELAY']);?>" required/>
</p>
</p>
<p style="margin-left: 40px">
Using sox:
e.g. shiftPitch=-1200 performs a shift of 1 octave down. This value is in 100ths of a semitone.<br>
<label for="freqshift_pitch">Pitch shift: </label>
<input name="freqshift_pitch" type="number" min="-4000" max="4000" step="1" value="<?php print($newconfig['FREQSHIFT_PITCH']);?>" required/><br>
</p>
</td></tr></table><br>
<table class="settingstable">
<tr>
<td>
<h2>Logging</h2>
<div class="callout callout-warning">
<b>Note:</b>
It is recommended that the Log Level be set to <b>Error</b> on production systems to keep output
manageable, by only reporting errors.
<br>
Not all components support the log level option at this time.
</div>
</td>
</tr>
<tr>
<td>Birdnet Recording:
<select id="LogLevel_BirdnetRecordingService" name="LogLevel_BirdnetRecordingService">
<option value="error" <?php echo $newconfig['LogLevel_BirdnetRecordingService'] == "error" || !array_key_exists('LogLevel_BirdnetRecordingService', $newconfig) ? "selected=''" : "" ?>>
Errors Only
</option>
<option value="warning" <?php echo $newconfig['LogLevel_BirdnetRecordingService'] == "warning" ? "selected=''" : "" ?>>
Warning
</option>
<option value="info" <?php echo $newconfig['LogLevel_BirdnetRecordingService'] == "info" ? "selected=''" : "" ?>>
Info
</option>
<option value="debug" <?php echo $newconfig['LogLevel_BirdnetRecordingService'] == "debug" ? "selected=''" : "" ?>>
Debug
</option>
</select>
</td>
</tr>
<tr>
<td>Live Audio Stream:
<select id="LogLevel_LiveAudioStreamService" name="LogLevel_LiveAudioStreamService">
<option value="error" <?php echo $newconfig['LogLevel_LiveAudioStreamService'] == "error" || !array_key_exists('LogLevel_LiveAudioStreamService', $newconfig) ? "selected=''" : "" ?>>
Errors Only
</option>
<option value="warning" <?php echo $newconfig['LogLevel_LiveAudioStreamService'] == "warning" ? "selected=''" : "" ?>>
Warning
</option>
<option value="info" <?php echo $newconfig['LogLevel_LiveAudioStreamService'] == "info" ? "selected=''" : "" ?>>
Info
</option>
<option value="debug" <?php echo $newconfig['LogLevel_LiveAudioStreamService'] == "debug" ? "selected=''" : "" ?>>
Debug
</option>
</select>
</td>
</tr>
<tr>
<td>Spectrogram Service:
<select id="LogLevel_SpectrogramViewerService" name="LogLevel_SpectrogramViewerService">
<option value="error" <?php echo $newconfig['LogLevel_SpectrogramViewerService'] == "error" || !array_key_exists('LogLevel_SpectrogramViewerService', $newconfig) ? "selected=''" : "" ?>>
Errors Only
</option>
<option value="warning" <?php echo $newconfig['LogLevel_SpectrogramViewerService'] == "warning" ? "selected=''" : "" ?>>
Warning
</option>
<option value="info" <?php echo $newconfig['LogLevel_SpectrogramViewerService'] == "info" ? "selected=''" : "" ?>>
Info
</option>
<option value="debug" <?php echo $newconfig['LogLevel_SpectrogramViewerService'] == "debug" ? "selected=''" : "" ?>>
Debug
</option>
</select>
</td>
</tr>
<tr>
<td>
<small>
<b>'error'</b> - Show all errors, including ones which can be recovered from. <b>This is the default value.</b><br>
<b>'warning'</b> - Show all warnings and errors. Any message related to possibly incorrect or unexpected events will be shown.<br>
<b>'info'</b> - Show informative messages and output during processing. This is in addition to warnings and errors. This will produce more output, use this for initial debugging.<br>
<b>'debug'</b> - Show everything, including debugging information. Produces a lot of output.<br>
</small>
</td>
</tr>
</table>
<br><br>
<input type="hidden" name="view" value="Advanced">
<button onclick="if(<?php print($newconfig['PRIVACY_THRESHOLD']);?> != document.getElementById('privacy_threshold').value){return confirm('This will take about 90 seconds.')}" type="submit" name="submit" value="advanced">
<button onclick="if(<?php print($newconfig['PRIVACY_THRESHOLD']);?> != document.getElementById('privacy_threshold').value){return confirm('This will take about 90 seconds.')} collectrtspUrls();" type="submit" name="submit" value="advanced">
<?php
if(isset($_GET['submit'])){
echo "Success!";
+1 -1
View File
@@ -189,7 +189,7 @@ TODAY="$RECS_DIR/$(date "+%B-%Y/%d-%A")"
if [ $(find ${YESTERDAY} -name '*wav' 2>/dev/null | wc -l) -gt 0 ];then
find $YESTERDAY -name '*wav' -type f -size 0 -delete
run_birdnet "${YESTERDAY}"
elif [ $(find ${TODAY} -name '*wav' | wc -l) -gt 0 ];then
elif [ $(find ${TODAY} -name '*wav' 2>/dev/null | wc -l) -gt 0 ];then
find $TODAY -name '*wav' -type f -size 0 -delete
run_birdnet "${TODAY}"
fi
+38 -3
View File
@@ -1,15 +1,50 @@
#!/usr/bin/env bash
set -x
# Performs the recording from the specified RTSP stream or soundcard
source /etc/birdnet/birdnet.conf
# Read the logging level from the configuration option
LOGGING_LEVEL="${LogLevel_BirdnetRecordingService}"
# If empty for some reason default to log level of error
[ -z $LOGGING_LEVEL ] && LOGGING_LEVEL='error'
# Additionally if we're at debug or info level then allow printing of script commands and variables
if [ "$LOGGING_LEVEL" == "info" ] || [ "$LOGGING_LEVEL" == "debug" ];then
# Enable printing of commands/variables etc to terminal for debugging
set -x
fi
[ -z $RECORDING_LENGTH ] && RECORDING_LENGTH=15
if [ ! -z $RTSP_STREAM ];then
[ -d $RECS_DIR/StreamData ] || mkdir -p $RECS_DIR/StreamData
# Explode the RSPT steam setting into an array so we can count the number we have
RTSP_STREAMS_EXPLODED_ARRAY=(${RTSP_STREAM//,/ })
while true;do
for i in ${RTSP_STREAM//,/ };do
ffmpeg -nostdin -i ${i} -t ${RECORDING_LENGTH} -vn -acodec pcm_s16le -ac 2 -ar 48000 file:${RECS_DIR}/StreamData/$(date "+%F")-birdnet-$(date "+%H:%M:%S").wav
# Original loop
# for i in ${RTSP_STREAM//,/ };do
# ffmpeg -nostdin -i ${i} -t ${RECORDING_LENGTH} -vn -acodec pcm_s16le -ac 2 -ar 48000 file:${RECS_DIR}/StreamData/$(date "+%F")-birdnet-$(date "+%H:%M:%S").wav
# done
# Initially start the count off at 1 - our very first stream
RTSP_STREAMS_STARTED_COUNT=1
FFMPEG_PARAMS=""
# Loop over the streams
for i in "${RTSP_STREAMS_EXPLODED_ARRAY[@]}"
do
# Map id used to map input to output (first stream being 0), this is 0 based in ffmpeg so decrement our counter (which is more human readable) by 1
MAP_ID=$((RTSP_STREAMS_STARTED_COUNT-1))
# Build up the parameters to process the RSTP stream, including mapping for the output
FFMPEG_PARAMS+="-vn -thread_queue_size 512 -i ${i} -map ${MAP_ID}:a:0 -t ${RECORDING_LENGTH} -acodec pcm_s16le -ac 2 -ar 48000 file:${RECS_DIR}/StreamData/$(date "+%F")-birdnet-RTSP_${RTSP_STREAMS_STARTED_COUNT}-$(date "+%H:%M:%S").wav "
# Increment counter
((RTSP_STREAMS_STARTED_COUNT += 1))
done
# Make sure were passing something valid to ffmpeg, ffmpeg will run interactive and control our loop by waiting ${RECORDING_LENGTH} between loops because it will stop once that much has been recorded
if [ -n "$FFMPEG_PARAMS" ];then
ffmpeg -hide_banner -loglevel $LOGGING_LEVEL -nostdin $FFMPEG_PARAMS
fi
done
else
if ! pulseaudio --check;then pulseaudio --start;fi
+100 -85
View File
@@ -2,6 +2,36 @@
error_reporting(E_ERROR);
ini_set('display_errors',1);
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
if (file_exists($home.'/BirdNET-Pi/scripts/thisrun.txt')) {
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.txt');
} elseif (file_exists($home.'/BirdNET-Pi/scripts/thisrun.ini')) {
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.ini');
}
if (file_exists($home."/BirdNET-Pi/apprise.txt")) {
$apprise_config = file_get_contents($home."/BirdNET-Pi/apprise.txt");
} else {
$apprise_config = "";
}
$caddypwd = $config['CADDY_PWD'];
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo '<table><tr><td>You cannot edit the settings for this installation</td></tr></table>';
exit;
} else {
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
$submitteduser = $_SERVER['PHP_AUTH_USER'];
if($submittedpwd !== $caddypwd || $submitteduser !== 'birdnet'){
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo '<table><tr><td>You cannot edit the settings for this installation</td></tr></table>';
exit;
}
}
function syslog_shell_exec($cmd, $sudo_user = null) {
if ($sudo_user) {
$cmd = "sudo -u $sudo_user $cmd";
@@ -53,6 +83,8 @@ if(isset($_GET["latitude"])){
$timezone = $_GET["timezone"];
$model = $_GET["model"];
$sf_thresh = $_GET["sf_thresh"];
$only_notify_species_names = $_GET['only_notify_species_names'];
$only_notify_species_names_2 = $_GET['only_notify_species_names_2'];
if(isset($_GET['apprise_notify_each_detection'])) {
$apprise_notify_each_detection = 1;
@@ -75,13 +107,13 @@ if(isset($_GET["latitude"])){
$apprise_weekly_report = 0;
}
if(isset($timezone)) {
if(isset($timezone) && in_array($timezone, DateTimeZone::listIdentifiers())) {
shell_exec("sudo timedatectl set-timezone ".$timezone);
date_default_timezone_set($timezone);
echo "<script>setTimeout(
function() {
const xhttp = new XMLHttpRequest();
xhttp.open(\"GET\", \"scripts/config.php?restart_php=true\", true);
xhttp.open(\"GET\", \"./config.php?restart_php=true\", true);
xhttp.send();
}, 1000);</script>";
}
@@ -91,7 +123,11 @@ if(isset($_GET["latitude"])){
// can't set the date manually if it's getting it from the internet, disable ntp
exec("sudo timedatectl set-ntp false");
exec("sudo date -s '".$_GET['date']." ".$_GET['time']."'");
// check if valid date and time
$datetime = DateTime::createFromFormat('Y-m-d H:i', $_GET['date'] . ' ' . $_GET['time']);
if ($datetime && $datetime->format('Y-m-d H:i') === $_GET['date'] . ' ' . $_GET['time']) {
exec("sudo date -s '".$_GET['date']." ".$_GET['time']."'");
}
} else {
// user checked 'use time from internet if available,' so make sure that's on
if(strlen(trim(exec("sudo timedatectl | grep \"NTP service: active\""))) == 0){
@@ -100,40 +136,29 @@ if(isset($_GET["latitude"])){
}
}
// Update Language settings only if a change is requested
if (file_exists('./scripts/thisrun.txt')) {
$lang_config = parse_ini_file('./scripts/thisrun.txt');
} elseif (file_exists('./scripts/firstrun.ini')) {
$lang_config = parse_ini_file('./scripts/firstrun.ini');
}
if ($language != $lang_config['DATABASE_LANG']){
$user = trim(shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"));
$home = trim(shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"));
// Archive old language file
syslog_shell_exec("cp -f $home/BirdNET-Pi/model/labels.txt $home/BirdNET-Pi/model/labels.txt.old", $user);
// Install new language label file
syslog_shell_exec("$home/BirdNET-Pi/scripts/install_language_label.sh -l $language", $user);
syslog(LOG_INFO, "Successfully changed language to '$language'");
if (file_exists($home.'/BirdNET-Pi/scripts/thisrun.txt')) {
$lang_config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.txt');
} elseif (file_exists($home.'/BirdNET-Pi/scripts/thisrun.ini')) {
$lang_config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.ini');
}
if ($model != $lang_config['MODEL']){
$user = trim(shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"));
$home = trim(shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"));
if ($model != $lang_config['MODEL'] || $language != $lang_config['DATABASE_LANG']){
if(strlen($language) == 2){
$user = trim(shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"));
$home = trim(shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"));
// Archive old language file
syslog_shell_exec("cp -f $home/BirdNET-Pi/model/labels.txt $home/BirdNET-Pi/model/labels.txt.old", $user);
// Archive old language file
syslog_shell_exec("cp -f $home/BirdNET-Pi/model/labels.txt $home/BirdNET-Pi/model/labels.txt.old", $user);
if($model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16"){
// Install new language label file
syslog_shell_exec("sudo chmod +x $home/BirdNET-Pi/scripts/install_language_label_nm.sh && $home/BirdNET-Pi/scripts/install_language_label_nm.sh -l $language", $user);
} else {
syslog_shell_exec("$home/BirdNET-Pi/scripts/install_language_label.sh -l $language", $user);
if($model == "BirdNET_GLOBAL_3K_V2.3_Model_FP16"){
// Install new language label file
syslog_shell_exec("chmod +x $home/BirdNET-Pi/scripts/install_language_label_nm.sh && $home/BirdNET-Pi/scripts/install_language_label_nm.sh -l $language", $user);
} else {
syslog_shell_exec("$home/BirdNET-Pi/scripts/install_language_label.sh -l $language", $user);
}
syslog(LOG_INFO, "Successfully changed language to '$language' and model to '$model'");
}
syslog(LOG_INFO, "Successfully changed language to '$language'");
}
@@ -154,8 +179,10 @@ if(isset($_GET["latitude"])){
$contents = preg_replace("/APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=.*/", "APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=$minimum_time_limit", $contents);
$contents = preg_replace("/MODEL=.*/", "MODEL=$model", $contents);
$contents = preg_replace("/SF_THRESH=.*/", "SF_THRESH=$sf_thresh", $contents);
$contents = preg_replace("/APPRISE_ONLY_NOTIFY_SPECIES_NAMES=.*/", "APPRISE_ONLY_NOTIFY_SPECIES_NAMES=\"$only_notify_species_names\"", $contents);
$contents = preg_replace("/APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=.*/", "APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=\"$only_notify_species_names_2\"", $contents);
$contents2 = file_get_contents("./scripts/thisrun.txt");
$contents2 = file_get_contents($home."/BirdNET-Pi/scripts/thisrun.txt");
$contents2 = preg_replace("/SITE_NAME=.*/", "SITE_NAME=\"$site_name\"", $contents2);
$contents2 = preg_replace("/LATITUDE=.*/", "LATITUDE=$latitude", $contents2);
$contents2 = preg_replace("/LONGITUDE=.*/", "LONGITUDE=$longitude", $contents2);
@@ -172,6 +199,8 @@ if(isset($_GET["latitude"])){
$contents2 = preg_replace("/APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=.*/", "APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=$minimum_time_limit", $contents2);
$contents2 = preg_replace("/MODEL=.*/", "MODEL=$model", $contents2);
$contents2 = preg_replace("/SF_THRESH=.*/", "SF_THRESH=$sf_thresh", $contents2);
$contents2 = preg_replace("/APPRISE_ONLY_NOTIFY_SPECIES_NAMES=.*/", "APPRISE_ONLY_NOTIFY_SPECIES_NAMES=\"$only_notify_species_names\"", $contents2);
$contents2 = preg_replace("/APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=.*/", "APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=\"$only_notify_species_names_2\"", $contents2);
@@ -183,7 +212,7 @@ if(isset($_GET["latitude"])){
}
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
$fh2 = fopen($home."/BirdNET-Pi/scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
@@ -201,18 +230,17 @@ if(isset($_GET["latitude"])){
}
if(isset($_GET['sendtest']) && $_GET['sendtest'] == "true") {
$db = new SQLite3('./birds.db', SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
if (file_exists('./thisrun.txt')) {
$config = parse_ini_file('./thisrun.txt');
} elseif (file_exists('./firstrun.ini')) {
$config = parse_ini_file('./firstrun.ini');
if (file_exists($home.'/BirdNET-Pi/scripts/thisrun.txt')) {
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.txt');
} elseif (file_exists($home.'/BirdNET-Pi/scripts/thisrun.ini')) {
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.ini');
}
$db = new SQLite3($home."/BirdNET-Pi/scripts/birds.db", SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);
$cf = explode("\n",$_GET['apprise_config']);
$cf = "'".implode("' '", $cf)."'";
@@ -280,11 +308,17 @@ if(isset($_GET['sendtest']) && $_GET['sendtest'] == "true") {
$body = str_replace("\$overlap", $overlap, $body);
$body = str_replace("\$flickrimage", $exampleimage, $body);
echo "<pre class=\"bash\">".shell_exec($home."/BirdNET-Pi/birdnet/bin/apprise -vv --plugin-path ".$home."/.apprise/plugins "." -t '".$title."' -b '".$body."' ".$attach." ".$cf." ")."</pre>";
echo "<pre class=\"bash\">".shell_exec($home."/BirdNET-Pi/birdnet/bin/apprise -vv --plugin-path ".$home."/.apprise/plugins "." -t '".escapeshellcmd($title)."' -b '".escapeshellcmd($body)."' ".$attach." ".$cf." ")."</pre>";
die();
}
// have to get the config again after we change the variables, so the UI reflects the changes too
if (file_exists($home.'/BirdNET-Pi/scripts/thisrun.txt')) {
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.txt');
} elseif (file_exists($home.'/BirdNET-Pi/scripts/thisrun.ini')) {
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.ini');
}
?>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@@ -293,42 +327,12 @@ if(isset($_GET['sendtest']) && $_GET['sendtest'] == "true") {
<div class="settings">
<div class="brbanner"><h1>Basic Settings</h1></div><br>
<form id="basicform" action="" method="GET">
<?php
if (file_exists('./scripts/thisrun.txt')) {
$config = parse_ini_file('./scripts/thisrun.txt');
} elseif (file_exists('./scripts/firstrun.ini')) {
$config = parse_ini_file('./scripts/firstrun.ini');
}
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
if (file_exists($home."/BirdNET-Pi/apprise.txt")) {
$apprise_config = file_get_contents($home."/BirdNET-Pi/apprise.txt");
} else {
$apprise_config = "";
}
$caddypwd = $config['CADDY_PWD'];
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo '<table><tr><td>You cannot edit the settings for this installation</td></tr></table>';
exit;
} else {
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
$submitteduser = $_SERVER['PHP_AUTH_USER'];
if($submittedpwd !== $caddypwd || $submitteduser !== 'birdnet'){
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo '<table><tr><td>You cannot edit the settings for this installation</td></tr></table>';
exit;
}
}
?>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('modelsel').addEventListener('change', function() {
if(this.value == "BirdNET_GLOBAL_3K_V2.2_Model_FP16"){
if(this.value == "BirdNET_GLOBAL_3K_V2.3_Model_FP16"){
document.getElementById("soft").style.display="unset";
} else {
document.getElementById("soft").style.display="none";
@@ -360,7 +364,7 @@ function sendTestNotification(e) {
<label for="model">Select a Model: </label>
<select id="modelsel" name="model">
<?php
$models = array("BirdNET_6K_GLOBAL_MODEL", "BirdNET_GLOBAL_3K_V2.2_Model_FP16");
$models = array("BirdNET_6K_GLOBAL_MODEL", "BirdNET_GLOBAL_3K_V2.3_Model_FP16");
foreach($models as $modelName){
$isSelected = "";
if($config['MODEL'] == $modelName){
@@ -375,7 +379,8 @@ function sendTestNotification(e) {
<span <?php if($config['MODEL'] == "BirdNET_6K_GLOBAL_MODEL") { ?>style="display: none"<?php } ?> id="soft">
<label for="sf_thresh">Species Occurence Frequency Threshold [0.0005, 0.99]: </label>
<input name="sf_thresh" type="number" max="0.99" min="0.0005" step="any" value="<?php print($config['SF_THRESH']);?>"/> <span onclick="document.getElementById('sfhelp').style.display='unset'" style="text-decoration:underline;cursor:pointer">[more info]</span><br>
<p id="sfhelp" style='display:none'>This value is used by the model to constrain the list of possible species that it will try to detect, given the minimum occurence frequency. A 0.03 threshold means that for a species to be included in this list, it needs to, on average, be seen on at least 3% of historically submitted eBird checklists for your given lat/lon/current week of year. So, the lower the threshold, the rarer the species it will include.<br><br>If you'd like to tinker with this threshold value and see which species make it onto the list, <?php if($config['MODEL'] == "BirdNET_6K_GLOBAL_MODEL"){ ?>please click "Update Settings" at the very bottom of this page to install the appropriate label file, then come back here and you'll be able to use the Species List Tester.<?php } else { ?>you can use this tool: <button type="button" class="testbtn" id="openModal">Species List Tester</button><?php } ?></p>
<p id="sfhelp" style='display:none'>This value is used by the model to constrain the list of possible species that it will try to detect, given the minimum occurence frequency. A 0.03 threshold means that for a species to be included in this list, it needs to, on average, be seen on at least 3% of historically submitted eBird checklists for your given lat/lon/current week of year. So, the lower the threshold, the rarer the species it will include.<br><img style='width:75%;padding-top:5px;padding-bottom:5px' alt="BirdNET-Pi new model detection flowchart" title="BirdNET-Pi new model detection flowchart" src="https://i.imgur.com/8YEAuSA.jpeg">
<br>If you'd like to tinker with this threshold value and see which species make it onto the list, <?php if($config['MODEL'] == "BirdNET_6K_GLOBAL_MODEL"){ ?>please click "Update Settings" at the very bottom of this page to install the appropriate label file, then come back here and you'll be able to use the Species List Tester.<?php } else { ?>you can use this tool: <button type="button" class="testbtn" id="openModal">Species List Tester</button><?php } ?></p>
</span>
<script src="static/dialog-polyfill.js"></script>
@@ -474,9 +479,11 @@ function runProcess() {
<dl>
<dt>BirdNET_6K_GLOBAL_MODEL (2020)</dt><br>
<dd id="ddnewline">This model comes from BirdNET-Lite, with bird sound recognition for more than 6,000 species worldwide. This is the default option and will generally work very well for most use cases.</dd>
<dt>BirdNET_GLOBAL_3K_V2.2_Model_FP16 (2022)</dt><br>
<dd id="ddnewline">This model comes from BirdNET-Analyzer, a newer work-in-progress project with aims to improve on the old model. Currently it only supports about 3,500 species worldwide, so for users in North America, this model is generally much more accurate than the above model, but elsewhere it will be less accurate and possibly useless.</dd>
<dd id="ddnewline">This is the BirdNET-Lite model, with bird sound recognition for more than 6,000 species worldwide. This is the default option and will generally work very for people in most of the world.</dd>
<br>
<dt>BirdNET_GLOBAL_3K_V2.3_Model_FP16 (2023)</dt><br>
<dd id="ddnewline">This is the BirdNET-Analyzer model, a newer work-in-progress project with aims to improve on the BirdNET-Lite model. Currently it only supports about 3,500 species worldwide, meaning for some regions (North America, Europe, Australia) it will usually outperform the BirdNET-Lite model, but for other regions it will be worse.</dd><br>
<dt>[ In-depth technical write-up on the models <a target="_blank" href="https://github.com/mcguirepr89/BirdNET-Pi/wiki/BirdNET-Pi:-some-theory-on-classification-&-some-practical-hints">here</a> ]</dt>
</dl>
</td></tr></table><br>
@@ -484,7 +491,7 @@ function runProcess() {
<h2>Location</h2>
<label for="site_name">Site Name: </label>
<input name="site_name" type="text" value="<?php print($config['SITE_NAME']);?>"/> (Optional)<br>
<label for="latitude">Latitude: </label>
<label for="latitude">Latitude: &nbsp; &nbsp;</label>
<input name="latitude" type="number" max="90" min="-90" step="0.0001" value="<?php print($config['LATITUDE']);?>" required/><br>
<label for="longitude">Longitude: </label>
<input name="longitude" type="number" max="180" min="-180" step="0.0001" value="<?php print($config['LONGITUDE']);?>" required/><br>
@@ -494,7 +501,7 @@ function runProcess() {
<h2>BirdWeather</h2>
<label for="birdweather_id">BirdWeather ID: </label>
<input name="birdweather_id" type="text" value="<?php print($config['BIRDWEATHER_ID']);?>" /><br>
<p><a href="https://app.birdweather.com" target="_blank">BirdWeather.com</a> is a weather map for bird sounds. Stations around the world supply audio and video streams to BirdWeather where they are then analyzed by BirdNET and compared to eBird Grid data. BirdWeather catalogues the bird audio and spectrogram visualizations so that you can listen to, view, and read about birds throughout the world. <a href="mailto:tim@birdweather.com?subject=Request%20BirdWeather%20ID&body=<?php include('./scripts/birdweather_request.php'); ?>" target="_blank">Email Tim</a> to request a BirdWeather ID</p>
<p><a href="https://app.birdweather.com" target="_blank">BirdWeather.com</a> is a weather map for bird sounds. Stations around the world supply audio and video streams to BirdWeather where they are then analyzed by BirdNET and compared to eBird Grid data. BirdWeather catalogues the bird audio and spectrogram visualizations so that you can listen to, view, and read about birds throughout the world. <a href="mailto:tim@birdweather.com?subject=Request%20BirdWeather%20ID&body=<?php include($home.'/BirdNET-Pi/scripts/birdweather_request.php'); ?>" target="_blank">Email Tim</a> to request a BirdWeather ID</p>
</td></tr></table><br>
<table class="settingstable" style="width:100%"><tr><td>
<h2>Notifications</h2>
@@ -547,11 +554,15 @@ https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}
<input type="checkbox" name="apprise_notify_each_detection" <?php if($config['APPRISE_NOTIFY_EACH_DETECTION'] == 1 && filesize($home."/BirdNET-Pi/apprise.txt") != 0) { echo "checked"; };?> >
<label for="apprise_weekly_report">Notify each new detection</label><br>
<input type="checkbox" name="apprise_weekly_report" <?php if($config['APPRISE_WEEKLY_REPORT'] == 1 && filesize($home."/BirdNET-Pi/apprise.txt") != 0) { echo "checked"; };?> >
<label for="apprise_weekly_report">Send weekly report</label><br>
<label for="apprise_weekly_report">Send <a href="views.php?view=Weekly%20Report"> weekly report</a></label><br>
<hr>
<label for="quantity">Minimum time between notifications of the same species (sec):</label>
<label for="minimum_time_limit">Minimum time between notifications of the same species (sec):</label>
<input type="number" id="minimum_time_limit" name="minimum_time_limit" value="<?php echo $config['APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES'];?>" min="0"><br>
<label for="only_notify_species_names">Exclude these species (comma separated common names):</label>
<input type="text" id="only_notify_species_names" placeholder="Mourning Dove,American Crow" name="only_notify_species_names" value="<?php echo $config['APPRISE_ONLY_NOTIFY_SPECIES_NAMES'];?>" size=96><br>
<label for="only_notify_species_names_2">ONLY notify for these species (comma separated common names):</label>
<input type="text" id="only_notify_species_names_2" placeholder="Northern Cardinal,Carolina Chickadee,Eastern Bluebird" name="only_notify_species_names_2" value="<?php echo $config['APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2'];?>" size=96><br>
<br>
@@ -647,8 +658,12 @@ https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}
<h2>Time and Date</h2>
<span>If connected to the internet, retrieve time automatically?</span>
<input type="checkbox" onchange='handleChange(this)' <?php echo $checkedvalue; ?> ><br>
<input onclick="this.showPicker()" type="date" id="date" name="date" value="<?php echo date('Y-m-d') ?>" <?php echo $disabledvalue; ?>>
<input onclick="this.showPicker()" type="time" id="time" name="time" value="<?php echo date('H:i') ?>" <?php echo $disabledvalue; ?>><br>
<?php
$date = new DateTime('now');
?>
<input onclick="this.showPicker()" type="date" id="date" name="date" value="<?php echo $date->format('Y-m-d') ?>" <?php echo $disabledvalue; ?>>
<input onclick="this.showPicker()" type="time" id="time" name="time" value="<?php echo $date->format('H:i'); ?>" <?php echo $disabledvalue; ?>><br>
<br>
<label for="timezone">Select a Timezone: </label>
<select name="timezone">
<option disabled selected>
+16 -6
View File
@@ -82,24 +82,25 @@ plot.set(xlabel="Detections")
# Generate crosstab matrix for heatmap plot
heat = pd.crosstab(df_plt_top10_today['Com_Name'], df_plt_top10_today['Hour of Day'])
# Order heatmap Birds by frequency of occurrance
heat.index = pd.CategoricalIndex(heat.index, categories=freq_order)
heat.sort_index(level=0, inplace=True)
hours_in_day = pd.Series(data=range(0, 24))
heat_frame = pd.DataFrame(data=0, index=heat.index, columns=hours_in_day)
heat = (heat + heat_frame).fillna(0)
# Generatie heatmap plot
# Get current hour
current_hour = now.hour
# Generate heatmap plot
plot = sns.heatmap(
heat,
norm=LogNorm(),
annot=True,
annot_kws={
"fontsize": 7},
annot_kws={"fontsize": 7},
fmt="g",
cmap=pal,
square=False,
@@ -107,9 +108,18 @@ plot = sns.heatmap(
linewidths=0.5,
linecolor="Grey",
ax=axs[1],
yticklabels=False)
yticklabels=False
)
# Set color and weight of tick label for current hour
for label in plot.get_xticklabels():
if int(label.get_text()) == current_hour:
label.set_color('yellow')
plot.set_xticklabels(plot.get_xticklabels(), rotation=0, size=7)
# Set heatmap border
for _, spine in plot.spines.items():
spine.set_visible(True)
+3 -1
View File
@@ -4,7 +4,9 @@ Below is the information I would like to use to request a BirdWeather ID
%0A%0A
Latitude={{ LATITUDE }}%0A
Longitude={{ LONGITUDE }}%0A
Location= [ Create a location name ]%0A
City= [ City ]%0A
State= [ State ]%0A
Country= [ Country ]%0A
%0A%0A
Thank you so much!%0A
[ Your Name Here ]
+84
View File
@@ -6,6 +6,9 @@
<div class="customlabels column1">
<form action="" method="GET" id="add">
<h3>All Species Labels</h3>
<input autocomplete="off" size="18" type="text" placeholder="Search Species..." id="exclude_species_searchterm" name="exclude_species_searchterm">
<br>
<span>Once the desired species has been highlighted, click it and then click ADD to have it excluded.</span>
<select name="species[]" id="species" multiple size="auto">
<option>Choose a species below to add to the Excluded Species List</option>
<?php
@@ -37,6 +40,7 @@
<div class="customlabels column3">
<form action="" method="GET" id="del">
<h3>Excluded Species List</h3>
<br><br>
<select name="species[]" id="value2" multiple size="auto">
<?php
$filename = './scripts/exclude_species_list.txt';
@@ -53,5 +57,85 @@
</div>
</div>
<script>
document.getElementById("add").addEventListener("submit", function(event) {
var speciesSelect = document.getElementById("species");
if (speciesSelect.selectedIndex < 1) {
alert("Please click the species you want to add.");
document.querySelector('.views').style.opacity = 1;
event.preventDefault();
}
});
var search_term = document.querySelector("input#exclude_species_searchterm");
search_term.addEventListener("keydown", doSearch);
//Index where we found a match
var search_match_idx = 1;
var last_search_term = "";
function doSearch(eventObj) {
//Don't do anything if the user is till composing
if (eventObj.isComposing || eventObj.keyCode === 229) {
return;
}
//If the key pressed is the enter key capture it, stop the form submitting and do the search
if (eventObj.key === 'Enter' || eventObj.keyCode === 13) {
eventObj.preventDefault();
//User wants to submit the text as a search
var search_text = search_term.value.toLowerCase();
//Now look at the select list, loop over the options and try find part of the text in the option's name/text
var species_select_list = document.querySelector('select#species');
//if the search text differs from last time start the search from the beginning of te list
if (search_text !== last_search_term) {
//Also unselect the last match
species_select_list[search_match_idx].removeAttribute('class');
search_match_idx = 1;
}
//Start the loop at 1 so we skip the very initial value asking the user to select a option
for (let i = search_match_idx; i < species_select_list.length; i++) {
// if (species_select_list[i] !== 'undefined') {
option_text = species_select_list[i].value;
search_match_text = option_text.toLowerCase().includes(search_text)
//Check if the item is already selected, that could mean that user may be searching the same phrase
if (species_select_list[search_match_idx].getAttribute('class') === "exclude_species_list_option_highlight") {
species_select_list[search_match_idx].removeAttribute('class');
// species_select_list[search_match_idx].removeAttribute('style');
//Go to the next item
i++
continue;
}
//There was a match,
if (search_match_text === true) {
//already on this item so skip it and continue with list
if (search_match_idx === i) {
i++
continue;
}
//Finally we havent found this item before
search_match_idx = i;
//Scroll into view and select it
species_select_list[search_match_idx].scrollIntoView({behavior: 'smooth', block: 'start'});
//Apply a style to the option since setting the selected value to true breaks scrolling on chrome :(
//the style is nicer anyway :)
species_select_list[search_match_idx].setAttribute('class', "exclude_species_list_option_highlight");
// species_select_list[search_match_idx].setAttribute('selected', 'true');
//break the loop
break;
}
}
//Track what search term was used so we know when to start over
last_search_term = search_text
}
}
</script>
</body>
+58 -55
View File
@@ -4,63 +4,66 @@
echo "........................................IPs....................................."
echo "LAN IP: $(hostname -I|cut -d' ' -f1)"
echo "Public IP: $(curl -s4 ifconfig.co)"
echo "..................................\`vcgencmd stats\`.............................."
sudo -u$USER vcgencmd get_throttled
hex=$(sudo -u$USER vcgencmd get_throttled|cut -d'x' -f2)
binary=$(echo "ibase=16;obase=2;$hex"|bc)
echo "Binary: $binary";
revbinary=$(echo $binary|rev)
if echo $binary | grep 1 ;then
echo "ISSUES DETECTED"
if [ ${revbinary:0:1} -eq 1 &> /dev/null ];then
message="Under-voltage detected"
echo "$message"
dmesg -H | grep -i voltage
fi
if [ ${revbinary:1:1} -eq 1 &> /dev/null ];then
message="Arm frequency capped"
echo "$message"
dmesg -H | grep -i frequen
fi
if [ ${revbinary:2:1} -eq 1 &> /dev/null ];then
message="Currently Throttled"
echo "$message"
dmesg -H | grep -i throttl
fi
if [ ${revbinary:3:1} -eq 1 &> /dev/null ];then
message="Soft temperatue limit active"
echo "$message"
dmesg -H | grep -i temperature
fi
if [ ${revbinary:16:1} -eq 1 &> /dev/null ];then
message="Under-voltage has occurred"
echo "$message"
dmesg -H | grep -i voltage
fi
if [ ${revbinary:17:1} -eq 1 &> /dev/null ];then
message="Arm frequency capping has occurred"
echo "$message"
dmesg -H | grep -i frequen
fi
if [ ${revbinary:18:1} -eq 1 &> /dev/null ];then
message="Throttling has occurred"
echo "$message"
dmesg -H | grep -i throttl
fi
if [ ${revbinary:19:1} -eq 1 &> /dev/null ];then
message="Soft temperature limit has occurred"
echo "$message"
dmesg -H | grep -i temperature
if ! dpkg -l | grep -q vcgencmd ; then
echo "..................................\`vcgencmd stats\`.............................."
sudo -u$USER vcgencmd get_throttled
hex=$(sudo -u$USER vcgencmd get_throttled | cut -d'x' -f2)
binary=$(echo "ibase=16;obase=2;$hex" | bc)
echo "Binary: $binary"
revbinary=$(echo $binary | rev)
if echo $binary | grep 1; then
echo "ISSUES DETECTED"
if [ ${revbinary:0:1} -eq 1 ] &>/dev/null; then
message="Under-voltage detected"
echo "$message"
dmesg -H | grep -i voltage
fi
if [ ${revbinary:1:1} -eq 1 ] &>/dev/null; then
message="Arm frequency capped"
echo "$message"
dmesg -H | grep -i frequen
fi
if [ ${revbinary:2:1} -eq 1 ] &>/dev/null; then
message="Currently Throttled"
echo "$message"
dmesg -H | grep -i throttl
fi
if [ ${revbinary:3:1} -eq 1 ] &>/dev/null; then
message="Soft temperatue limit active"
echo "$message"
dmesg -H | grep -i temperature
fi
if [ ${revbinary:16:1} -eq 1 ] &>/dev/null; then
message="Under-voltage has occurred"
echo "$message"
dmesg -H | grep -i voltage
fi
if [ ${revbinary:17:1} -eq 1 ] &>/dev/null; then
message="Arm frequency capping has occurred"
echo "$message"
dmesg -H | grep -i frequen
fi
if [ ${revbinary:18:1} -eq 1 ] &>/dev/null; then
message="Throttling has occurred"
echo "$message"
dmesg -H | grep -i throttl
fi
if [ ${revbinary:19:1} -eq 1 ] &>/dev/null; then
message="Soft temperature limit has occurred"
echo "$message"
dmesg -H | grep -i temperature
fi
fi
echo "....................................Clock Speeds................................"
for i in arm core h264 isp v3d uart pwm emmc pixel vec hdmi dpi; do
echo -e "${i}:\t$(sudo -u$USER vcgencmd measure_clock ${i})"
done
echo "........................................Volts..................................."
for i in core sdram_c sdram_i sdram_p; do
echo -e "${i}:\t$(sudo -u$USER vcgencmd measure_volts ${i})"
done
fi
echo "....................................Clock Speeds................................"
for i in arm core h264 isp v3d uart pwm emmc pixel vec hdmi dpi;do
echo -e "${i}:\t$(sudo -u$USER vcgencmd measure_clock ${i})"
done
echo "........................................Volts..................................."
for i in core sdram_c sdram_i sdram_p;do
echo -e "${i}:\t$(sudo -u$USER vcgencmd measure_volts ${i})"
done
echo ".....................................Caddyfile.................................."
cat /etc/caddy/Caddyfile
echo ".................................... Crontab...................................."
+16 -5
View File
@@ -118,11 +118,22 @@ for h in "${SCAN_DIRS[@]}";do
sox -V1 "${h}/${OLDFILE}" "${NEWSPECIES_BYDATE}/${NEWFILE}" \
trim ="${START}" ="${END}"
# Create spectrogram for extraction
sox -V1 "${NEWSPECIES_BYDATE}/${NEWFILE}" -n remix 1 rate 24k spectrogram \
-t "${COMMON_NAME}" \
-c "${NEWSPECIES_BYDATE//$HOME\/}/${NEWFILE}" \
-o "${NEWSPECIES_BYDATE}/${NEWFILE}.png"
RAW_SPECTROGRAM=${RAW_SPECTROGRAM}
# Check if RAW_SPECTROGRAM is 1
if [ "$RAW_SPECTROGRAM" == "1" ]; then
# If it is, add "-r" as an argument to the SOX command
sox -V1 "${NEWSPECIES_BYDATE}/${NEWFILE}" -n remix 1 rate 24k spectrogram \
-t "${COMMON_NAME}" \
-c "${NEWSPECIES_BYDATE//$HOME\/}/${NEWFILE}" \
-o "${NEWSPECIES_BYDATE}/${NEWFILE}.png" \
-r
else
# If it's not, run the SOX command without the "-r" argument
sox -V1 "${NEWSPECIES_BYDATE}/${NEWFILE}" -n remix 1 rate 24k spectrogram \
-t "$(echo "${COMMON_NAME}" | iconv -f utf8 -t ascii//TRANSLIT)" \
-c "${NEWSPECIES_BYDATE//$HOME\/}/${NEWFILE}" \
-o "${NEWSPECIES_BYDATE}/${NEWFILE}.png"
fi
done < "${TMPFILE}"
+9 -2
View File
@@ -1,4 +1,9 @@
<?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_ALL);
ini_set('display_errors',1);
ini_set('display_startup_errors',1);
@@ -148,14 +153,16 @@ function submitID() {
</table>
<?php // <br><button type="button" onclick="showDialog()">Export as CSV for eBird</button><br><br> ?>
<?php
$time = time();
if (file_exists('./Charts/'.$chart)) {
echo "<img src=\"/Charts/$chart?nocache=time()\" >";
echo "<img src=\"/Charts/$chart?nocache=$time\" >";
} else {
echo "<p>No Charts for $theDate</p>";
}
echo "<hr>";
if (file_exists('./Charts/'.$chart2)) {
echo "<img src=\"/Charts/$chart2?nocache=time()\">";
echo "<img src=\"/Charts/$chart2?nocache=$time\">";
} else {
echo "<p>No Charts For $theDate</p>";
}?>
+20 -3
View File
@@ -8,8 +8,8 @@ echo "Beginning $0"
birdnet_conf=$my_dir/birdnet.conf
# Retrieve latitude and longitude from web
LATITUDE=$(curl -s4 ifconfig.co/json | jq .latitude)
LONGITUDE=$(curl -s4 ifconfig.co/json | jq .longitude)
LATITUDE=$(curl -s4 http://ip-api.com/json?fields=lat,lon | jq .lat)
LONGITUDE=$(curl -s4 http://ip-api.com/json?fields=lat,lon | jq .lon)
# Define regular expression pattern
pattern='^[+-]?[0-9]{2}\.[0-9]{4}$'
@@ -99,12 +99,14 @@ RTSP_STREAM=
#----------------------- Apprise Miscellanous Configuration -------------------#
APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection"
APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence"
APPRISE_NOTIFICATION_BODY="A \$comname (\$sciname) was just detected with a confidence of \$confidence"
APPRISE_NOTIFY_EACH_DETECTION=0
APPRISE_NOTIFY_NEW_SPECIES=0
APPRISE_WEEKLY_REPORT=1
APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY=0
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=0
APPRISE_ONLY_NOTIFY_SPECIES_NAMES=""
APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=""
#---------------------- Flickr Images API Configuration -----------------------#
## If FLICKR_API_KEY is set, the web interface will try and display bird images
@@ -167,6 +169,9 @@ FREQSHIFT_HI=6000
## FREQSHIFT_LO
FREQSHIFT_LO=3000
## FREQSHIFT_RECONNECT_DELAY
FREQSHIFT_RECONNECT_DELAY=4000
## If the tool is sox, you have to define the pitch shift (amount of 100ths of semintone)
## FREQSHIFT_PITCH
FREQSHIFT_PITCH=-1500
@@ -228,6 +233,18 @@ HEARTBEAT_URL=
SILENCE_UPDATE_INDICATOR=0
## RAW_SPECTROGRAM is for removing the axes and labels of the spectrograms
## that are generated by Sox for each detection for a cleaner appearance.
RAW_SPECTROGRAM=0
## CUSTOM_IMAGE and CUSTOM_IMAGE_TITLE allow you to show a custom image on the
## Overview page of your BirdNET-Pi. This can be used to show a dynamically
## updating picture of your garden, for example.
CUSTOM_IMAGE=
CUSTOM_IMAGE_TITLE=""
## These are just for debugging
LAST_RUN=
THIS_RUN=
+35 -2
View File
@@ -2,14 +2,47 @@
# Live Audio Stream Service Script
source /etc/birdnet/birdnet.conf
# Read the logging level from the configuration option
LOGGING_LEVEL="${LogLevel_LiveAudioStreamService}"
# If empty for some reason default to log level of error
[ -z $LOGGING_LEVEL ] && LOGGING_LEVEL='error'
# Additionally if we're at debug or info level then allow printing of script commands and variables
if [ "$LOGGING_LEVEL" == "info" ] || [ "$LOGGING_LEVEL" == "debug" ];then
# Enable printing of commands/variables etc to terminal for debugging
set -x
fi
if [ "$ACTIVATE_FREQSHIFT_IN_LIVESTREAM" == "true" ]; then
FREQSHIFT_OPT='-af rubberband=pitch='${FREQSHIFT_LO}'/'${FREQSHIFT_HI}
fi
if [ -z ${REC_CARD} ];then
echo "Stream not supported"
elif [[ ! -z ${RTSP_STREAM} ]];then
ffmpeg -nostdin -loglevel 32 -ac ${CHANNELS} -i ${RTSP_STREAM} -acodec libmp3lame \
# Explode the RSPT steam setting into an array so we can count the number we have
RSTP_STREAMS_EXPLODED_ARRAY=(${RTSP_STREAM//,/ })
# If for some reason the RTSP_STREAM_TO_LIVESTREAM is not set, then init it to 0 to use the first stream
if [[ -z ${RTSP_STREAM_TO_LIVESTREAM} ]];then
RTSP_STREAM_TO_LIVESTREAM=0
fi
# Get the RSTP stream at the specified array index
SELECTED_RSTP_STREAM=${RSTP_STREAMS_EXPLODED_ARRAY[RTSP_STREAM_TO_LIVESTREAM]}
# If for some reason the RTSP stream url is null
if [[ -z ${SELECTED_RSTP_STREAM} ]];then
# Try select the first stream
SELECTED_RSTP_STREAM=${RSTP_STREAMS_EXPLODED_ARRAY[0]}
fi
ffmpeg -nostdin -loglevel $LOGGING_LEVEL -ac ${CHANNELS} -i ${SELECTED_RSTP_STREAM} -acodec libmp3lame \
-b:a 320k -ac ${CHANNELS} -content_type 'audio/mpeg' \
${FREQSHIFT_OPT} \
-f mp3 icecast://source:${ICE_PWD}@localhost:8000/stream -re
else
ffmpeg -nostdin -loglevel 32 -ac ${CHANNELS} -f alsa -i ${REC_CARD} -acodec libmp3lame \
ffmpeg -nostdin -loglevel $LOGGING_LEVEL -ac ${CHANNELS} -f alsa -i ${REC_CARD} -acodec libmp3lame \
-b:a 320k -ac ${CHANNELS} -content_type 'audio/mpeg' \
${FREQSHIFT_OPT} \
-f mp3 icecast://source:${ICE_PWD}@localhost:8000/stream -re
fi
+297 -21
View File
@@ -2,6 +2,7 @@
error_reporting(E_ERROR);
ini_set('display_errors',1);
ini_set('session.gc_maxlifetime', 7200);
ini_set('user_agent', 'PHP_Flickr/1.0');
session_set_cookie_params(7200);
session_start();
$myDate = date('Y-m-d');
@@ -23,6 +24,55 @@ $user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
$statement2 = $db->prepare('SELECT COUNT(*) FROM detections WHERE Date == DATE(\'now\', \'localtime\')');
if($statement2 == False) {
echo "Database is busy";
header("refresh: 0;");
}
$result2 = $statement2->execute();
$todaycount = $result2->fetchArray(SQLITE3_ASSOC);
if(isset($_GET['custom_image'])){
if(isset($config["CUSTOM_IMAGE"])) {
?>
<br>
<h3><?php echo $config["CUSTOM_IMAGE_TITLE"]; ?></h3>
<?php
$image_data = file_get_contents($config["CUSTOM_IMAGE"]);
$image_base64 = base64_encode($image_data);
$img_tag = "<img src='data:image/png;base64," . $image_base64 . "'>";
echo $img_tag;
}
die();
}
if(isset($_GET['blacklistimage'])) {
if(isset($_SERVER['PHP_AUTH_USER'])) {
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
$submitteduser = $_SERVER['PHP_AUTH_USER'];
if($submittedpwd == $config['CADDY_PWD'] && $submitteduser == 'birdnet'){
$imageid = $_GET['blacklistimage'];
$file_handle = fopen($home."/BirdNET-Pi/scripts/blacklisted_images.txt", 'a+');
fwrite($file_handle, $imageid . "\n");
fclose($file_handle);
unset($_SESSION['images']);
die("OK");
} else {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo 'You must be authenticated.';
exit;
}
} else {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo 'You must be authenticated.';
exit;
}
}
if(isset($_GET['fetch_chart_string']) && $_GET['fetch_chart_string'] == "true") {
$myDate = date('Y-m-d');
$chart = "Combo-$myDate.png";
@@ -32,7 +82,7 @@ if(isset($_GET['fetch_chart_string']) && $_GET['fetch_chart_string'] == "true")
if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isset($_GET['previous_detection_identifier'])) {
$statement4 = $db->prepare('SELECT Com_Name, Sci_Name, Date, Time, Confidence, File_Name FROM detections ORDER BY Date DESC, Time DESC LIMIT 5');
$statement4 = $db->prepare('SELECT Com_Name, Sci_Name, Date, Time, Confidence, File_Name FROM detections ORDER BY Date DESC, Time DESC LIMIT 15');
if($statement4 == False) {
echo "Database is busy";
header("refresh: 0;");
@@ -43,6 +93,7 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
}
$iterations = 0;
$lines;
$licenses_urls = array();
// hopefully one of the 5 most recent detections has an image that is valid, we'll use that one as the most recent detection until the newer ones get their images created
while($mostrecent = $result4->fetchArray(SQLITE3_ASSOC)) {
$comname = preg_replace('/ /', '_', $mostrecent['Com_Name']);
@@ -81,6 +132,19 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
if($key !== false) {
$image = $_SESSION['images'][$key];
} else {
// Get license information if we haven't already
if (empty($licenses_urls)) {
$licenses_url = "https://api.flickr.com/services/rest/?method=flickr.photos.licenses.getInfo&api_key=".$config["FLICKR_API_KEY"]."&format=json&nojsoncallback=1";
$licenses_response = file_get_contents($licenses_url);
$licenses_data = json_decode($licenses_response, true)["licenses"]["license"];
foreach ($licenses_data as $license) {
$license_id = $license["id"];
$license_name = $license["name"];
$license_url = $license["url"];
$licenses_urls[$license_id] = $license_url;
}
}
// only open the file once per script execution
if(!isset($lines)) {
$lines = file($home."/BirdNET-Pi/model/labels_flickr.txt");
@@ -93,11 +157,30 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
}
}
$flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=".$config["FLICKR_API_KEY"]."&text=".str_replace(" ", "%20", $engname).$comnameprefix."&sort=relevance".$args."&per_page=5&media=photos&format=json&nojsoncallback=1"), true)["photos"]["photo"][0];
$modaltext = "https://flickr.com/photos/".$flickrjson["owner"]."/".$flickrjson["id"];
$authorlink = "https://flickr.com/people/".$flickrjson["owner"];
$imageurl = 'https://farm' .$flickrjson["farm"]. '.static.flickr.com/' .$flickrjson["server"]. '/' .$flickrjson["id"]. '_' .$flickrjson["secret"]. '.jpg';
array_push($_SESSION['images'], array($comname,$imageurl,$flickrjson["title"], $modaltext, $authorlink));
// Read the blacklisted image ids from the file into an array
$blacklisted_ids = array_map('trim', file($home."/BirdNET-Pi/scripts/blacklisted_images.txt"));
// Make the API call
$flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=".$config["FLICKR_API_KEY"]."&text=".str_replace(" ", "%20", $engname).$comnameprefix."&sort=relevance".$args."&per_page=5&media=photos&format=json&nojsoncallback=1"), true)["photos"]["photo"];
// Find the first photo that is not blacklisted or is not the specific blacklisted id
$photo = null;
foreach ($flickrjson as $flickrphoto) {
if ($flickrphoto["id"] !== "4892923285" && !in_array($flickrphoto["id"], $blacklisted_ids)) {
$photo = $flickrphoto;
break;
}
}
$license_url = "https://api.flickr.com/services/rest/?method=flickr.photos.getInfo&api_key=".$config["FLICKR_API_KEY"]."&photo_id=".$photo["id"]."&format=json&nojsoncallback=1";
$license_response = file_get_contents($license_url);
$license_info = json_decode($license_response, true)["photo"]["license"];
$license_url = $licenses_urls[$license_info];
$modaltext = "https://flickr.com/photos/".$photo["owner"]."/".$photo["id"];
$authorlink = "https://flickr.com/people/".$photo["owner"];
$imageurl = 'https://farm' .$photo["farm"]. '.static.flickr.com/' .$photo["server"]. '/' .$photo["id"]. '_' .$photo["secret"]. '.jpg';
array_push($_SESSION['images'], array($comname,$imageurl,$photo["title"], $modaltext, $authorlink, $license_url));
$image = $_SESSION['images'][count($_SESSION['images'])-1];
}
}
@@ -127,12 +210,12 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
<td class="relative"><a target="_blank" href="index.php?filename=<?php echo $mostrecent['File_Name']; ?>"><img class="copyimage" title="Open in new tab" width="25" height="25" src="images/copy.png"></a>
<div class="centered_image_container" style="margin-bottom: 0px !important;">
<?php if(!empty($config["FLICKR_API_KEY"]) && strlen($image[2]) > 0) { ?>
<img onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>")' src="<?php echo $image[1]; ?>" class="img1">
<img onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>", "<?php echo $image[5]; ?>")' src="<?php echo $image[1]; ?>" class="img1">
<?php } ?>
<form action="" method="GET">
<input type="hidden" name="view" value="Species Stats">
<button type="submit" name="species" value="<?php echo $mostrecent['Com_Name'];?>"><?php echo $mostrecent['Com_Name'];?></button></br>
<a href="https://wikipedia.org/wiki/<?php echo $sciname;?>" target="_blank"/><i><?php echo $mostrecent['Sci_Name'];?></i></a>
<button type="submit" name="species" value="<?php echo $mostrecent['Com_Name'];?>"><?php echo $mostrecent['Com_Name'];?></button><img style="width: unset !important;display: inline;height: 1em;cursor:pointer" title="View species stats" onclick="generateMiniGraph(this, '<?php echo $comname; ?>')" width=25 src="images/chart.svg"><br>
<a href="https://wikipedia.org/wiki/<?php echo $sciname;?>" target="_blank"><i><?php echo $mostrecent['Sci_Name'];?></i></a>
<br>Confidence: <?php echo $percent = round((float)round($mostrecent['Confidence'],2) * 100 ) . '%';?><br></div><br>
<video style="margin-top:10px" onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster="<?php echo $filename.".png";?>" preload="none" title="<?php echo $filename;?>"><source src="<?php echo $filename;?>"></video></td>
</form>
@@ -141,7 +224,11 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
}
}
if($iterations == 0) {
echo "<h3>No Detections For Today.</h3>";
if($todaycount['COUNT(*)'] > 0) {
echo "<h3>Your system is currently processing a backlog of audio. This can take several hours before normal functionality of your BirdNET-Pi resumes.</h3>";
} else {
echo "<h3>No Detections For Today.</h3>";
}
}
die();
}
@@ -156,14 +243,6 @@ if($statement == False) {
$result = $statement->execute();
$totalcount = $result->fetchArray(SQLITE3_ASSOC);
$statement2 = $db->prepare('SELECT COUNT(*) FROM detections WHERE Date == DATE(\'now\', \'localtime\')');
if($statement2 == False) {
echo "Database is busy";
header("refresh: 0;");
}
$result2 = $statement2->execute();
$todaycount = $result2->fetchArray(SQLITE3_ASSOC);
$statement3 = $db->prepare('SELECT COUNT(*) FROM detections WHERE Date == Date(\'now\', \'localtime\') AND TIME >= TIME(\'now\', \'localtime\', \'-1 hour\')');
if($statement3 == False) {
echo "Database is busy";
@@ -230,13 +309,18 @@ body::-webkit-scrollbar {
</style>
</head>
<div class="overview">
<dialog id="attribution-dialog">
<dialog style="margin-top: 5px;max-height: 95vh;
overflow-y: auto;overscroll-behavior:contain" id="attribution-dialog">
<h1 id="modalHeading"></h1>
<p id="modalText"></p>
<button onclick="hideDialog()">Close</button>
<button style="font-weight:bold;color:blue" onclick="if(confirm('Are you sure you want to blacklist this image?')) { blacklistImage(); }">Blacklist this image</button>
</dialog>
<script src="static/dialog-polyfill.js"></script>
<script src="static/Chart.bundle.js"></script>
<script src="static/chartjs-plugin-trendline.min.js"></script>
<script>
var last_photo_link;
var dialog = document.querySelector('dialog');
dialogPolyfill.registerDialog(dialog);
@@ -248,9 +332,25 @@ body::-webkit-scrollbar {
document.getElementById('attribution-dialog').close();
}
function setModalText(iter, title, text, authorlink, photolink) {
function blacklistImage() {
const match = last_photo_link.match(/\d+$/); // match one or more digits
const result = match ? match[0] : null; // extract the first match or return null if no match is found
console.log(last_photo_link)
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
if(this.responseText.length > 0) {
location.reload();
}
}
xhttp.open("GET", "overview.php?blacklistimage="+result, true);
xhttp.send();
}
function setModalText(iter, title, text, authorlink, photolink, licenseurl) {
document.getElementById('modalHeading').innerHTML = "Photo: \""+decodeURIComponent(title.replaceAll("+"," "))+"\" Attribution";
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px' src='"+photolink+"'></div><br><div>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a></div>";
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px;max-height: calc(100vh - 15rem);display: block;margin: 0 auto;' src='"+photolink+"'></div><br><div style='white-space:nowrap'>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a><br>License URL: <a href="+licenseurl+" target='_blank'>"+licenseurl+"</a></div>";
last_photo_link = text;
showDialog();
}
</script>
@@ -282,8 +382,12 @@ if (file_exists('./Charts/'.$chart)) {
$refresh = $config['RECORDING_LENGTH'];
$time = time();
echo "<img id=\"spectrogramimage\" src=\"/spectrogram.png?nocache=$time\">";
?>
<div id="customimage"></div>
<br>
</div>
</div>
@@ -360,5 +464,177 @@ window.addEventListener("load", function(){
window.setInterval(function(){
document.getElementById("spectrogramimage").src = "/spectrogram.png?nocache="+Date.now();
}, <?php echo $refresh; ?>*1000);
<?php if(isset($config["CUSTOM_IMAGE"]) && strlen($config["CUSTOM_IMAGE"]) > 2){?>
// every 1 second, this loop will run and refresh the custom image
window.setInterval(function(){
// Find the customimage element
var customimage = document.getElementById("customimage");
function updateCustomImage() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "overview.php?custom_image=true", true);
xhr.onload = function() {
customimage.innerHTML = xhr.responseText;
}
xhr.send();
}
updateCustomImage();
}, 1000);
<?php } ?>
</script>
<style>
.tooltip {
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
padding: 10px;
transition: opacity 0.2s ease-in-out;
}
</style>
<script>
function generateMiniGraph(elem, comname) {
// Make an AJAX call to fetch the number of detections for the bird species
var xhr = new XMLHttpRequest();
xhr.open('GET', '/todays_detections.php?comname=' + comname);
xhr.onload = function() {
if (xhr.status === 200) {
var detections = JSON.parse(xhr.responseText);
// Create a div element for the chart window
if (typeof(window.chartWindow) != 'undefined') {
document.body.removeChild(window.chartWindow);
window.chartWindow = undefined;
}
var chartWindow = document.createElement('div');
chartWindow.className = "chartdiv"
chartWindow.style.position = 'fixed';
chartWindow.style.top = '0%';
chartWindow.style.left = '50%';
chartWindow.style.width = window.innerWidth < 700 ? '40%' : '20%';
chartWindow.style.height = window.innerWidth < 700 ? '25%' : '16%';
chartWindow.style.backgroundColor = '#fff';
chartWindow.style.zIndex = '9999';
chartWindow.style.overflow = 'auto';
chartWindow.style.borderRadius = '5px';
chartWindow.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
document.body.appendChild(chartWindow);
// Create a canvas element for the chart
var canvas = document.createElement('canvas');
canvas.width = chartWindow.offsetWidth;
canvas.height = chartWindow.offsetHeight;
chartWindow.appendChild(canvas);
// Create a new Chart.js chart
var ctx = canvas.getContext('2d');
var chart = new Chart(ctx, {
type: 'line',
data: {
labels: detections.map(item => item.date),
datasets: [{
label: 'Detections',
data: detections.map(item => item.count),
backgroundColor: '#9fe29b',
borderColor: '#77c487',
borderWidth: 1,
lineTension: 0.3, // Add smoothing to the line
pointRadius: 1, // Make the data points smaller
pointHitRadius: 10, // Increase the area around data points for mouse events
trendlineLinear: {
style: "rgba(55, 99, 64, 0.5)",
lineStyle: "solid",
width: 1.5
}
}]
},
options: {
layout: {
padding: {
right: 10
}
},
title: {
display: true,
text: 'Detections Over 30d'
},
legend: {
display: false
},
scales: {
xAxes: [{
display: false,
gridLines: {
display: false // Hide the gridlines on the x-axis
},
ticks: {
autoSkip: true,
maxTicksLimit: 2
}
}],
yAxes: [{
gridLines: {
display: false // Hide the gridlines on the y-axis
},
ticks: {
beginAtZero: true,
precision: 0,
stepSize: 1
}
}]
}
}
});
var buttonRect = elem.getBoundingClientRect();
var chartRect = chartWindow.getBoundingClientRect();
if (window.innerWidth < 700) {
chartWindow.style.left = 'calc(75% - ' + (chartRect.width / 2) + 'px)';
} else {
chartWindow.style.left = (buttonRect.right + 10) + 'px';
}
// Calculate the top position of the chart to center it with the button
var buttonCenter = buttonRect.top + (buttonRect.height / 2);
var chartHeight = chartWindow.offsetHeight;
var chartTop = buttonCenter - (chartHeight / 2);
chartWindow.style.top = chartTop + 'px';
// Add a close button to the chart window
var closeButton = document.createElement('button');
closeButton.id = "chartcb";
closeButton.innerText = 'X';
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '5px';
closeButton.addEventListener('click', function() {
document.body.removeChild(chartWindow);
window.chartWindow = undefined;
});
chartWindow.appendChild(closeButton);
window.chartWindow = chartWindow;
}
};
xhr.send();
}
// Listen for the scroll event on the window object
window.addEventListener('scroll', function() {
// Get all chart elements
var charts = document.querySelectorAll('.chartdiv');
// Loop through all chart elements and remove them
charts.forEach(function(chart) {
chart.parentNode.removeChild(chart);
window.chartWindow = undefined;
});
});
</script>
+148 -50
View File
@@ -1,4 +1,9 @@
<?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);
@@ -101,6 +106,31 @@ $shifted_path = $home."/BirdSongs/Extracted/By_Date/shifted/";
if(isset($_GET['shiftfile'])) {
if (file_exists('./scripts/thisrun.txt')) {
$config = parse_ini_file('./scripts/thisrun.txt');
} elseif (file_exists('./scripts/firstrun.ini')) {
$config = parse_ini_file('./scripts/firstrun.ini');
}
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
$caddypwd = $config['CADDY_PWD'];
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo '<table><tr><td>You cannot shift files for this installation</td></tr></table>';
exit;
} else {
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
$submitteduser = $_SERVER['PHP_AUTH_USER'];
if($submittedpwd !== $caddypwd || $submitteduser !== 'birdnet'){
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo '<table><tr><td>You cannot shift files for this installation<</td></tr></table>';
exit;
}
}
$filename = $_GET['shiftfile'];
$pp = pathinfo($filename);
$dir = $pp['dirname'];
@@ -109,27 +139,22 @@ if(isset($_GET['shiftfile'])) {
$pi = $home."/BirdSongs/Extracted/By_Date/";
if(isset($_GET['doshift'])) {
$freqshift_tool = $config['FREQSHIFT_TOOL'];
$freqshift_tool = $config['FREQSHIFT_TOOL'];
if ($freqshift_tool == "ffmpeg") {
$cmd = "/usr/bin/nohup /usr/bin/ffmpeg -y -i \"".$pi.$filename."\" -af \"rubberband=pitch=".$config['FREQSHIFT_LO']."/".$config['FREQSHIFT_HI']."\" \"".$shifted_path.$filename."\"";
shell_exec("mkdir -p ".$shifted_path.$dir." && echo \"".$cmd."\" > /tmp/shift.sh && chmod +x /tmp/shift.sh");
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 = "/usr/bin/nohup /usr/bin/sox \"".$pi.$filename."\" \"".$shifted_path.$filename."\" pitch ".$soxopt." ".$soxpitch;
shell_exec("mkdir -p ".$shifted_path.$dir." && echo \"".$cmd."\" > /tmp/shift.sh && chmod +x /tmp/shift.sh");
}
shell_exec("/tmp/shift.sh");
shell_exec("rm -f /tmp/shift.sh");
} 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 = "rm -f " . $shifted_path.$filename;
shell_exec("echo \"".$cmd."\" > /tmp/unshift.sh && chmod +x /tmp/unshift.sh");
shell_exec("/tmp/unshift.sh");
shell_exec("rm -f /tmp/unshift.sh");
$cmd = "sudo rm -f " . escapeshellarg($shifted_path.$filename);
shell_exec($cmd);
}
echo "OK";
@@ -236,7 +261,7 @@ function toggleLock(filename, type, elem) {
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("add","del"));
} else {
elem.setAttribute("src","images/unlock.svg");
elem.setAttribute("title", "This file is not excluded from being purged.");
elem.setAttribute("title", "This file will be deleted when disk space needs to be freed.");
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("del","add"));
}
}
@@ -259,21 +284,31 @@ function toggleShiftFreq(filename, shiftAction, elem) {
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")[0];
video.setAttribute("title", video.getAttribute("title").replace("/By_Date/","/By_Date/shifted/"));
source = video.getElementsByTagName("source")[0];
source.setAttribute("src", source.getAttribute("src").replace("/By_Date/","/By_Date/shifted/"));
video.load();
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")[0];
video.setAttribute("title", video.getAttribute("title").replace("/By_Date/shifted/","/By_Date/"));
source = video.getElementsByTagName("source")[0];
source.setAttribute("src", source.getAttribute("src").replace("/By_Date/shifted/","/By_Date/"));
video.load();
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/"));
}
}
}
}
@@ -325,22 +360,76 @@ if(!isset($_GET['species']) && !isset($_GET['filename'])){
#By Species
} elseif($view == "byspecies") {
while($results=$result->fetchArray(SQLITE3_ASSOC)){
$birds = array();
while($results=$result->fetchArray(SQLITE3_ASSOC))
{
$name = $results['Com_Name'];
echo "<td>
<button action=\"submit\" name=\"species\" value=\"$name\">$name</button></td></tr>";}
#Specific Date
} elseif($view == "date") {
while($results=$result->fetchArray(SQLITE3_ASSOC)){
$name = $results['Com_Name'];
if(realpath($home."/BirdSongs/Extracted/By_Date/".$date."/".str_replace(" ", "_",$name)) !== false){
echo "<td>
<button action=\"submit\" name=\"species\" value=\"$name\">$name</button></td></tr>";
}
$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'];
if(realpath($home."/BirdSongs/Extracted/By_Date/".$date."/".str_replace(" ", "_",$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>
@@ -440,7 +529,7 @@ echo "<table>
if($config["FULL_DISK"] == "purge") {
if(!in_array($filename_formatted, $disk_check_exclude_arr)) {
$imageicon = "images/unlock.svg";
$title = "This file is not excluded from being purged.";
$title = "This file will be deleted when disk space needs to be freed (>95% usage).";
$type = "add";
} else {
$imageicon = "images/lock.svg";
@@ -452,7 +541,7 @@ echo "<table>
$shiftImageIcon = "images/unshift.svg";
$shiftTitle = "This file has been shifted down in frequency.";
$shiftAction = "unshift";
$filename = $filename_shifted;
$filename = $filename_shifted;
} else {
$shiftImageIcon = "images/shift.svg";
$shiftTitle = "This file is not shifted in frequency.";
@@ -460,7 +549,7 @@ echo "<table>
}
echo "<tr>
<td class=\"relative\">
<td class=\"relative\">
<img style='cursor:pointer;right:90px' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\")' class=\"copyimage\" width=25 title='Delete Detection'>
<img style='cursor:pointer;right:45px' onclick='toggleLock(\"".$filename_formatted."\",\"".$type."\", this)' class=\"copyimage\" width=25 title=\"".$title."\" src=\"".$imageicon."\">
@@ -471,7 +560,7 @@ echo "<table>
</tr>";
} else {
echo "<tr>
<td class=\"relative\">$date $time<br>$confidence
<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>
@@ -515,7 +604,7 @@ echo "<table>
if($config["FULL_DISK"] == "purge") {
if(!in_array($filename_formatted, $disk_check_exclude_arr)) {
$imageicon = "images/unlock.svg";
$title = "This file is not excluded from being purged.";
$title = "This file will be deleted when disk space needs to be freed (>95% usage).";
$type = "add";
} else {
$imageicon = "images/lock.svg";
@@ -527,7 +616,7 @@ echo "<table>
$shiftImageIcon = "images/unshift.svg";
$shiftTitle = "This file has been shifted down in frequency.";
$shiftAction = "unshift";
$filename = $filename_shifted;
$filename = $filename_shifted;
} else {
$shiftImageIcon = "images/shift.svg";
$shiftTitle = "This file is not shifted in frequency.";
@@ -535,7 +624,7 @@ echo "<table>
}
echo "<tr>
<td class=\"relative\">
<td class=\"relative\">
<img style='cursor:pointer;right:90px' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\", true)' class=\"copyimage\" width=25 title='Delete Detection'>
<img style='cursor:pointer;right:45px' onclick='toggleLock(\"".$filename_formatted."\",\"".$type."\", this)' class=\"copyimage\" width=25 title=\"".$title."\" src=\"".$imageicon."\">
@@ -545,7 +634,7 @@ echo "<table>
</tr>";
} else {
echo "<tr>
<td class=\"relative\">$date $time<br>$confidence
<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>";
@@ -553,4 +642,13 @@ echo "<table>
}echo "</table>";}?>
</div>
<style>
td.spec {
width: calc(100% / <?php echo $num_cols;?>);
}
tr:first-child td.spec {
padding-top: 10px;
}
</style>
</html>
+107 -6
View File
@@ -11,6 +11,8 @@ import sqlite3
from sqlite3 import Connection
import plotly.express as px
from sklearn.preprocessing import normalize
from suntime import Sun
from datetime import datetime
pio.templates.default = "plotly_white"
@@ -38,13 +40,13 @@ st.markdown("""
""", unsafe_allow_html=True)
@st.cache(hash_funcs={Connection: id})
# @st.cache(allow_output_mutation=True)
@st.cache_resource()
def get_connection(path: str):
return sqlite3.connect(path, check_same_thread=False)
def get_data(conn: Connection):
@st.cache_resource()
def get_data(_conn: Connection):
df1 = pd.read_sql("SELECT * FROM detections", con=conn)
return df1
@@ -89,7 +91,7 @@ else:
# end_date = datetime(2022 ,5 ,17).date()
@st.cache()
@st.cache_data()
def date_filter(df, start_date, end_date):
filt = (df2.index >= pd.Timestamp(start_date)) & (df2.index <= pd.Timestamp(end_date + timedelta(days=1)))
df = df[filt]
@@ -132,7 +134,7 @@ else:
resample_time = resample_times[resample_sel]
@st.cache()
@st.cache_data()
def time_resample(df, resample_time):
if resample_time == 'Raw':
df_resample = df['Com_Name']
@@ -168,6 +170,68 @@ top_N_species = (df5.value_counts()[:top_N])
font_size = 15
def sunrise_sunset_scatter(num_days_to_display):
latitude = df['Lat'][0]
longitude = df['Lon'][0]
sun = Sun(latitude, longitude)
sunrise_list = []
sunset_list = []
sunrise_week_list = []
sunset_week_list = []
sunrise_text_list = []
sunset_text_list = []
now = datetime.now()
for past_day in range(num_days_to_display):
d = timedelta(days=num_days_to_display - past_day - 1)
current_date = now - d
# current_date = datetime.fromisocalendar(2022, week + 1, 5)
# time_zone = datetime.now()
sun_rise = sun.get_local_sunrise_time(current_date)
sun_dusk = sun.get_local_sunset_time(current_date)
sun_rise_time = float(sun_rise.hour) + float(sun_rise.minute) / 60.0
sun_dusk_time = float(sun_dusk.hour) + float(sun_dusk.minute) / 60.0
temp_time = str(sun_rise)[-14:-9] + " Sunrise"
sunrise_text_list.append(temp_time)
temp_time = str(sun_dusk)[-14:-9] + " Sunset"
sunset_text_list.append(temp_time)
sunrise_list.append(sun_rise_time)
sunset_list.append(sun_dusk_time)
sunrise_week_list.append(past_day)
sunset_week_list.append(past_day)
sunrise_week_list.append(None)
sunrise_list.append(None)
sunrise_text_list.append(None)
sun_list = sunrise_list.extend(sunset_list)
sun_week_list = sunrise_week_list.extend(sunset_week_list)
sunrise_text_list.extend(sunset_text_list)
return sunrise_week_list, sunrise_list, sunrise_text_list
def hms_to_dec(t):
# (h, m, s) = t.split(':')
h = t.hour
m = t.minute / 60
s = t.second / 3600
result = h + m + s
return result
def hms_to_str(t):
# (h, m, s) = t.split(':')
h = t.hour
m = t.minute
# s = t.second / 3600
# result = h + m + s
return "%02d:%02d" % (h, m)
if daily is False:
if resample_time != '1D':
@@ -364,8 +428,11 @@ if daily is False:
df4.index = [df4.index.date, df4.index.time]
day_hour_freq = df4.unstack().fillna(0)
saved_time_labels = [hms_to_str(h) for h in day_hour_freq.columns.tolist()]
fig_dec_y = [hms_to_dec(h) for h in day_hour_freq.columns.tolist()]
fig_x = [d.strftime('%d-%m-%Y') for d in day_hour_freq.index.tolist()]
fig_y = [h.strftime('%H:%M') for h in day_hour_freq.columns.tolist()]
day_hour_freq.columns = fig_dec_y
fig_z = day_hour_freq.values.transpose()
# fig_heatmap = go.Figure(data=go.Heatmap(x=fig_x,y=fig_y,z=fig_z))
@@ -374,7 +441,41 @@ if daily is False:
# yaxis={'categoryorder':'total ascending'})
color_pals = px.colors.named_colorscales()
selected_pal = st.sidebar.selectbox('Select Color Pallet for Daily Detections', color_pals)
fig.add_trace(go.Heatmap(x=fig_x, y=fig_y, z=fig_z, autocolorscale=False, colorscale=selected_pal), row=1, col=1)
heatmap = go.Heatmap(
# x=fig_x, y=fig_y,
x=fig_x,
y=day_hour_freq.columns,
z=fig_z, # heat.values,
showscale=False,
# text=labels,
texttemplate="%{text}", autocolorscale=False, colorscale=selected_pal
)
num_days_to_display = len(fig_x)
sunrise_week_list, sunrise_list, sunrise_text_list = sunrise_sunset_scatter(num_days_to_display)
daysback_range = fig_x
daysback_range.append(None)
daysback_range.extend(daysback_range)
daysback_range = daysback_range[:-1]
sunrise_sunset = go.Scatter(x=daysback_range,
y=sunrise_list,
mode='lines',
hoverinfo='text',
text=sunrise_text_list,
line_color='orange', line_width=1, name=' ')
fig = go.Figure(data=[heatmap, sunrise_sunset])
number_of_y_ticks = 12
y_downscale_factor = int(len(saved_time_labels) / number_of_y_ticks)
fig.update_layout(
yaxis=dict(
tickmode = 'array',
tickvals = day_hour_freq.columns[::y_downscale_factor],
ticktext = saved_time_labels[::y_downscale_factor],
nticks = 6
)
)
st.plotly_chart(fig, use_container_width=True) # , config=config)
else:
fig = make_subplots(
+3
View File
@@ -9,6 +9,9 @@ for service in "${services[@]}"; do
echo ""
done
echo "========= Syslog snippet =========="
tail -n 100 /var/log/syslog
# 2. Mounted file systems
echo "========== Mounted File Systems =========="
df -h
+2 -1
View File
@@ -13,7 +13,8 @@ services=(chart_viewer.service
icecast2.service
extraction.service
birdnet_recording.service
birdnet_log.service)
birdnet_log.service
birdnet_stats.service)
for i in "${services[@]}";do
sudo systemctl restart "${i}"
+37 -9
View File
@@ -1,3 +1,4 @@
import re
from pathlib import Path
from tzlocal import get_localzone
import datetime
@@ -12,6 +13,7 @@ import operator
import socket
import threading
import os
import gzip
from utils.notifications import sendAppriseNotifications
from utils.parse_settings import config_to_settings
@@ -70,7 +72,7 @@ def loadModel():
print('LOADING TF LITE MODEL...', end=' ')
# Load TFLite model and allocate tensors.
# model will either be BirdNET_GLOBAL_3K_V2.2_Model_FP16 (new) or BirdNET_6K_GLOBAL_MODEL (old)
# model will either be BirdNET_GLOBAL_3K_V2.3_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()
@@ -104,7 +106,7 @@ def loadMetaModel():
global M_OUTPUT_LAYER_INDEX
# Load TFLite model and allocate tensors.
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.2_MData_Model_FP16.tflite')
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.3_MData_Model_FP16.tflite')
M_INTERPRETER.allocate_tensors()
# Get input and output tensors.
@@ -276,7 +278,7 @@ def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,):
start = time.time()
print('ANALYZING AUDIO...', end=' ', flush=True)
if model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16":
if model == "BirdNET_GLOBAL_3K_V2.3_Model_FP16":
if len(PREDICTED_SPECIES_LIST) == 0 or len(INCLUDE_LIST) != 0:
predictSpeciesList(lat, lon, week)
@@ -402,16 +404,40 @@ def handle_client(conn, addr):
birdweather_id = args.birdweather_id
# Read audio data
audioData = readAudioData(args.i, args.overlap)
# Read audio data & handle errors
try:
audioData = readAudioData(args.i, args.overlap)
except (NameError, TypeError) as e:
print(f"Error with the following info: {e}")
open('~/BirdNET-Pi/analyzing_now.txt', 'w').close()
finally:
pass
# Get Date/Time from filename in case Pi gets behind
# now = datetime.now()
full_file_name = args.i
# print('FULL FILENAME: -' + full_file_name + '-')
file_name = Path(full_file_name).stem
# Get the RSTP stream identifier from the filename if it exists
RTSP_ident_for_fn = ""
RTSP_ident = re.search("RTSP_[0-9]+-", file_name)
if RTSP_ident is not None:
RTSP_ident_for_fn = RTSP_ident.group()
# Find and remove the identifier for the RSTP stream url it was from that is added when more than one
# RSTP stream is recorded simultaneously, in order to make the filenames unique as filenames are all
# generated at the same time
file_name = re.sub("RTSP_[0-9]+-", "", file_name)
# Now we can read the date and time as normal
# First portion of the filename contaning the date in Y m d
file_date = file_name.split('-birdnet-')[0]
# Second portion of the filename containing the time in H:M:S
file_time = file_name.split('-birdnet-')[1]
# Join the date and time together to get a complete string representing when the audio was recorded
date_time_str = file_date + ' ' + file_time
date_time_obj = datetime.datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S')
# print('Date:', date_time_obj.date())
@@ -471,7 +497,7 @@ def handle_client(conn, addr):
Overlap = str(args.overlap)
Com_Name = Com_Name.replace("'", "")
File_Name = Com_Name.replace(" ", "_") + '-' + Confidence + '-' + \
Date.replace("/", "-") + '-birdnet-' + Time + audiofmt
Date.replace("/", "-") + '-birdnet-' + RTSP_ident_for_fn + Time + audiofmt
# Connect to SQLite Database
for attempt_number in range(3):
@@ -543,7 +569,9 @@ def handle_client(conn, addr):
with open(args.i, 'rb') as f:
wav_data = f.read()
response = requests.post(url=soundscape_url, data=wav_data, headers={'Content-Type': 'application/octet-stream'})
gzip_wav_data = gzip.compress(wav_data)
response = requests.post(url=soundscape_url, data=gzip_wav_data, headers={'Content-Type': 'application/octet-stream',
'Content-Encoding': 'gzip'})
print("Soundscape POST Response Status - ", response.status_code)
sdata = response.json()
soundscape_id = sdata['soundscape']['id']
@@ -565,8 +593,8 @@ def handle_client(conn, addr):
post_commonName = "\"commonName\": \"" + entry[0].split('_')[1].split("/")[0] + "\","
post_scientificName = "\"scientificName\": \"" + entry[0].split('_')[0] + "\","
if model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16":
post_algorithm = "\"algorithm\": " + "\"2p2\"" + ","
if model == "BirdNET_GLOBAL_3K_V2.3_Model_FP16":
post_algorithm = "\"algorithm\": " + "\"2p3\"" + ","
else:
post_algorithm = "\"algorithm\": " + "\"alpha\"" + ","
+4 -4
View File
@@ -25,10 +25,10 @@ function service_status($name) {
<div class="servicecontrols">
<form action="" method="GET">
<h3>Live Audio Stream <?php echo service_status("livestream.service");?></h3>
<button type="submit" name="submit" value="sudo systemctl stop livestream.service && sudo /etc/init.d/icecast2 stop">Stop</button>
<button type="submit" name="submit" value="sudo systemctl restart livestream.service && sudo /etc/init.d/icecast2 restart">Restart </button>
<button type="submit" name="submit" value="sudo systemctl disable --now livestream.service && sudo systemctl disable icecast2 && sudo /etc/init.d/icecast2 stop">Disable</button>
<button type="submit" name="submit" value="sudo systemctl enable icecast2 && sudo /etc/init.d/icecast2 start && sudo systemctl enable --now livestream.service">Enable</button>
<button type="submit" name="submit" value="sudo systemctl stop livestream.service && sudo systemctl stop icecast2.service">Stop</button>
<button type="submit" name="submit" value="sudo systemctl restart livestream.service && sudo systemctl restart icecast2.service">Restart </button>
<button type="submit" name="submit" value="sudo systemctl disable --now livestream.service && sudo systemctl disable icecast2 && sudo systemctl stop icecast2.service">Disable</button>
<button type="submit" name="submit" value="sudo systemctl enable icecast2 && sudo systemctl start icecast2.service && sudo systemctl enable --now livestream.service">Enable</button>
</form>
<form action="" method="GET">
<h3>Web Terminal <?php echo service_status("web_terminal.service");?></h3>
+1 -1
View File
@@ -17,7 +17,7 @@ def loadMetaModel():
global CLASSES
# Load TFLite model and allocate tensors.
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.2_MData_Model_FP16.tflite')
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.3_MData_Model_FP16.tflite')
M_INTERPRETER.allocate_tensors()
# Get input and output tensors.
+246 -12
View File
@@ -1,30 +1,84 @@
<?php
error_reporting(E_ERROR);
ini_set('display_errors',1);
if(isset($_GET['ajax_csv'])) {
if (file_exists('./scripts/thisrun.txt')) {
$config = parse_ini_file('./scripts/thisrun.txt');
$config = parse_ini_file('./scripts/thisrun.txt');
} elseif (file_exists('./scripts/firstrun.ini')) {
$config = parse_ini_file('./scripts/firstrun.ini');
$config = parse_ini_file('./scripts/firstrun.ini');
}
if(!empty($config['FREQSHIFT_RECONNECT_DELAY']) && is_numeric($config['FREQSHIFT_RECONNECT_DELAY'])){
$FREQSHIFT_RECONNECT_DELAY = ($config['FREQSHIFT_RECONNECT_DELAY']);
}else{
$FREQSHIFT_RECONNECT_DELAY = 4000;
}
if(isset($_GET['ajax_csv'])) {
$RECS_DIR = $config["RECS_DIR"];
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
$files = scandir($RECS_DIR."/".date('F-Y')."/".date('d-l')."/", SCANDIR_SORT_ASCENDING);
//Replace variables to Fix up the file paths just in case the ENV environment variables don't resolve via PHP
$RECS_DIR = str_replace("\$HOME", $home, $RECS_DIR);
$STREAM_DATA_DIR = $RECS_DIR . "/StreamData/";
//Try find the latest file out of the soundcard recording folder
$look_in_directory = $RECS_DIR."/".date('F-Y')."/".date('d-l')."/";
$files = scandir($look_in_directory, SCANDIR_SORT_ASCENDING);
//Extract the filename, positions 0 and 1 are the folder hierarchy '.' and '..'
$newest_file = $files[2];
//Couldn't find under e.g /home/pi/April-2023/03-Monday/ (where USB audio streams are recorded
//look in the StreamData directory (RTSP streams are recorded here)
if (empty($files) || array_key_exists(2, $files) === false) {
$look_in_directory = $STREAM_DATA_DIR;
//Load the file in the directory
$files = scandir($look_in_directory, SCANDIR_SORT_ASCENDING);
//Because there might be more than 1 stream, we can't really assume the file at index 2 is the latest, or even for the stream being listened to
//Read the RTSP_STREAM_TO_LIVESTREAM setting, then try to find that CSV file
if(!empty($config['RTSP_STREAM_TO_LIVESTREAM']) && is_numeric($config['RTSP_STREAM_TO_LIVESTREAM'])){
//The stored setting of RTSP_STREAM_TO_LIVESTREAM is 0 based, but filenames are 1's based, so just add 1 to the config value
//so we can match up the stream the user is listening to with the appropriate filename
$RTSP_STREAM_LISTENED_TO = ($config['RTSP_STREAM_TO_LIVESTREAM'] + 1);
}else{
//Setting is invalid somehow
//The stored setting of RTSP_STREAM_TO_LIVESTREAM is 0 based, but filenames are 1's based, so just add 1 to the config value
//This will be the first stream
$RTSP_STREAM_LISTENED_TO = 1;
}
//The RTSP streams contain 'RTSP_X' in the filename, were X is the stream url index in the comma separated list of RTSP streams
//We can use this to locate the file for this stream
foreach ($files as $file_idx => $stream_file_name) {
//Skip the folder hierarchy entries
if ($stream_file_name != "." && $stream_file_name != "..") {
//See if the filename contains the correct RTSP name, also only check .wav files by excluding the csv files with the filter .wav.csv
if (stripos($stream_file_name, 'RTSP_' . $RTSP_STREAM_LISTENED_TO) !== false && stripos($stream_file_name, '.wav.csv') === false) {
//Found a match - set it as the newest file
$newest_file = $stream_file_name;
}
}
}
}
//If the newest file param has been supplied and it's the same as the newest file found
//then stop processing
if($newest_file == $_GET['newest_file']) {
die();
}
//Print out the filename
echo "file,".$newest_file."\n";
//Print out the detected birds as CSV
$row = 1;
if (($handle = fopen($RECS_DIR."/".date('F-Y')."/".date('d-l')."/".$newest_file.".csv", "r")) !== FALSE) {
if (($handle = fopen($look_in_directory . $newest_file . ".csv", "r")) !== FALSE) {
while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
if($row != 1){
$num = count($data);
@@ -37,8 +91,29 @@ if (($handle = fopen($RECS_DIR."/".date('F-Y')."/".date('d-l')."/".$newest_file.
}
fclose($handle);
}
//Kill the script so no further processing or output is done
die();
}
//Hold the array of RTSP steams once they are exploded
$RTSP_Stream_Config = array();
//Load the birdnet config so we can read the RTSP setting
// Valid config data
if (is_array($config) && array_key_exists('RTSP_STREAM',$config)) {
if (is_null($config['RTSP_STREAM']) === false && $config['RTSP_STREAM'] !== "") {
$RTSP_Stream_Config_Data = explode(",", $config['RTSP_STREAM']);
//Process the stream further
//we need to able to ID it (just do this by position), get the hostname to show in the dropdown box
foreach ($RTSP_Stream_Config_Data as $stream_idx => $stream_url) {
//$stream_idx is the array position of the the RSP stream URL, idx of 0 is the first, 1 - second etc
$RTSP_stream_url = parse_url($stream_url);
$RTSP_Stream_Config[$stream_idx] = $RTSP_stream_url['host'];
}
}
}
?>
<script>
// CREDITS: https://codepen.io/jakealbaugh/pen/jvQweW
@@ -139,21 +214,27 @@ function applyText(text,x,y,opacity) {
var add =0;
var newest_file;
var newest_file_tmp;
var lastbird;
function loadDetectionIfNewExists() {
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
// if there's a new detection that needs to be updated to the page
if(this.responseText.length > 0 && !this.responseText.includes("Database")) {
// Case insensitive match for RTSP_X
var RTSP_regex = /RTSP_[0-9]+-/i;
var RTSP_regex_match = false;
var split = this.responseText.split("\n")
for(var i = 1;i < split.length; i++) {
if(parseInt(split[i].split(",")[0]) >= 0){
newest_file = split[0].split(",")[1]
newest_file_tmp = newest_file;//Copy the file name var so we something to work with
//Remove the string that denotes the RTSP stream from the filename to make things easier, it's not really needed
RTSP_regex_match = RTSP_regex.test(newest_file_tmp)
newest_file_tmp = newest_file_tmp.replace(RTSP_regex, "")
//applyText(split[i].split(",")[1],document.body.querySelector('canvas').width - ((parseInt(split[i].split(",")[0]))*avgfps), (document.body.querySelector('canvas').height * 0.50))
d1 = new Date(newest_file.split("-")[0]+"/"+newest_file.split("-")[1]+"/"+newest_file.split("-")[2]+ " "+newest_file.split("-")[4].replace(".wav",""))
d1 = new Date(newest_file_tmp.split("-")[0]+"/"+newest_file_tmp.split("-")[1]+"/"+newest_file_tmp.split("-")[2]+ " "+newest_file_tmp.split("-")[4].replace(".wav",""))
console.log("d1 "+d1)
d2 = new Date(xhttp.getResponseHeader("Date"));
console.log("d2 "+d2)
@@ -173,7 +254,13 @@ function loadDetectionIfNewExists() {
console.log(add)
// Date csv file was created + relative detection time of bird + mic delay
secago = Math.abs(timeDiff) - split[i].split(",")[0] - 6.8;
//If we can't find the regex in the filename, then the RTSP_X string doesn't exist, unlikely to be a RTSP stream
if (RTSP_regex_match == false){
secago = (Math.abs(timeDiff) - split[i].split(",")[0]) - 6.8;
}else{
//half the delay for RTSP streams ~ just a rough guess
secago = (Math.abs(timeDiff) - split[i].split(",")[0]) - 4;
}
x = document.body.querySelector('canvas').width - ((parseInt(secago))*avgfps);
// if the text is too close to the right side of the canvas and will be cut off, wait 3 seconds before adding text
@@ -235,6 +322,48 @@ function toggleCompression(state) {
}
}
function toggleFreqshift(state) {
if (state == true) {
console.log("freqshift activated")
} else {
console.log("freqshift deactivated")
}
freqShiftReconnectDelay = <?php echo $FREQSHIFT_RECONNECT_DELAY; ?>;
var livestream_freqshift_spinner = document.getElementById('livestream_freqshift_spinner');
livestream_freqshift_spinner.style.display = "inline";
// Create the XMLHttpRequest object.
const xhr = new XMLHttpRequest();
// Initialize the request
xhr.open("GET", './views.php?activate_freqshift_in_livestream=' + state + '&view=Advanced&submit=advanced');
// Send the request
xhr.send();
// Fired once the request completes successfully
xhr.onload = function (e) {
// Check if the request was a success
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
// Restart the audio player in case it stopped working while the livestream service was restarted
var audio_player = document.querySelector('audio#player');
if (audio_player !== 'undefined') {
//central_controls_element.appendChild(h1_loading);
//Wait 2 seconds before restarting the stream
setTimeout(function () {
console.log("Restarting connection with livestream");
audio_player.pause();
audio_player.setAttribute('src', '/stream');
audio_player.load();
audio_player.play();
livestream_freqshift_spinner.style.display = "none";
},
freqShiftReconnectDelay
)
}
}
}
}
function initialize() {
document.body.querySelector('h1').remove();
const CVS = document.body.querySelector('canvas');
@@ -273,6 +402,7 @@ function initialize() {
gainNode.connect(ACTX.destination);
document.getElementById("compression").removeAttribute("disabled");
document.getElementById("freqshift").removeAttribute("disabled");
console.log(SOURCE);
const DATA = new Uint8Array(ANALYSER.frequencyBinCount);
@@ -340,6 +470,46 @@ h1 {
<img id="spectrogramimage" style="width:100%;height:100%;display:none" src="/spectrogram.png?nocache=<?php echo $time;?>">
<div class="centered">
<?php
if (isset($RTSP_Stream_Config) && !empty($RTSP_Stream_Config)) {
?>
<div style="display:inline" id="RTSP_streams">
<label>RTSP Stream: </label>
<select id="rtsp_stream_select" name="RTSP Streams">
<?php
//The setting representing which livestream to stream is more than the number of RTSP streams available
//maybe the list of streams has been modified
//This isn't the ideal for this, but needed a way to fix this setting without calling the advanced setting page
if (array_key_exists($config['RTSP_STREAM_TO_LIVESTREAM'], $RTSP_Stream_Config) === false) {
$contents = file_get_contents('/etc/birdnet/birdnet.conf');
$contents2 = file_get_contents('./scripts/thisrun.txt');
$contents = preg_replace("/RTSP_STREAM_TO_LIVESTREAM=.*/", "RTSP_STREAM_TO_LIVESTREAM=\"0\"", $contents);
$contents2 = preg_replace("/RTSP_STREAM_TO_LIVESTREAM=.*/", "RTSP_STREAM_TO_LIVESTREAM=\"0\"", $contents2);
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
fwrite($fh2, $contents2);
exec("sudo systemctl restart livestream.service");
}
//Print out the dropdown list for the RTSP streams
foreach ($RTSP_Stream_Config as $stream_id => $stream_host) {
$isSelected = "";
//Match up the selected value saved in config so we can preselect it
if ($config['RTSP_STREAM_TO_LIVESTREAM'] == $stream_id) {
$isSelected = 'selected="selected"';
}
//Create the select option
echo "<option value=" . $stream_id . " $isSelected >" . $stream_host . "</option>";
}
?>
</select>
</div>
&mdash;
<?php
}
?>
<div style="display:inline" id="gain" >
<label>Gain: </label>
<span class="slidecontainer">
@@ -347,18 +517,77 @@ h1 {
<span id="gain_value"></span>%
</span>
</div>
&mdash;
<div style="display:inline" id="comp" >
<label>Compression: </label>
<label>Compression: </label>
<input name="compression" type="checkbox" id="compression" disabled>
</div>
<div style="display:inline" id="fshift" >
<label>Freq shift: </label>
<?php
if ($config['ACTIVATE_FREQSHIFT_IN_LIVESTREAM'] == "true") {
$freqshift_state = "checked";
} else {
$freqshift_state = "";
}
?>
<input name="freqshift" type="checkbox" id="freqshift" <?php echo($freqshift_state); ?> disabled>
<img id="livestream_freqshift_spinner" src=images/spinner.gif style="height: 25px; vertical-align: top; display: none">
</div>
</div>
<audio style="display:none" controls="" crossorigin="anonymous" id='player' preload="none"><source id="playersrc" src="/stream"></audio>
<h1>Loading...</h1>
<h1 id="loading-h1">Loading...</h1>
<canvas></canvas>
<script>
var rtsp_stream_select = document.getElementById("rtsp_stream_select");
if (typeof (rtsp_stream_select) !== 'undefined' && rtsp_stream_select !== null) {
//When the dropdown selection is changed set the new value is settings, then restart the livestream service so it broadcasts newly selected RTSP stream
rtsp_stream_select.onchange = function () {
if (this.value !== 'undefined') {
// Get the audio player element
var audio_player = document.querySelector('audio#player');
var central_controls_element = document.getElementsByClassName('centered')[0];
//Create the loading header again as a placeholder while we're waiting to reload the stream
var h1_loading = document.createElement("H1");
var h1_loading_text = document.createTextNode("Loading...");
h1_loading.setAttribute("id", "loading-h1");
h1_loading.setAttribute("style", "font-size:48px; font-weight: bolder; color: #FFF");
h1_loading.appendChild(h1_loading_text);
// Create the XMLHttpRequest object.
const xhr = new XMLHttpRequest();
// Initialize the request
xhr.open("GET", './views.php?rtsp_stream_to_livestream=' + this.value + '&view=Advanced&submit=advanced');
// Send the request
xhr.send();
// Fired once the request completes successfully
xhr.onload = function (e) {
// Check if the request was a success
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
// Restart the audio player in case it stopped working while the livestream service was restarted
if (audio_player !== 'undefined') {
central_controls_element.appendChild(h1_loading);
//Wait 5 seconds before restarting the stream
setTimeout(function () {
audio_player.pause();
audio_player.setAttribute('src', '/stream');
audio_player.load();
audio_player.play();
document.getElementById('loading-h1').remove()
},
10000
)
}
}
}
}
}
}
var slider = document.getElementById("gain_input");
var output = document.getElementById("gain_value");
output.innerHTML = slider.value; // Display the default slider value
@@ -374,4 +603,9 @@ var compression = document.getElementById("compression");
compression.onclick = function() {
toggleCompression(this.checked);
}
var freqshift = document.getElementById("freqshift");
freqshift.onclick = function() {
toggleFreqshift(this.checked);
}
</script>
+26 -3
View File
@@ -1,6 +1,29 @@
#!/usr/bin/env bash
# Make sox spectrogram
source /etc/birdnet/birdnet.conf
analyzing_now="$(cat $HOME/BirdNET-Pi/analyzing_now.txt)"
spectrogram_png=${EXTRACTED}/spectrogram.png
sox -V1 "${analyzing_now}" -n remix 1 rate 24k spectrogram -c "${analyzing_now//$HOME\/}" -o "${spectrogram_png}"
# Read the logging level from the configuration option
LOGGING_LEVEL="${LogLevel_SpectrogramViewerService}"
# If empty for some reason default to log level of error
[ -z $LOGGING_LEVEL ] && LOGGING_LEVEL='error'
# Additionally if we're at debug or info level then allow printing of script commands and variables
if [ "$LOGGING_LEVEL" == "info" ] || [ "$LOGGING_LEVEL" == "debug" ];then
# Enable printing of commands/variables etc to terminal for debugging
set -x
fi
# Time to sleep between generating spectrogram's, default set the recording length
# To try catch the spectrogram as soon as possible run at a smaller intervals
SLEEP_DELAY=$((RECORDING_LENGTH / 4))
# Continuously loop generating a spectrogram every 10 seconds
while true; do
analyzing_now="$(cat $HOME/BirdNET-Pi/analyzing_now.txt)"
if [ ! -z "${analyzing_now}" ] && [ -f "${analyzing_now}" ]; then
spectrogram_png=${EXTRACTED}/spectrogram.png
sox -V1 "${analyzing_now}" -n remix 1 rate 24k spectrogram -c "${analyzing_now//$HOME\//}" -o "${spectrogram_png}"
fi
sleep $SLEEP_DELAY
done
+61 -21
View File
@@ -1,4 +1,10 @@
<?php
/* Prevent XSS input */
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
ini_set('user_agent', 'PHP_Flickr/1.0');
error_reporting(0);
ini_set('display_errors', 0);
@@ -87,28 +93,62 @@ if(!file_exists($home."/BirdNET-Pi/scripts/disk_check_exclude.txt") || strpos(fi
</button>
</form>
</div>
<table>
<?php
while($results=$result2->fetchArray(SQLITE3_ASSOC))
{
$comname = preg_replace('/ /', '_', $results['Com_Name']);
$comname = preg_replace('/\'/', '', $comname);
$filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
?>
<tr>
<form action="" method="GET">
<td><input type="hidden" name="sort" value="<?php if(isset($_GET['sort'])){echo $_GET['sort'];}?>">
<input type="hidden" name="view" value="Species Stats">
<button type="submit" name="species" value="<?php echo $results['Com_Name'];?>"><?php echo $results['Com_Name'];?></button>
</td>
<?php
}
?>
</form>
</tr>
<table style="padding-top:10px">
<?php
$birds = array();
while($results=$result2->fetchArray(SQLITE3_ASSOC))
{
$comname = preg_replace('/ /', '_', $results['Com_Name']);
$comname = preg_replace('/\'/', '', $comname);
$filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
$birds[] = $results['Com_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>
<form action="" method="GET">
<input type="hidden" name="sort" value="<?php if(isset($_GET['sort'])){echo $_GET['sort'];}?>">
<input type="hidden" name="view" value="Species Stats">
<button type="submit" name="species" value="<?php echo $birds[$index];?>"><?php echo $birds[$index];?></button>
</form>
</td>
<?php
} else {
echo "<td></td>";
}
}
echo "</tr>";
}
?>
</table>
<style>
td {
padding: 0px;
width: calc(100% / <?php echo $num_cols;?>);
}
tr:first-child td {
padding-top: 10px;
}
</style>
</div>
<dialog id="attribution-dialog">
<dialog style="margin-top: 5px;max-height: 95vh;
overflow-y: auto;overscroll-behavior:contain" id="attribution-dialog">
<h1 id="modalHeading"></h1>
<p id="modalText"></p>
<button onclick="hideDialog()">Close</button>
@@ -128,7 +168,7 @@ function hideDialog() {
function setModalText(iter, title, text, authorlink) {
document.getElementById('modalHeading').innerHTML = "Photo "+iter+": \""+title+"\" Attribution";
document.getElementById('modalText').innerHTML = "Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a>";
document.getElementById('modalText').innerHTML = "<div style='white-space:nowrap'>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a></div>";
showDialog();
}
</script>
+14
View File
@@ -10,6 +10,12 @@ if (preg_match("/behind '.*?' by (\d+) commit(s?)\b/", $str, $matches)) {
$num_commits_behind = $matches[1];
$_SESSION['behind'] = $num_commits_behind;
}
if (preg_match('/\b(\d+)\b and \b(\d+)\b different commits each/', $str, $matches)) {
$num1 = (int) $matches[1];
$num2 = (int) $matches[2];
$sum = $num1 + $num2;
$_SESSION['behind'] = $sum;
}
?><html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<br>
@@ -38,4 +44,12 @@ function update() {
<form action="" method="GET">
<button type="submit" name="submit" value="sudo clear_all_data.sh" onclick="return confirm('Clear ALL Data? Note that this cannot be undone and will take up to 90 seconds.')">Clear ALL data</button>
</form>
<?php
$cmd="cd ".$home."/BirdNET-Pi && sudo -u ".$user." git rev-list --max-count=1 HEAD";
$curr_hash = shell_exec($cmd);
?>
<p style="font-size:11px;text-align:center"></br></br>Running version: </p>
<a href="https://github.com/mcguirepr89/BirdNET-Pi/commit/<?php echo $curr_hash; ?>" target="_blank">
<p style="font-size:11px;text-align:center;box-sizing: border-box"><?php echo $curr_hash; ?></p>
</a>
</div>
+323 -23
View File
@@ -1,5 +1,11 @@
<?php
/* Prevent XSS input */
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
ini_set('session.gc_maxlifetime', 7200);
ini_set('user_agent', 'PHP_Flickr/1.0');
session_set_cookie_params(7200);
session_start();
error_reporting(E_ERROR);
@@ -80,6 +86,52 @@ $user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
if(isset($_GET['comname'])) {
$birdName = $_GET['comname'];
$birdName = str_replace("_", " ", $birdName);
// Prepare a SQL statement to retrieve the detection data for the specified bird
$stmt = $db->prepare('SELECT Date, COUNT(*) AS Detections FROM detections WHERE Com_Name = :com_name AND Date BETWEEN DATE("now", "-30 days") AND DATE("now") GROUP BY Date');
// Bind the bird name parameter to the SQL statement
$stmt->bindValue(':com_name', $birdName);
// Execute the SQL statement and get the result set
$result = $stmt->execute();
// Fetch the result set as an associative array
$data = array();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$data[$row['Date']] = $row['Detections'];
}
// Create an array of all dates in the last 14 days
$last14Days = array();
for ($i = 0; $i < 31; $i++) {
$last14Days[] = date('Y-m-d', strtotime("-$i days"));
}
// Merge the data array with the last14Days array
$data = array_merge(array_fill_keys($last14Days, 0), $data);
// Sort the data by date in ascending order
ksort($data);
// Convert the data to an array of objects
$data = array_map(function($date, $count) {
return array('date' => $date, 'count' => $count);
}, array_keys($data), $data);
// Close the database connection
$db->close();
// Return the data as JSON
echo json_encode($data);
die();
}
// from https://stackoverflow.com/questions/2690504/php-producing-relative-date-time-from-timestamps
function relativeTime($ts)
{
@@ -166,7 +218,8 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
$_SESSION['images'] = [];
}
$iterations = 0;
$lines;
$lines=null;
$licenses_urls = array();
if (file_exists('./scripts/thisrun.txt')) {
$config = parse_ini_file('./scripts/thisrun.txt');
@@ -175,7 +228,6 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
}
while($todaytable=$result0->fetchArray(SQLITE3_ASSOC))
{
$iterations++;
@@ -183,6 +235,7 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
$comname = preg_replace('/ /', '_', $todaytable['Com_Name']);
$comname = preg_replace('/\'/', '_', $comname);
$filename = "/By_Date/".date('Y-m-d')."/".$comname."/".$todaytable['File_Name'];
$filename_formatted = $todaytable['Date']."/".$comname."/".$todaytable['File_Name'];
$sciname = preg_replace('/ /', '_', $todaytable['Sci_Name']);
$args = "&license=2%2C3%2C4%2C5%2C6%2C9&orientation=square,portrait";
$comnameprefix = "%20bird";
@@ -207,6 +260,19 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
if($key !== false) {
$image = $_SESSION['images'][$key];
} else {
// Get license information if we haven't already
if (empty($licenses_urls)) {
$licenses_url = "https://api.flickr.com/services/rest/?method=flickr.photos.licenses.getInfo&api_key=".$config["FLICKR_API_KEY"]."&format=json&nojsoncallback=1";
$licenses_response = file_get_contents($licenses_url);
$licenses_data = json_decode($licenses_response, true)["licenses"]["license"];
foreach ($licenses_data as $license) {
$license_id = $license["id"];
$license_name = $license["name"];
$license_url = $license["url"];
$licenses_urls[$license_id] = $license_url;
}
}
// only open the file once per script execution
if(!isset($lines)) {
$lines = file($home."/BirdNET-Pi/model/labels_flickr.txt");
@@ -218,48 +284,88 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
break;
}
}
$flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=".$config["FLICKR_API_KEY"]."&text=".str_replace(" ", "%20", $engname).$comnameprefix."&sort=relevance".$args."&per_page=5&media=photos&format=json&nojsoncallback=1"), true)["photos"]["photo"][0];
$modaltext = "https://flickr.com/photos/".$flickrjson["owner"]."/".$flickrjson["id"];
$authorlink = "https://flickr.com/people/".$flickrjson["owner"];
$imageurl = 'https://farm' .$flickrjson["farm"]. '.static.flickr.com/' .$flickrjson["server"]. '/' .$flickrjson["id"]. '_' .$flickrjson["secret"]. '.jpg';
array_push($_SESSION['images'], array($comname,$imageurl,$flickrjson["title"], $modaltext, $authorlink));
// Read the blacklisted image ids from the file into an array
$blacklisted_ids = array_map('trim', file($home."/BirdNET-Pi/scripts/blacklisted_images.txt"));
// Make the API call
$flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=".$config["FLICKR_API_KEY"]."&text=".str_replace(" ", "%20", $engname).$comnameprefix."&sort=relevance".$args."&per_page=5&media=photos&format=json&nojsoncallback=1"), true)["photos"]["photo"];
// Find the first photo that is not blacklisted or is not the specific blacklisted id
$photo = null;
foreach ($flickrjson as $flickrphoto) {
if ($flickrphoto["id"] !== "4892923285" && !in_array($flickrphoto["id"], $blacklisted_ids)) {
$photo = $flickrphoto;
break;
}
}
$license_url = "https://api.flickr.com/services/rest/?method=flickr.photos.getInfo&api_key=".$config["FLICKR_API_KEY"]."&photo_id=".$photo["id"]."&format=json&nojsoncallback=1";
$license_response = file_get_contents($license_url);
$license_info = json_decode($license_response, true)["photo"]["license"];
$license_url = $licenses_urls[$license_info];
$modaltext = "https://flickr.com/photos/".$photo["owner"]."/".$photo["id"];
$authorlink = "https://flickr.com/people/".$photo["owner"];
$imageurl = 'https://farm' .$photo["farm"]. '.static.flickr.com/' .$photo["server"]. '/' .$photo["id"]. '_' .$photo["secret"]. '.jpg';
array_push($_SESSION['images'], array($comname,$imageurl,$photo["title"], $modaltext, $authorlink, $license_url));
$image = $_SESSION['images'][count($_SESSION['images'])-1];
}
}
?>
<?php if(isset($_GET['display_limit']) && is_numeric($_GET['display_limit'])){ ?>
<tr class="relative" id="<?php echo $iterations; ?>">
<td class="relative"><a target="_blank" href="index.php?filename=<?php echo $todaytable['File_Name']; ?>"><img class="copyimage" title="Open in new tab" width=25 src="images/copy.png"></a>
<td class="relative">
<img style='cursor:pointer;right:45px' src='images/delete.svg' onclick='deleteDetection("<?php echo $filename_formatted; ?>")' class="copyimage" width=25 title='Delete Detection'>
<a target="_blank" href="index.php?filename=<?php echo $todaytable['File_Name']; ?>"><img class="copyimage" title="Open in new tab" width=25 src="images/copy.png"></a>
<div class="centered_image_container">
<?php if(!empty($config["FLICKR_API_KEY"]) && strlen($image[2]) > 0) { ?>
<img onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>")' src="<?php echo $image[1]; ?>" class="img1">
<img onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>", "<?php echo $image[5]; ?>")' src="<?php echo $image[1]; ?>" class="img1">
<?php } ?>
<?php echo $todaytable['Time'];?><br>
<b><a class="a2" href="https://allaboutbirds.org/guide/<?php echo $comname;?>" target="top"><?php echo $todaytable['Com_Name'];?></a></b><br>
<b><a class="a2" href="https://allaboutbirds.org/guide/<?php echo $comname;?>" target="top"><?php echo $todaytable['Com_Name'];?></a></b><img style="height: 1em;cursor:pointer" title="View species stats" onclick="generateMiniGraph(this, '<?php echo $comname; ?>')" width=25 src="images/chart.svg"><br>
<a class="a2" href="https://wikipedia.org/wiki/<?php echo $sciname;?>" target="top"><i><?php echo $todaytable['Sci_Name'];?></i></a><br>
<b>Confidence:</b> <?php echo round((float)round($todaytable['Confidence'],2) * 100 ) . '%';?><br></div><br>
<video onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster="<?php echo $filename.".png";?>" preload="none" title="<?php echo $filename;?>"><source preload="none" src="<?php echo $filename;?>"></video>
</td>
<?php } else { //legacy mode ?>
<tr class="relative" id="<?php echo $iterations; ?>">
<td><?php if($_GET['kiosk'] == true) { echo relativeTime(strtotime($todaytable['Time'])); } else {echo $todaytable['Time'];}?><br></td><td id="recent_detection_middle_td">
<td><?php if($_GET['kiosk'] == true) { echo relativeTime(strtotime($todaytable['Time'])); } else {echo $todaytable['Time'];}?><br></td>
<td id="recent_detection_middle_td">
<div>
<div>
<?php if(!empty($config["FLICKR_API_KEY"]) && (isset($_GET['hard_limit']) || $_GET['kiosk'] == true) && strlen($image[2]) > 0) { ?>
<img style="float:left;height:75px;" onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>")' src="<?php echo $image[1]; ?>" id="birdimage" class="img1">
<img style="float:left;height:75px;" onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>", "<?php echo $image[5]; ?>")' src="<?php echo $image[1]; ?>" id="birdimage" class="img1">
<?php } ?>
</div>
<div>
<b><a class="a2" <?php if($_GET['kiosk'] == false){?>href="https://allaboutbirds.org/guide/<?php echo $comname;?>"<?php } else {echo "style='color:blue;'";} ?> target="top"><?php echo $todaytable['Com_Name'];?></a></b><br>
<a class="a2" <?php if($_GET['kiosk'] == false){?>href="https://wikipedia.org/wiki/<?php echo $sciname;?>"<?php } else {echo "style='color:blue;'";} ?> target="top"><i><?php echo $todaytable['Sci_Name'];?></i></a><br></td>
</div></div>
<b><a class="a2" <?php if($_GET['kiosk'] == false){?>href="https://allaboutbirds.org/guide/<?php echo $comname;?>"<?php } else {echo "style='color:blue;'";} ?> target="top"><?php echo $todaytable['Com_Name'];?></a></b>
<?php
//If on mobile, add in a icon to link off to the recording so the user can see more info
if (isset($_GET['mobile'])) {
?>
<br>
<img style="height: 1em;cursor:pointer;float:unset;display:inline" title="View species stats" onclick="generateMiniGraph(this, '<?php echo $comname; ?>')" width=25 src="images/chart.svg">
<a target="_blank" href="index.php?filename=<?php echo $todaytable['File_Name']; ?>"><img style="height: 1em;cursor:pointer;float:unset;display:inline" class="copyimage-mobile" title="Open in new tab" width=16 src="images/copy.png"></a>'
<?php
}else{
//Else just put the species stats icon
?>
<img style="height: 1em;cursor:pointer;float:unset;display:inline" title="View species stats" onclick="generateMiniGraph(this, '<?php echo $comname; ?>')" width=25 src="images/chart.svg">
<?php
}
?>
<br>
<a class="a2" <?php if($_GET['kiosk'] == false){?>href="https://wikipedia.org/wiki/<?php echo $sciname;?>"<?php } else {echo "style='color:blue;'";} ?> target="top"><i><?php echo $todaytable['Sci_Name'];?></i></a><br>
</div>
</div>
</td>
<td><b>Confidence:</b> <?php echo round((float)round($todaytable['Confidence'],2) * 100 ) . '%';?><br></td>
<?php if(!isset($_GET['mobile'])) { ?>
<td style="min-width:180px"><audio controls preload="none" title="<?php echo $filename;?>"><source preload="none" src="<?php echo $filename;?>"></video>
<td style="min-width:180px"><audio controls preload="none" title="<?php echo $filename;?>"><source preload="none" src="<?php echo $filename;?>"></audio></td>
<?php } ?>
</td>
<?php } ?>
<?php }?>
</tr>
@@ -321,13 +427,38 @@ die();
</style>
</head>
<div class="viewdb">
<dialog id="attribution-dialog">
<dialog style="margin-top: 5px;max-height: 95vh;
overflow-y: auto;overscroll-behavior:contain" id="attribution-dialog">
<h1 id="modalHeading"></h1>
<p id="modalText"></p>
<button style="background-color: #9fe29b;padding:20px" onclick="hideDialog()">Close</button>
<button style="font-weight:bold;color:blue" onclick="hideDialog()">Close</button>
<button style="font-weight:bold;color:blue" onclick="if(confirm('Are you sure you want to blacklist this image?')) { blacklistImage(); }">Blacklist this image</button>
</dialog>
<script src="static/dialog-polyfill.js"></script>
<script src="static/Chart.bundle.js"></script>
<script src="static/chartjs-plugin-trendline.min.js"></script>
<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("Database busy.")
}
}
xhttp.open("GET", "play.php?deletefile="+filename, true);
xhttp.send();
}
}
var last_photo_link;
var dialog = document.querySelector('dialog');
dialogPolyfill.registerDialog(dialog);
@@ -339,13 +470,29 @@ die();
document.getElementById('attribution-dialog').close();
}
function setModalText(iter, title, text, authorlink, photolink) {
function blacklistImage() {
const match = last_photo_link.match(/\d+$/); // match one or more digits
const result = match ? match[0] : null; // extract the first match or return null if no match is found
console.log(last_photo_link)
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
if(this.responseText.length > 0) {
location.reload();
}
}
xhttp.open("GET", "overview.php?blacklistimage="+result, true);
xhttp.send();
}
function setModalText(iter, title, text, authorlink, photolink, licenseurl) {
document.getElementById('modalHeading').innerHTML = "Photo: \""+decodeURIComponent(title.replaceAll("+"," "))+"\" Attribution";
<?php if($kiosk == false) { ?>
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px' src='"+photolink+"'></div><br><div>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a></div>";
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px;max-height: calc(100vh - 15rem);display: block;margin: 0 auto;' src='"+photolink+"'></div><br><div style='white-space:nowrap'>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a><br>License URL: <a href="+licenseurl+" target='_blank'>"+licenseurl+"</a></div>";
<?php } else { ?>
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px' src='"+photolink+"'></div><br><div>Image link: <a target='_blank'>"+text+"</a><br>Author link: <a target='_blank'>"+authorlink+"</a></div>";
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px;max-height: calc(100vh - 15rem);display: block;margin: 0 auto;' src='"+photolink+"'></div><br><div style='white-space:nowrap'>Image link: <a target='_blank'>"+text+"</a><br>Author link: <a target='_blank'>"+authorlink+"</a><br>License URL: <a target='_blank'>"+licenseurl+"</a></div>";
<?php } ?>
last_photo_link = text;
showDialog();
}
</script>
@@ -450,7 +597,9 @@ function searchDetections(searchvalue) {
function loadDetections(detections_limit, element=undefined) {
const xhttp = new XMLHttpRequest();
xhttp.onload = function() {
document.getElementsByClassName("legacyview")[0].style.display="unset";
<?php if($kiosk == false) { ?>
document.getElementsByClassName("legacyview")[0].style.display="unset";
<?php } ?>
if(typeof element !== "undefined")
{
element.remove();
@@ -497,3 +646,154 @@ window.addEventListener("load", function(){
});
</script>
<style>
.tooltip {
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
padding: 10px;
transition: opacity 0.2s ease-in-out;
}
</style>
<script>
function generateMiniGraph(elem, comname) {
// Make an AJAX call to fetch the number of detections for the bird species
var xhr = new XMLHttpRequest();
xhr.open('GET', '/todays_detections.php?comname=' + comname);
xhr.onload = function() {
if (xhr.status === 200) {
var detections = JSON.parse(xhr.responseText);
// Create a div element for the chart window
if (typeof(window.chartWindow) != 'undefined') {
document.body.removeChild(window.chartWindow);
}
var chartWindow = document.createElement('div');
chartWindow.className = "chartdiv"
chartWindow.style.position = 'fixed';
chartWindow.style.top = '0%';
chartWindow.style.left = '50%';
chartWindow.style.width = window.innerWidth < 700 ? '40%' : '20%';
chartWindow.style.height = window.innerWidth < 700 ? '25%' : '16%';
chartWindow.style.backgroundColor = '#fff';
chartWindow.style.zIndex = '9999';
chartWindow.style.overflow = 'auto';
chartWindow.style.borderRadius = '5px';
chartWindow.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
document.body.appendChild(chartWindow);
// Create a canvas element for the chart
var canvas = document.createElement('canvas');
canvas.width = chartWindow.offsetWidth;
canvas.height = chartWindow.offsetHeight;
chartWindow.appendChild(canvas);
// Create a new Chart.js chart
var ctx = canvas.getContext('2d');
var chart = new Chart(ctx, {
type: 'line',
data: {
labels: detections.map(item => item.date),
datasets: [{
label: 'Detections',
data: detections.map(item => item.count),
backgroundColor: '#9fe29b',
borderColor: '#77c487',
borderWidth: 1,
lineTension: 0.3, // Add smoothing to the line
pointRadius: 1, // Make the data points smaller
pointHitRadius: 10, // Increase the area around data points for mouse events
trendlineLinear: {
style: "rgba(55, 99, 64, 0.5)",
lineStyle: "solid",
width: 1.5
}
}]
},
options: {
layout: {
padding: {
right: 10
}
},
title: {
display: true,
text: 'Detections Over 30d'
},
legend: {
display: false
},
scales: {
xAxes: [{
display: false,
gridLines: {
display: false // Hide the gridlines on the x-axis
},
ticks: {
autoSkip: true,
maxTicksLimit: 2
}
}],
yAxes: [{
gridLines: {
display: false // Hide the gridlines on the y-axis
},
ticks: {
beginAtZero: true,
precision: 0,
stepSize: 1
}
}]
}
}
});
// Position the chart window to the right of the button
var buttonRect = elem.getBoundingClientRect();
var chartRect = chartWindow.getBoundingClientRect();
if (window.innerWidth < 700) {
chartWindow.style.left = 'calc(75% - ' + (chartRect.width / 2) + 'px)';
} else {
chartWindow.style.left = (buttonRect.right + 10) + 'px';
}
// Calculate the top position of the chart to center it with the button
var buttonCenter = buttonRect.top + (buttonRect.height / 2);
var chartHeight = chartWindow.offsetHeight;
var chartTop = buttonCenter - (chartHeight / 2);
chartWindow.style.top = chartTop + 'px';
// Add a close button to the chart window
var closeButton = document.createElement('button');
closeButton.id = "chartcb";
closeButton.innerText = 'X';
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '5px';
closeButton.addEventListener('click', function() {
document.body.removeChild(chartWindow);
});
chartWindow.appendChild(closeButton);
window.chartWindow = chartWindow;
}
};
xhr.send();
}
// Listen for the scroll event on the window object
window.addEventListener('scroll', function() {
// Get all chart elements
var charts = document.querySelectorAll('.chartdiv');
// Loop through all chart elements and remove them
charts.forEach(function(chart) {
chart.parentNode.removeChild(chart);
});
});
</script>
+6
View File
@@ -41,6 +41,9 @@ sudo_with_user () {
set +x
}
# Get current HEAD hash
commit_hash=$(sudo_with_user git -C $HOME/BirdNET-Pi rev-parse HEAD)
# Reset current HEAD to remove any local changes
sudo_with_user git -C $HOME/BirdNET-Pi reset --hard
@@ -50,6 +53,9 @@ sudo_with_user git -C $HOME/BirdNET-Pi fetch $remote $branch
# Switches git to specified branch
sudo_with_user git -C $HOME/BirdNET-Pi switch -C $branch --track $remote/$branch
# Prints out changes
sudo_with_user git -C $HOME/BirdNET-Pi diff --stat $commit_hash HEAD
sudo systemctl daemon-reload
sudo ln -sf $my_dir/* /usr/local/bin/
+75 -6
View File
@@ -7,8 +7,17 @@ HOME=$(awk -F: '/1000/ {print $6}' /etc/passwd)
my_dir=$HOME/BirdNET-Pi/scripts
# Sets proper permissions and ownership
sudo -E chown -R $USER:$USER $HOME/*
sudo chmod -R g+wr $HOME/*
#sudo -E chown -R $USER:$USER $HOME/*
#sudo chmod -R g+wr $HOME/*
find $HOME/Bird* -type f ! -perm -g+wr -exec chmod g+wr {} + 2>/dev/null
find $HOME/Bird* -not -user $USER -execdir sudo -E chown $USER:$USER {} \+
chmod 666 ~/BirdNET-Pi/scripts/*.txt
chmod 666 ~/BirdNET-Pi/*.txt
find $HOME/BirdNET-Pi -path "$HOME/BirdNET-Pi/birdnet" -prune -o -type f ! -perm /o=w -exec chmod a+w {} \;
# remove world-writable perms
chmod -R o-w ~/BirdNET-Pi/templates/*
# Create blank sitename as it's optional. First time install will use $HOSTNAME.
if ! grep SITE_NAME /etc/birdnet/birdnet.conf &>/dev/null;then
@@ -66,7 +75,7 @@ fi
apprise_installation_status=$(~/BirdNET-Pi/birdnet/bin/python3 -c 'import pkgutil; print("installed" if pkgutil.find_loader("apprise") else "not installed")')
if [[ "$apprise_installation_status" = "not installed" ]];then
$HOME/BirdNET-Pi/birdnet/bin/pip3 install -U pip
$HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise
$HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise==1.2.1
fi
[ -f $HOME/BirdNET-Pi/apprise.txt ] || sudo -E -ucaddy touch $HOME/BirdNET-Pi/apprise.txt
if ! which lsof &>/dev/null;then
@@ -125,8 +134,7 @@ if ! grep '\-\-browser.gatherUsageStats false' $HOME/BirdNET-Pi/templates/birdne
fi
# Make IceCast2 a little more secure
sudo sed -i 's|<!-- <bind-address>.*|<bind-address>127.0.0.1</bind-address>|;s|<!-- <shoutcast-mount>.*|<shoutcast-mount>/stream</shoutcast-mount>|' /etc/icecast2/icecast.xml
sudo systemctl restart icecast2
sudo sed -i.bak -e 's|<!-- <bind-address>.*|<bind-address>127.0.0.1</bind-address>|;s|<!-- <shoutcast-mount>.*|<shoutcast-mount>/stream</shoutcast-mount>|' /etc/icecast2/icecast.xml && if [ -s /etc/icecast2/icecast.xml.bak ] && ! sudo diff /etc/icecast2/icecast.xml /etc/icecast2/icecast.xml.bak > /dev/null; then sudo systemctl restart icecast2; fi
if ! grep FREQSHIFT_TOOL /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "FREQSHIFT_TOOL=sox" >> /etc/birdnet/birdnet.conf
@@ -140,6 +148,12 @@ fi
if ! grep FREQSHIFT_PITCH /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "FREQSHIFT_PITCH=-1500" >> /etc/birdnet/birdnet.conf
fi
if ! grep ACTIVATE_FREQSHIFT_IN_LIVESTREAM /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "ACTIVATE_FREQSHIFT_IN_LIVESTREAM=\"false\"" >> /etc/birdnet/birdnet.conf
fi
if ! grep FREQSHIFT_RECONNECT_DELAY /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "FREQSHIFT_RECONNECT_DELAY=4000" >> /etc/birdnet/birdnet.conf
fi
if ! grep HEARTBEAT_URL /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "HEARTBEAT_URL=" >> /etc/birdnet/birdnet.conf
fi
@@ -157,12 +171,67 @@ CREATE INDEX IF NOT EXISTS "detections_Com_Name" ON "detections" ("Com_Name");
CREATE INDEX IF NOT EXISTS "detections_Date_Time" ON "detections" ("Date" DESC, "Time" DESC);
EOF
$HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise==1.2.1 >/dev/null
apprise_version=$($HOME/BirdNET-Pi/birdnet/bin/python3 -c "import apprise; print(apprise.__version__)")
streamlit_version=$($HOME/BirdNET-Pi/birdnet/bin/pip3 show streamlit 2>/dev/null | grep Version | awk '{print $2}')
[[ $apprise_version != "1.2.1" ]] && $HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise==1.2.1
[[ $streamlit_version != "1.19.0" ]] && $HOME/BirdNET-Pi/birdnet/bin/pip3 install streamlit==1.19.0
if ! grep -q 'RuntimeMaxSec=' "$HOME/BirdNET-Pi/templates/birdnet_analysis.service"&>/dev/null; then
sudo -E sed -i '/\[Service\]/a RuntimeMaxSec=3600' "$HOME/BirdNET-Pi/templates/birdnet_analysis.service"
sudo systemctl daemon-reload && restart_services.sh
fi
if ! grep RAW_SPECTROGRAM /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "RAW_SPECTROGRAM=0" >> /etc/birdnet/birdnet.conf
fi
if ! grep CUSTOM_IMAGE /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "CUSTOM_IMAGE=" >> /etc/birdnet/birdnet.conf
fi
if ! grep CUSTOM_IMAGE_TITLE /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "CUSTOM_IMAGE_TITLE=\"\"" >> /etc/birdnet/birdnet.conf
fi
if ! grep APPRISE_ONLY_NOTIFY_SPECIES_NAMES /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "APPRISE_ONLY_NOTIFY_SPECIES_NAMES=\"\"" >> /etc/birdnet/birdnet.conf
fi
if ! grep APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=\"\"" >> /etc/birdnet/birdnet.conf
fi
if ! grep RTSP_STREAM_TO_LIVESTREAM /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "RTSP_STREAM_TO_LIVESTREAM=\"0\"" >> /etc/birdnet/birdnet.conf
fi
suntime_installation_status=$(~/BirdNET-Pi/birdnet/bin/python3 -c 'import pkgutil; print("installed" if pkgutil.find_loader("suntime") else "not installed")')
if [[ "$suntime_installation_status" = "not installed" ]];then
$HOME/BirdNET-Pi/birdnet/bin/pip3 install -U pip
$HOME/BirdNET-Pi/birdnet/bin/pip3 install suntime
fi
# For new Advanced Setting Logging level options
if ! grep LogLevel_BirdnetRecordingService /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "LogLevel_BirdnetRecordingService=\"error\"" >> /etc/birdnet/birdnet.conf
fi
if ! grep LogLevel_LiveAudioStreamService /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "LogLevel_LiveAudioStreamService=\"error\"" >> /etc/birdnet/birdnet.conf
fi
if ! grep LogLevel_SpectrogramViewerService /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "LogLevel_SpectrogramViewerService=\"error\"" >> /etc/birdnet/birdnet.conf
fi
if grep -q '^MODEL=BirdNET_GLOBAL_3K_V2.2_Model_FP16$' /etc/birdnet/birdnet.conf;then
language=$(grep "^DATABASE_LANG=" /etc/birdnet/birdnet.conf | cut -d= -f2)
sed -i 's/BirdNET_GLOBAL_3K_V2.2_Model_FP16/BirdNET_GLOBAL_3K_V2.3_Model_FP16/' /etc/birdnet/birdnet.conf
sed -i 's/BirdNET_GLOBAL_3K_V2.2_Model_FP16/BirdNET_GLOBAL_3K_V2.3_Model_FP16/' $HOME/BirdNET-Pi/scripts/thisrun.txt
sed -i 's/BirdNET_GLOBAL_3K_V2.2_Model_FP16/BirdNET_GLOBAL_3K_V2.3_Model_FP16/' $HOME/BirdNET-Pi/birdnet.conf
cp -f $HOME/BirdNET-Pi/model/labels.txt $HOME/BirdNET-Pi/model/labels.txt.old
sudo chmod +x $HOME/BirdNET-Pi/scripts/install_language_label_nm.sh && $HOME/BirdNET-Pi/scripts/install_language_label_nm.sh -l "$language"
fi
sudo systemctl daemon-reload
restart_services.sh
+16 -5
View File
@@ -50,6 +50,16 @@ def sendAppriseNotifications(species, confidence, confidencepct, path,
body = settings_dict.get('APPRISE_NOTIFICATION_BODY')
sciName, comName = species.split("_")
APPRISE_ONLY_NOTIFY_SPECIES_NAMES = settings_dict.get('APPRISE_ONLY_NOTIFY_SPECIES_NAMES')
if APPRISE_ONLY_NOTIFY_SPECIES_NAMES is not None and APPRISE_ONLY_NOTIFY_SPECIES_NAMES.strip() != "":
if any(bird.lower().replace(" ", "") in comName.lower().replace(" ", "") for bird in APPRISE_ONLY_NOTIFY_SPECIES_NAMES.split(",")):
return
APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 = settings_dict.get('APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2')
if APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 is not None and APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2.strip() != "":
if not any(bird.lower().replace(" ", "") in comName.lower().replace(" ", "") for bird in APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2.split(",")):
return
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES = settings_dict.get('APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES')
if APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES != "0":
if species_last_notified.get(comName) is not None:
@@ -75,9 +85,10 @@ def sendAppriseNotifications(species, confidence, confidencepct, path,
if comName not in flickr_images:
try:
# TODO: Make this work with non-english comnames. Implement the "// convert sci name to English name" logic from overview.php here
url = 'https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key='+str(settings_dict.get('FLICKR_API_KEY'))+'&text='+str(
comName)+' bird&sort=relevance&per_page=5&media=photos&format=json&license=2%2C3%2C4%2C5%2C6%2C9&nojsoncallback=1'
resp = requests.get(url=url)
headers = {'User-Agent': 'Python_Flickr/1.0'}
url = 'https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key='+str(settings_dict.get('FLICKR_API_KEY'))+'&text='+str(comName)+' bird&sort=relevance&per_page=5&media=photos&format=json&license=2%2C3%2C4%2C5%2C6%2C9&nojsoncallback=1'
resp = requests.get(url=url, headers=headers)
resp.encoding = "utf-8"
data = resp.json()["photos"]["photo"][0]
@@ -171,7 +182,7 @@ def sendAppriseNotifications(species, confidence, confidencepct, path,
except sqlite3.Error as e:
print(e)
print("Database busy")
time.sleep(2)
timeim.sleep(2)
if settings_dict.get('APPRISE_NOTIFY_NEW_SPECIES') == "1":
try:
@@ -220,7 +231,7 @@ def sendAppriseNotifications(species, confidence, confidencepct, path,
con.close()
except sqlite3.Error:
print("Database busy")
time.sleep(2)
timeim.sleep(2)
if __name__ == "__main__":
+16 -8
View File
@@ -100,15 +100,19 @@ if(isset($_GET['ascii'])) {
$priorweekcount = $totalcount['COUNT(*)'];
// really percent changed
$percentagediff = round( (($scount - $priorweekcount) / $priorweekcount) * 100 );
if($priorweekcount > 0){
$percentagediff = round( (($scount - $priorweekcount) / $priorweekcount) * 100 );
if($percentagediff > 0) {
$percentagediff = "<span style='color:green;font-size:small'>+".$percentagediff."%</span>";
} else {
$percentagediff = "<span style='color:red;font-size:small'>-".abs($percentagediff)."%</span>";
}
if($percentagediff > 0) {
$percentagediff = "<span style='color:green;font-size:small'>+".$percentagediff."%</span>";
} else {
$percentagediff = "<span style='color:red;font-size:small'>-".abs($percentagediff)."%</span>";
}
echo $com_name." - ".$scount." (".$percentagediff.")<br>";
echo $com_name." - ".$scount." (".$percentagediff.")<br>";
} else {
echo $com_name." - ".$scount ."<br>";
}
}
}
@@ -206,7 +210,11 @@ while($detection=$result1->fetchArray(SQLITE3_ASSOC))
$totalcount = $result2->fetchArray(SQLITE3_ASSOC);
$priorweekcount = $totalcount['COUNT(*)'];
$percentagediff = round( (($scount - $priorweekcount) / $priorweekcount) * 100 );
if ($priorweekcount > 0) {
$percentagediff = round( (($scount - $priorweekcount) / $priorweekcount) * 100 );
} else {
$percentagediff = 0;
}
if($percentagediff > 0) {
$percentagediff = "<span style='color:green;font-size:small'>+".$percentagediff."%</span>";
+1
View File
@@ -11,3 +11,4 @@ sed -i s/'^BIRDWEATHER_ID=$'/"BIRDWEATHER_ID=${birdweather_id}"/g ${birders_conf
sed -i s/'^BIRDNETPI_URL=$'/"BIRDNETPI_URL=${birdnetpi_url/\/\//\\\/\\\/}"/g ${birders_conf}
sed -i s/'^WEBTERMINAL_URL=$'/"WEBTERMINAL_URL=${extractionlog_url/\/\//\\\/\\\/}"/g ${birders_conf}
sed -i s/'^BIRDNETLOG_URL=$'/"BIRDNETLOG_URL=${birdnetlog_url/\/\//\\\/\\\/}"/g ${birders_conf}