Merge branch 'main' into python-lint
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
BirdNET-Pi <img src="https://img.shields.io/badge/Version-0.13-pink" />
|
BirdNET-Pi
|
||||||
</h1>
|
</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A realtime acoustic bird classification system for the Raspberry Pi 4B
|
A realtime acoustic bird classification system for the Raspberry Pi 4B
|
||||||
@@ -38,6 +38,7 @@ Currently listening in these countries . . . that I know of . . .
|
|||||||
- Finland
|
- Finland
|
||||||
- Australia
|
- Australia
|
||||||
- Canada
|
- Canada
|
||||||
|
- Switzerland
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* 24/7 recording and BirdNET-Lite analysis
|
* 24/7 recording and BirdNET-Lite analysis
|
||||||
|
|||||||
+11
-14
@@ -56,17 +56,11 @@ BIRDNETPI_URL=
|
|||||||
|
|
||||||
RTSP_STREAM=
|
RTSP_STREAM=
|
||||||
|
|
||||||
#------------------- Mobile Notifications via Pushed.co ---------------------#
|
#----------------------- Apprise Miscellanous Configuration -------------------#
|
||||||
#____________The two variables below enable mobile notifications_______________#
|
|
||||||
#_____________See https://pushed.co/quick-start-guide to get___________________#
|
|
||||||
#_________________________these values for your app.___________________________#
|
|
||||||
|
|
||||||
# Keep these EMPTY if haven't setup a Pushed.co App yet. #
|
APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection"
|
||||||
|
APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence"
|
||||||
## Pushed.co App Key and App Secret
|
APPRISE_NOTIFY_EACH_DETECTION=false
|
||||||
|
|
||||||
PUSHED_APP_KEY=
|
|
||||||
PUSHED_APP_SECRET=
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
#-------------------------------- Defaults ----------------------------------#
|
#-------------------------------- Defaults ----------------------------------#
|
||||||
@@ -120,11 +114,14 @@ CHANNELS=2
|
|||||||
|
|
||||||
FULL_DISK=purge
|
FULL_DISK=purge
|
||||||
|
|
||||||
## PRIVACY_MODE can be set to 'on' or 'off' to configure analysis to be more
|
## PRIVACY_THRESHOLD can be set to enable sensitivity to Human sounds. This
|
||||||
## sensitive to human detections. PRIVACY_MODE 'on' will purge any data that
|
## setting is an effort to introduce privacy into the data collection.
|
||||||
## receives even a low HUMAN_HUMAN confidence score.
|
## The PRIVACY_THRESHOLD value represents a percentage of the entire species
|
||||||
|
## list used during analysis. If a human sound is predicted anywhere within
|
||||||
|
## the precentile set below, no data is collected for that audio chunk.
|
||||||
|
## Valid range: 0-3
|
||||||
|
|
||||||
PRIVACY_MODE=off
|
PRIVACY_THRESHOLD=0
|
||||||
|
|
||||||
## RECORDING_LENGTH sets the length of the recording that BirdNET-Lite will
|
## RECORDING_LENGTH sets the length of the recording that BirdNET-Lite will
|
||||||
## analyze.
|
## analyze.
|
||||||
|
|||||||
@@ -349,6 +349,36 @@ button:hover {
|
|||||||
width: 20%;
|
width: 20%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 33%;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #d3d3d3;
|
||||||
|
outline: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
-webkit-transition: .2s;
|
||||||
|
transition: opacity .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #04AA6D;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #04AA6D;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
#body::-webkit-scrollbar {
|
#body::-webkit-scrollbar {
|
||||||
# display:none
|
# display:none
|
||||||
#}
|
#}
|
||||||
|
|||||||
Binary file not shown.
@@ -10,3 +10,4 @@ pandas
|
|||||||
seaborn
|
seaborn
|
||||||
streamlit
|
streamlit
|
||||||
plotly
|
plotly
|
||||||
|
apprise
|
||||||
+26
-29
@@ -109,27 +109,12 @@ if(isset($_GET['submit'])) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isset($_GET["privacy_mode"])) {
|
if(isset($_GET["privacy_threshold"])) {
|
||||||
$privacy_mode = $_GET["privacy_mode"];
|
$privacy_threshold = $_GET["privacy_threshold"];
|
||||||
if(strcmp($config['PRIVACY_MODE'], "1") == 0 ) {
|
if(strcmp($privacy_threshold,$config['PRIVACY_THRESHOLD']) !== 0) {
|
||||||
$pmode = "on";
|
$contents = preg_replace("/PRIVACY_THRESHOLD=.*/", "PRIVACY_THRESHOLD=$privacy_threshold", $contents);
|
||||||
}elseif(strcmp($config['PRIVACY_MODE'], "") == 0) {
|
$contents2 = preg_replace("/PRIVACY_THRESHOLD=.*/", "PRIVACY_THRESHOLD=$privacy_threshold", $contents2);
|
||||||
$pmode = "off";
|
exec('restart_services.sh');
|
||||||
}
|
|
||||||
if(strcmp($privacy_mode,$pmode) !== 0) {
|
|
||||||
$contents = preg_replace("/PRIVACY_MODE=.*/", "PRIVACY_MODE=$privacy_mode", $contents);
|
|
||||||
$contents2 = preg_replace("/PRIVACY_MODE=.*/", "PRIVACY_MODE=$privacy_mode", $contents2);
|
|
||||||
if(strcmp($privacy_mode,"on") == 0) {
|
|
||||||
exec('sudo sed -i \'s/\/usr\/local\/bin\/server.py/\/usr\/local\/bin\/privacy_server.py/g\' ../../BirdNET-Pi/templates/birdnet_server.service');
|
|
||||||
exec('sudo systemctl daemon-reload');
|
|
||||||
exec('restart_services.sh');
|
|
||||||
header('Location: /log');
|
|
||||||
} elseif(strcmp($privacy_mode,"off") == 0) {
|
|
||||||
exec('sudo sed -i \'s/\/usr\/local\/bin\/privacy_server.py/\/usr\/local\/bin\/server.py/g\' ../../BirdNET-Pi/templates/birdnet_server.service');
|
|
||||||
exec('sudo systemctl daemon-reload');
|
|
||||||
exec('restart_services.sh');
|
|
||||||
header('Location: /log');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +163,9 @@ if(isset($_GET['submit'])) {
|
|||||||
fwrite($fh, $contents);
|
fwrite($fh, $contents);
|
||||||
fwrite($fh2, $contents2);
|
fwrite($fh2, $contents2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$count_labels = count(file("./scripts/labels.txt"));
|
||||||
|
$count = $count_labels;
|
||||||
?>
|
?>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<style>
|
<style>
|
||||||
@@ -194,14 +182,23 @@ if (file_exists('./scripts/thisrun.txt')) {
|
|||||||
?>
|
?>
|
||||||
<h2>Advanced Settings</h2>
|
<h2>Advanced Settings</h2>
|
||||||
<form action="" method="GET">
|
<form action="" method="GET">
|
||||||
<label>Privacy Mode: </label>
|
<label>Privacy Threshold: </label><br>
|
||||||
<label for="on">
|
<div class="slidecontainer">
|
||||||
<input name="privacy_mode" type="radio" id="on" value="on" <?php if (strcmp($newconfig['PRIVACY_MODE'], "1") == 0) { echo "checked"; }?>>On</label>
|
<input name="privacy_threshold" type="range" min="0" max="3" value="<?php print($newconfig['PRIVACY_THRESHOLD']);?>" class="slider" id="privacy_threshold">
|
||||||
<label for="off">
|
<p>Value: <span id="threshold_value"></span>%</p>
|
||||||
<input name="privacy_mode" type="radio" id="off" value="off" <?php if (strcmp($newconfig['PRIVACY_MODE'], "") == 0) { echo "checked"; }?>>Off</label>
|
</div>
|
||||||
<p>Privacy mode can be set to 'on' or 'off' to configure analysis to be more sensitive to human detections. Privacy mode 'on' will purge any data that receives even a low Human confidence score.
|
<script>
|
||||||
Please note that changing this setting restarts services and replaces the running server. It will take about 90, so please be patient!</p>
|
var slider = document.getElementById("privacy_threshold");
|
||||||
|
var output = document.getElementById("threshold_value");
|
||||||
|
output.innerHTML = slider.value; // Display the default slider value
|
||||||
|
|
||||||
|
// Update the current slider value (each time you drag the slider handle)
|
||||||
|
slider.oninput = function() {
|
||||||
|
output.innerHTML = this.value;
|
||||||
|
document.getElementById("predictionCount").innerHTML = parseInt((this.value * <?php echo $count; ?>)/100);
|
||||||
|
}
|
||||||
|
</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>
|
<label>Full Disk Behavior: </label>
|
||||||
<label for="purge">
|
<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>
|
<input name="full_disk" type="radio" id="purge" value="purge" <?php if (strcmp($newconfig['FULL_DISK'], "purge") == 0) { echo "checked"; }?>>Purge</label>
|
||||||
@@ -255,7 +252,7 @@ foreach($formats as $format){
|
|||||||
<p>Min=0.5, Max=1.5</p>
|
<p>Min=0.5, Max=1.5</p>
|
||||||
<br><br>
|
<br><br>
|
||||||
<input type="hidden" name="view" value="Advanced">
|
<input type="hidden" name="view" value="Advanced">
|
||||||
<button 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.')}" type="submit" name="submit" value="advanced">
|
||||||
<?php
|
<?php
|
||||||
if(isset($_GET['submit'])){
|
if(isset($_GET['submit'])){
|
||||||
echo "Success!";
|
echo "Success!";
|
||||||
|
|||||||
@@ -95,11 +95,8 @@ run_analysis() {
|
|||||||
echo "${1}/${i}" > $HOME/BirdNET-Pi/analyzing_now.txt
|
echo "${1}/${i}" > $HOME/BirdNET-Pi/analyzing_now.txt
|
||||||
[ -z ${RECORDING_LENGTH} ] && RECORDING_LENGTH=15
|
[ -z ${RECORDING_LENGTH} ] && RECORDING_LENGTH=15
|
||||||
echo "RECORDING_LENGTH set to ${RECORDING_LENGTH}"
|
echo "RECORDING_LENGTH set to ${RECORDING_LENGTH}"
|
||||||
a=0
|
|
||||||
until [ -z "$(lsof -t ${1}/${i})" ];do
|
until [ -z "$(lsof -t ${1}/${i})" ];do
|
||||||
sleep 2
|
sleep 2
|
||||||
[ $a -ge ${RECORDING_LENGTH} ] && rm -f ${1}/${i} && break
|
|
||||||
a=$((a+2))
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if ! grep 5050 <(netstat -tulpn 2>&1) &> /dev/null 2>&1;then
|
if ! grep 5050 <(netstat -tulpn 2>&1) &> /dev/null 2>&1;then
|
||||||
@@ -108,19 +105,19 @@ run_analysis() {
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
# prepare optional parameters for analyse.py
|
# prepare optional parameters for analyze.py
|
||||||
if [ -f ${INCLUDE_LIST} ]; then
|
if [ -f ${INCLUDE_LIST} ]; then
|
||||||
INCLUDEPARAM="--include_list \"${INCLUDE_LIST}\""
|
INCLUDEPARAM="--include_list ${INCLUDE_LIST}"
|
||||||
else
|
else
|
||||||
INCLUDEPARAM=""
|
INCLUDEPARAM=""
|
||||||
fi
|
fi
|
||||||
if [ -f ${EXCLUDE_LIST} ]; then
|
if [ -f ${EXCLUDE_LIST} ]; then
|
||||||
EXCLUDEPARAM="--include_list \"${EXCLUDE_LIST}\""
|
EXCLUDEPARAM="--exclude_list ${EXCLUDE_LIST}"
|
||||||
else
|
else
|
||||||
EXCLUDEPARAM=""
|
EXCLUDEPARAM=""
|
||||||
fi
|
fi
|
||||||
if [ ! -z $BIRDWEATHER_ID ]; then
|
if [ ! -z $BIRDWEATHER_ID ]; then
|
||||||
BIRDWEATHER_ID_PARAM="--birdweather_id \"${BIRDWEATHER_ID}\""
|
BIRDWEATHER_ID_PARAM="--birdweather_id ${BIRDWEATHER_ID}"
|
||||||
BIRDWEATHER_ID_LOG="--birdweather_id \"IN_USE\""
|
BIRDWEATHER_ID_LOG="--birdweather_id \"IN_USE\""
|
||||||
else
|
else
|
||||||
BIRDWEATHER_ID_PARAM=""
|
BIRDWEATHER_ID_PARAM=""
|
||||||
|
|||||||
+59
-13
@@ -7,28 +7,54 @@ if(isset($_GET["latitude"])){
|
|||||||
$latitude = $_GET["latitude"];
|
$latitude = $_GET["latitude"];
|
||||||
$longitude = $_GET["longitude"];
|
$longitude = $_GET["longitude"];
|
||||||
$birdweather_id = $_GET["birdweather_id"];
|
$birdweather_id = $_GET["birdweather_id"];
|
||||||
$pushed_app_key = $_GET["pushed_app_key"];
|
$apprise_input = $_GET['apprise_input'];
|
||||||
$pushed_app_secret = $_GET["pushed_app_secret"];
|
$apprise_notification_title = $_GET['apprise_notification_title'];
|
||||||
|
$apprise_notification_body = $_GET['apprise_notification_body'];
|
||||||
|
if(isset($_GET['apprise_notify_each_detection'])) {
|
||||||
|
$apprise_notify_each_detection = 1;
|
||||||
|
} else {
|
||||||
|
$apprise_notify_each_detection = 0;
|
||||||
|
}
|
||||||
|
if(isset($_GET['apprise_notify_each_species'])) {
|
||||||
|
exec('sudo systemctl start pushed_notifications.service');
|
||||||
|
} else {
|
||||||
|
exec('sudo systemctl stop pushed_notifications.service');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$contents = file_get_contents("/etc/birdnet/birdnet.conf");
|
$contents = file_get_contents("/etc/birdnet/birdnet.conf");
|
||||||
$contents = preg_replace("/LATITUDE=.*/", "LATITUDE=$latitude", $contents);
|
$contents = preg_replace("/LATITUDE=.*/", "LATITUDE=$latitude", $contents);
|
||||||
$contents = preg_replace("/LONGITUDE=.*/", "LONGITUDE=$longitude", $contents);
|
$contents = preg_replace("/LONGITUDE=.*/", "LONGITUDE=$longitude", $contents);
|
||||||
$contents = preg_replace("/BIRDWEATHER_ID=.*/", "BIRDWEATHER_ID=$birdweather_id", $contents);
|
$contents = preg_replace("/BIRDWEATHER_ID=.*/", "BIRDWEATHER_ID=$birdweather_id", $contents);
|
||||||
$contents = preg_replace("/PUSHED_APP_KEY=.*/", "PUSHED_APP_KEY=$pushed_app_key", $contents);
|
$contents = preg_replace("/APPRISE_NOTIFICATION_TITLE=.*/", "APPRISE_NOTIFICATION_TITLE=\"$apprise_notification_title\"", $contents);
|
||||||
$contents = preg_replace("/PUSHED_APP_SECRET=.*/", "PUSHED_APP_SECRET=$pushed_app_secret", $contents);
|
$contents = preg_replace("/APPRISE_NOTIFICATION_BODY=.*/", "APPRISE_NOTIFICATION_BODY=\"$apprise_notification_body\"", $contents);
|
||||||
|
$contents = preg_replace("/APPRISE_NOTIFY_EACH_DETECTION=.*/", "APPRISE_NOTIFY_EACH_DETECTION=$apprise_notify_each_detection", $contents);
|
||||||
|
|
||||||
|
|
||||||
$contents2 = file_get_contents("./scripts/thisrun.txt");
|
$contents2 = file_get_contents("./scripts/thisrun.txt");
|
||||||
$contents2 = preg_replace("/LATITUDE=.*/", "LATITUDE=$latitude", $contents2);
|
$contents2 = preg_replace("/LATITUDE=.*/", "LATITUDE=$latitude", $contents2);
|
||||||
$contents2 = preg_replace("/LONGITUDE=.*/", "LONGITUDE=$longitude", $contents2);
|
$contents2 = preg_replace("/LONGITUDE=.*/", "LONGITUDE=$longitude", $contents2);
|
||||||
$contents2 = preg_replace("/BIRDWEATHER_ID=.*/", "BIRDWEATHER_ID=$birdweather_id", $contents2);
|
$contents2 = preg_replace("/BIRDWEATHER_ID=.*/", "BIRDWEATHER_ID=$birdweather_id", $contents2);
|
||||||
$contents2 = preg_replace("/PUSHED_APP_KEY=.*/", "PUSHED_APP_KEY=$pushed_app_key", $contents2);
|
$contents2 = preg_replace("/APPRISE_NOTIFICATION_TITLE=.*/", "APPRISE_NOTIFICATION_TITLE=\"$apprise_notification_title\"", $contents2);
|
||||||
$contents2 = preg_replace("/PUSHED_APP_SECRET=.*/", "PUSHED_APP_SECRET=$pushed_app_secret", $contents2);
|
$contents2 = preg_replace("/APPRISE_NOTIFICATION_BODY=.*/", "APPRISE_NOTIFICATION_BODY=\"$apprise_notification_body\"", $contents2);
|
||||||
|
$contents2 = preg_replace("/APPRISE_NOTIFY_EACH_DETECTION=.*/", "APPRISE_NOTIFY_EACH_DETECTION=$apprise_notify_each_detection", $contents2);
|
||||||
|
|
||||||
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
||||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||||
fwrite($fh, $contents);
|
fwrite($fh, $contents);
|
||||||
fwrite($fh2, $contents2);
|
fwrite($fh2, $contents2);
|
||||||
|
|
||||||
|
if(isset($apprise_input)){
|
||||||
|
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||||
|
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||||
|
$home = trim($home);
|
||||||
|
|
||||||
|
$appriseconfig = fopen($home."/BirdNET-Pi/apprise.txt", "w");
|
||||||
|
fwrite($appriseconfig, $apprise_input);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$language = $_GET["language"];
|
$language = $_GET["language"];
|
||||||
if ($language != "none"){
|
if ($language != "none"){
|
||||||
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||||
@@ -54,6 +80,14 @@ if (file_exists('./scripts/thisrun.txt')) {
|
|||||||
} elseif (file_exists('./scripts/firstrun.ini')) {
|
} elseif (file_exists('./scripts/firstrun.ini')) {
|
||||||
$config = parse_ini_file('./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'];
|
$caddypwd = $config['CADDY_PWD'];
|
||||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||||
@@ -78,12 +112,24 @@ if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
|||||||
<p>Set your Latitude and Longitude to 4 decimal places. Get your coordinates <a href="https://latlong.net" target="_blank">here</a>.</p>
|
<p>Set your Latitude and Longitude to 4 decimal places. Get your coordinates <a href="https://latlong.net" target="_blank">here</a>.</p>
|
||||||
<label for="birdweather_id">BirdWeather ID: </label>
|
<label for="birdweather_id">BirdWeather ID: </label>
|
||||||
<input name="birdweather_id" type="text" value="<?php print($config['BIRDWEATHER_ID']);?>" /><br>
|
<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('./scripts/birdweather_request.php'); ?>" target="_blank">Email Tim</a> to request a BirdWeather ID</p><br>
|
||||||
<label for="pushed_app_key">Pushed App Key: </label>
|
<h3>Notifications</h3>
|
||||||
<input name="pushed_app_key" type="text" value="<?php print($config['PUSHED_APP_KEY']);?>" /><br>
|
<p><a target="_blank" href="https://github.com/caronc/apprise/wiki">Apprise Notifications</a> can be setup and enabled for 70+ notification services. Each service should be on its own line.</p>
|
||||||
<label for="pushed_app_secret">Pushed App Secret: </label>
|
<label for="apprise_input">Apprise Notifications Configuration: </label>
|
||||||
<input name="pushed_app_secret" type="text" value="<?php print($config['PUSHED_APP_SECRET']);?>" /><br>
|
<textarea placeholder="mailto://{user}:{password}@gmail.com
|
||||||
<p><a target="_blank" href="https://pushed.co/quick-start-guide">Pushed iOS Notifications</a> can be setup and enabled for New Species notifications. Be sure to "Enable" the "Pushed Notifications" in "Tools" > "Services" if you would like to use this feature. Sorry, Android users, this only works on iOS.</p>
|
tgram://{bot_token}/{chat_id}
|
||||||
|
twitter://{ConsumerKey}/{ConsumerSecret}/{AccessToken}/{AccessSecret}
|
||||||
|
https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}
|
||||||
|
..." style="vertical-align: top" name="apprise_input" cols="140" rows="5" type="text" ><?php print($apprise_config);?></textarea><br><br>
|
||||||
|
<label for="apprise_notification_title">Notification Title: </label>
|
||||||
|
<input name="apprise_notification_title" type="text" value="<?php print($config['APPRISE_NOTIFICATION_TITLE']);?>" /><br>
|
||||||
|
<label for="apprise_notification_body">Notification Body (use variables $sciname, $comname, or $confidence): </label>
|
||||||
|
<input name="apprise_notification_body" type="text" value="<?php print($config['APPRISE_NOTIFICATION_BODY']);?>" /><br>
|
||||||
|
<input type="checkbox" name="apprise_notify_each_species" <?php $output = shell_exec("service pushed_notifications status"); if (!strpos($output, 'dead') !== false && filesize($home."/BirdNET-Pi/apprise.txt") != 0) { echo "checked"; } ?>>
|
||||||
|
<label for="apprise_notify_each_species">Notify each new species</label><br>
|
||||||
|
<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_notify_each_detection">Notify each new detection</label><br><br>
|
||||||
|
<h3>Localization</h3>
|
||||||
<label for="language">Database Language: </label>
|
<label for="language">Database Language: </label>
|
||||||
<select name="language">
|
<select name="language">
|
||||||
<option value="none">Select your language</option>
|
<option value="none">Select your language</option>
|
||||||
@@ -120,7 +166,7 @@ if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
|||||||
<br><br>
|
<br><br>
|
||||||
<input type="hidden" name="status" value="success">
|
<input type="hidden" name="status" value="success">
|
||||||
<input type="hidden" name="submit" value="settings">
|
<input type="hidden" name="submit" value="settings">
|
||||||
<button type="submit" name="view" value="Settings">
|
<button onclick="if(Boolean(Number(<?php print($config['APPRISE_NOTIFY_EACH_DETECTION']); ?>)) != document.getElementsByName('apprise_notify_each_detection')[0].checked) type="submit" name="view" value="Settings">
|
||||||
<?php
|
<?php
|
||||||
if(isset($_GET['status'])){
|
if(isset($_GET['status'])){
|
||||||
echo "Success!";
|
echo "Success!";
|
||||||
|
|||||||
+17
-2
@@ -7,8 +7,23 @@ if [ "${used//%}" -ge 95 ]; then
|
|||||||
|
|
||||||
case $FULL_DISK in
|
case $FULL_DISK in
|
||||||
purge) echo "Removing oldest data"
|
purge) echo "Removing oldest data"
|
||||||
rm -drfv "$(find ${EXTRACTED}/By_Date/* -maxdepth 1 -type d -prune \
|
cd ${EXTRACTED}/By_Date/
|
||||||
| sort -r | tail -n1)";;
|
curl localhost/views.php?view=Species%20Stats &>/dev/null
|
||||||
|
filestodelete=$(($(find ${EXTRACTED}/By_Date/* -type f | wc -l) / $(find ${EXTRACTED}/By_Date/* -maxdepth 0 -type d | wc -l)))
|
||||||
|
iter=0
|
||||||
|
for i in */*/*; do
|
||||||
|
if [ $iter -ge $filestodelete ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! grep -qxFe "$i" $HOME/BirdNET-Pi/scripts/disk_check_exclude.txt; then
|
||||||
|
rm "$i"
|
||||||
|
fi
|
||||||
|
((iter++))
|
||||||
|
done
|
||||||
|
find ${EXTRACTED}/By_Date/ -empty -type d -delete;;
|
||||||
|
|
||||||
|
#rm -drfv "$(find ${EXTRACTED}/By_Date/* -maxdepth 1 -type d -prune \
|
||||||
|
# | sort -r | tail -n1)";;
|
||||||
keep) echo "Stopping Core Services"
|
keep) echo "Stopping Core Services"
|
||||||
/usr/local/bin/stop_core_services.sh;;
|
/usr/local/bin/stop_core_services.sh;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+11
-14
@@ -69,17 +69,11 @@ BIRDNETPI_URL=
|
|||||||
|
|
||||||
RTSP_STREAM=
|
RTSP_STREAM=
|
||||||
|
|
||||||
#------------------- Mobile Notifications via Pushed.co ---------------------#
|
#----------------------- Apprise Miscellanous Configuration -------------------#
|
||||||
#____________The two variables below enable mobile notifications_______________#
|
|
||||||
#_____________See https://pushed.co/quick-start-guide to get___________________#
|
|
||||||
#_________________________these values for your app.___________________________#
|
|
||||||
|
|
||||||
# Keep these EMPTY if haven't setup a Pushed.co App yet. #
|
APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection"
|
||||||
|
APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence"
|
||||||
## Pushed.co App Key and App Secret
|
APPRISE_NOTIFY_EACH_DETECTION=false
|
||||||
|
|
||||||
PUSHED_APP_KEY=
|
|
||||||
PUSHED_APP_SECRET=
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
#-------------------------------- Defaults ----------------------------------#
|
#-------------------------------- Defaults ----------------------------------#
|
||||||
@@ -133,11 +127,14 @@ CHANNELS=2
|
|||||||
|
|
||||||
FULL_DISK=purge
|
FULL_DISK=purge
|
||||||
|
|
||||||
## PRIVACY_MODE can be set to 'on' or 'off' to configure analysis to be more
|
## PRIVACY_THRESHOLD can be set to enable sensitivity to Human sounds. This
|
||||||
## sensitive to human detections. PRIVACY_MODE 'on' will purge any data that
|
## setting is an effort to introduce privacy into the data collection.
|
||||||
## receives even a low HUMAN_HUMAN confidence score.
|
## The PRIVACY_THRESHOLD value represents a percentage of the entire species
|
||||||
|
## list used during analysis. If a human sound is predicted anywhere within
|
||||||
|
## the precentile set below, no data is collected for that audio chunk.
|
||||||
|
## Valid range: 0-3
|
||||||
|
|
||||||
PRIVACY_MODE=off
|
PRIVACY_THRESHOLD=0
|
||||||
|
|
||||||
## RECORDING_LENGTH sets the length of the recording that BirdNET-Lite will
|
## RECORDING_LENGTH sets the length of the recording that BirdNET-Lite will
|
||||||
## analyze.
|
## analyze.
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ function loadDetectionIfNewExists(previous_detection_identifier=undefined) {
|
|||||||
const xhttp = new XMLHttpRequest();
|
const xhttp = new XMLHttpRequest();
|
||||||
xhttp.onload = function() {
|
xhttp.onload = function() {
|
||||||
// if there's a new detection that needs to be updated to the page
|
// if there's a new detection that needs to be updated to the page
|
||||||
if(this.responseText.length > 0) {
|
if(this.responseText.length > 0 && !this.responseText.includes("Database is busy.")) {
|
||||||
document.getElementById("most_recent_detection").innerHTML = this.responseText;
|
document.getElementById("most_recent_detection").innerHTML = this.responseText;
|
||||||
|
|
||||||
// only going to load left chart if there's a new detection
|
// only going to load left chart if there's a new detection
|
||||||
|
|||||||
@@ -1,540 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from tzlocal import get_localzone
|
|
||||||
import datetime
|
|
||||||
import sqlite3
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
import librosa
|
|
||||||
import operator
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
import os
|
|
||||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
|
||||||
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
|
||||||
|
|
||||||
try:
|
|
||||||
import tflite_runtime.interpreter as tflite
|
|
||||||
except BaseException:
|
|
||||||
from tensorflow import lite as tflite
|
|
||||||
|
|
||||||
|
|
||||||
HEADER = 64
|
|
||||||
PORT = 5050
|
|
||||||
SERVER = socket.gethostbyname(socket.gethostname())
|
|
||||||
ADDR = (SERVER, PORT)
|
|
||||||
FORMAT = 'utf-8'
|
|
||||||
DISCONNECT_MESSAGE = "!DISCONNECT"
|
|
||||||
|
|
||||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
try:
|
|
||||||
server.bind(ADDR)
|
|
||||||
except BaseException:
|
|
||||||
print("Waiting on socket")
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
|
|
||||||
# Open most recent Configuration and grab DB_PWD as a python variable
|
|
||||||
userDir = os.path.expanduser('~')
|
|
||||||
with open(userDir + '/BirdNET-Pi/scripts/thisrun.txt', 'r') as f:
|
|
||||||
this_run = f.readlines()
|
|
||||||
audiofmt = "." + \
|
|
||||||
str(str(str([i for i in this_run if i.startswith(
|
|
||||||
'AUDIOFMT')]).split('=')[1]).split('\\')[0])
|
|
||||||
|
|
||||||
|
|
||||||
def loadModel():
|
|
||||||
|
|
||||||
global INPUT_LAYER_INDEX
|
|
||||||
global OUTPUT_LAYER_INDEX
|
|
||||||
global MDATA_INPUT_INDEX
|
|
||||||
global CLASSES
|
|
||||||
|
|
||||||
print('LOADING TF LITE MODEL...', end=' ')
|
|
||||||
|
|
||||||
# Load TFLite model and allocate tensors.
|
|
||||||
modelpath = userDir + '/BirdNET-Pi/model/BirdNET_6K_GLOBAL_MODEL.tflite'
|
|
||||||
myinterpreter = tflite.Interpreter(model_path=modelpath, num_threads=2)
|
|
||||||
myinterpreter.allocate_tensors()
|
|
||||||
|
|
||||||
# Get input and output tensors.
|
|
||||||
input_details = myinterpreter.get_input_details()
|
|
||||||
output_details = myinterpreter.get_output_details()
|
|
||||||
|
|
||||||
# Get input tensor index
|
|
||||||
INPUT_LAYER_INDEX = input_details[0]['index']
|
|
||||||
MDATA_INPUT_INDEX = input_details[1]['index']
|
|
||||||
OUTPUT_LAYER_INDEX = output_details[0]['index']
|
|
||||||
|
|
||||||
# Load labels
|
|
||||||
CLASSES = []
|
|
||||||
with open(userDir + '/BirdNET-Pi/model/labels.txt', 'r') as lfile:
|
|
||||||
for line in lfile.readlines():
|
|
||||||
CLASSES.append(line.replace('\n', ''))
|
|
||||||
|
|
||||||
print('DONE!')
|
|
||||||
|
|
||||||
return myinterpreter
|
|
||||||
|
|
||||||
|
|
||||||
def loadCustomSpeciesList(path):
|
|
||||||
|
|
||||||
slist = []
|
|
||||||
if os.path.isfile(path):
|
|
||||||
with open(path, 'r') as csfile:
|
|
||||||
for line in csfile.readlines():
|
|
||||||
slist.append(line.replace('\r', '').replace('\n', ''))
|
|
||||||
|
|
||||||
return slist
|
|
||||||
|
|
||||||
|
|
||||||
def splitSignal(sig, rate, overlap, seconds=3.0, minlen=1.5):
|
|
||||||
|
|
||||||
# Split signal with overlap
|
|
||||||
sig_splits = []
|
|
||||||
for i in range(0, len(sig), int((seconds - overlap) * rate)):
|
|
||||||
split = sig[i:i + int(seconds * rate)]
|
|
||||||
|
|
||||||
# End of signal?
|
|
||||||
if len(split) < int(minlen * rate):
|
|
||||||
break
|
|
||||||
|
|
||||||
# Signal chunk too short? Fill with zeros.
|
|
||||||
if len(split) < int(rate * seconds):
|
|
||||||
temp = np.zeros((int(rate * seconds)))
|
|
||||||
temp[:len(split)] = split
|
|
||||||
split = temp
|
|
||||||
|
|
||||||
sig_splits.append(split)
|
|
||||||
|
|
||||||
return sig_splits
|
|
||||||
|
|
||||||
|
|
||||||
def readAudioData(path, overlap, sample_rate=48000):
|
|
||||||
|
|
||||||
print('READING AUDIO DATA...', end=' ', flush=True)
|
|
||||||
|
|
||||||
# Open file with librosa (uses ffmpeg or libav)
|
|
||||||
sig, rate = librosa.load(
|
|
||||||
path, sr=sample_rate, mono=True, res_type='kaiser_fast')
|
|
||||||
|
|
||||||
# Split audio into 3-second chunks
|
|
||||||
chunks = splitSignal(sig, rate, overlap)
|
|
||||||
|
|
||||||
print('DONE! READ', str(len(chunks)), 'CHUNKS.')
|
|
||||||
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
|
|
||||||
def convertMetadata(m):
|
|
||||||
|
|
||||||
# Convert week to cosine
|
|
||||||
if m[2] >= 1 and m[2] <= 48:
|
|
||||||
m[2] = math.cos(math.radians(m[2] * 7.5)) + 1
|
|
||||||
else:
|
|
||||||
m[2] = -1
|
|
||||||
|
|
||||||
# Add binary mask
|
|
||||||
mask = np.ones((3,))
|
|
||||||
if m[0] == -1 or m[1] == -1:
|
|
||||||
mask = np.zeros((3,))
|
|
||||||
if m[2] == -1:
|
|
||||||
mask[2] = 0.0
|
|
||||||
|
|
||||||
return np.concatenate([m, mask])
|
|
||||||
|
|
||||||
|
|
||||||
def custom_sigmoid(x, sensitivity=1.0):
|
|
||||||
return 1 / (1.0 + np.exp(-sensitivity * x))
|
|
||||||
|
|
||||||
|
|
||||||
def predict(sample, sensitivity):
|
|
||||||
global INTERPRETER
|
|
||||||
# Make a prediction
|
|
||||||
INTERPRETER.set_tensor(
|
|
||||||
INPUT_LAYER_INDEX, np.array(
|
|
||||||
sample[0], dtype='float32'))
|
|
||||||
INTERPRETER.set_tensor(
|
|
||||||
MDATA_INPUT_INDEX, np.array(
|
|
||||||
sample[1], dtype='float32'))
|
|
||||||
INTERPRETER.invoke()
|
|
||||||
prediction = INTERPRETER.get_tensor(OUTPUT_LAYER_INDEX)[0]
|
|
||||||
|
|
||||||
# Apply custom sigmoid
|
|
||||||
p_sigmoid = custom_sigmoid(prediction, sensitivity)
|
|
||||||
|
|
||||||
# Get label and scores for pooled predictions
|
|
||||||
p_labels = dict(zip(CLASSES, p_sigmoid))
|
|
||||||
|
|
||||||
# Sort by score
|
|
||||||
p_sorted = sorted(
|
|
||||||
p_labels.items(),
|
|
||||||
key=operator.itemgetter(1),
|
|
||||||
reverse=True)
|
|
||||||
|
|
||||||
# Remove species that are on blacklist
|
|
||||||
for i in range(min(10, len(p_sorted))):
|
|
||||||
if p_sorted[i][0] in ['Non-bird_Non-bird', 'Noise_Noise']:
|
|
||||||
p_sorted[i] = (p_sorted[i][0], 0.0)
|
|
||||||
if p_sorted[i][0] == 'Human_Human':
|
|
||||||
print("HUMAN SCORE:", str(p_sorted[i]))
|
|
||||||
with open(userDir + '/BirdNET-Pi/HUMAN.txt', 'a') as rfile:
|
|
||||||
rfile.write(str(datetime.datetime.now()) +
|
|
||||||
str(p_sorted[i]) + '\n')
|
|
||||||
# date_stamp=datetime.datetime.now().strftime("%d_%m_%y_%H:%M:%S")
|
|
||||||
#
|
|
||||||
# sf.write('./home/*/human_sample.wav',np.random.randn(10,2) , 44100)
|
|
||||||
# #sample[0]
|
|
||||||
|
|
||||||
# Only return first the top ten results
|
|
||||||
# INCREASE THIS TO SEE IF HUMAN IS DETECTED MORE RELIABLY
|
|
||||||
# print('P_SORTED-------', p_sorted)
|
|
||||||
return p_sorted[:100]
|
|
||||||
|
|
||||||
|
|
||||||
def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,):
|
|
||||||
global INTERPRETER
|
|
||||||
|
|
||||||
detections = {}
|
|
||||||
start = time.time()
|
|
||||||
print('ANALYZING AUDIO...', end=' ', flush=True)
|
|
||||||
|
|
||||||
# Convert and prepare metadata
|
|
||||||
mdata = convertMetadata(np.array([lat, lon, week]))
|
|
||||||
mdata = np.expand_dims(mdata, 0)
|
|
||||||
|
|
||||||
# Parse every chunk
|
|
||||||
pred_start = 0.0
|
|
||||||
for c in chunks:
|
|
||||||
|
|
||||||
# Prepare as input signal
|
|
||||||
sig = np.expand_dims(c, 0)
|
|
||||||
|
|
||||||
# Make prediction
|
|
||||||
p = predict([sig, mdata], sensitivity)
|
|
||||||
# print("PPPPP",p)
|
|
||||||
HUMAN_DETECTED = False
|
|
||||||
# Catch if Human is recognized
|
|
||||||
for x in range(len(p)):
|
|
||||||
if "Human" in p[x][0]:
|
|
||||||
# print("HUMAN DETECTED!!",p[x][0])
|
|
||||||
# clear list
|
|
||||||
HUMAN_DETECTED = True
|
|
||||||
print("CHUNK -----", c)
|
|
||||||
|
|
||||||
# Save result and timestamp
|
|
||||||
pred_end = pred_start + 3.0
|
|
||||||
|
|
||||||
if HUMAN_DETECTED is True:
|
|
||||||
p = [('Human_Human', 0.0)] * 10
|
|
||||||
print("HUMAN DETECTED!!!", p)
|
|
||||||
|
|
||||||
detections[str(pred_start) + ';' + str(pred_end)] = p
|
|
||||||
|
|
||||||
pred_start = pred_end - overlap
|
|
||||||
|
|
||||||
print('DONE! Time', int((time.time() - start) * 10) / 10.0, 'SECONDS')
|
|
||||||
# print('DETECTIONS:::::',detections)
|
|
||||||
return detections
|
|
||||||
|
|
||||||
|
|
||||||
def writeResultsToFile(detections, min_conf, path):
|
|
||||||
|
|
||||||
print('WRITING RESULTS TO', path, '...', end=' ')
|
|
||||||
rcnt = 0
|
|
||||||
with open(path, 'w') as rfile:
|
|
||||||
rfile.write(
|
|
||||||
'Start (s);End (s);Scientific name;Common name;Confidence\n')
|
|
||||||
for d in detections:
|
|
||||||
for entry in detections[d]:
|
|
||||||
if entry[1] >= min_conf and ((entry[0] in INCLUDE_LIST or len(
|
|
||||||
INCLUDE_LIST) == 0) and (entry[0] not in EXCLUDE_LIST or len(EXCLUDE_LIST) == 0)):
|
|
||||||
rfile.write(d +
|
|
||||||
';' +
|
|
||||||
entry[0].replace('_', ';') +
|
|
||||||
';' +
|
|
||||||
str(entry[1]) +
|
|
||||||
'\n')
|
|
||||||
rcnt += 1
|
|
||||||
print('DONE! WROTE', rcnt, 'RESULTS.')
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def handle_client(conn, addr):
|
|
||||||
global INCLUDE_LIST
|
|
||||||
global EXCLUDE_LIST
|
|
||||||
print(f"[NEW CONNECTION] {addr} connected.")
|
|
||||||
|
|
||||||
connected = True
|
|
||||||
while connected:
|
|
||||||
msg_length = conn.recv(HEADER).decode(FORMAT)
|
|
||||||
if msg_length:
|
|
||||||
msg_length = int(msg_length)
|
|
||||||
msg = conn.recv(msg_length).decode(FORMAT)
|
|
||||||
if msg == DISCONNECT_MESSAGE:
|
|
||||||
connected = False
|
|
||||||
else:
|
|
||||||
# print(f"[{addr}] {msg}")
|
|
||||||
|
|
||||||
args = type('', (), {})()
|
|
||||||
|
|
||||||
args.i = ''
|
|
||||||
args.o = ''
|
|
||||||
args.birdweather_id = '99999'
|
|
||||||
args.include_list = 'null'
|
|
||||||
args.exclude_list = 'null'
|
|
||||||
args.overlap = 0.0
|
|
||||||
args.week = -1
|
|
||||||
args.sensitivity = 1.25
|
|
||||||
args.min_conf = 0.70
|
|
||||||
args.lat = -1
|
|
||||||
args.lon = -1
|
|
||||||
|
|
||||||
for line in msg.split('||'):
|
|
||||||
inputvars = line.split('=')
|
|
||||||
if inputvars[0] == 'i':
|
|
||||||
args.i = inputvars[1]
|
|
||||||
elif inputvars[0] == 'o':
|
|
||||||
args.o = inputvars[1]
|
|
||||||
elif inputvars[0] == 'birdweather_id':
|
|
||||||
args.birdweather_id = inputvars[1]
|
|
||||||
elif inputvars[0] == 'include_list':
|
|
||||||
args.include_list = inputvars[1]
|
|
||||||
elif inputvars[0] == 'exclude_list':
|
|
||||||
args.exclude_list = inputvars[1]
|
|
||||||
elif inputvars[0] == 'overlap':
|
|
||||||
args.overlap = float(inputvars[1])
|
|
||||||
elif inputvars[0] == 'week':
|
|
||||||
args.week = int(inputvars[1])
|
|
||||||
elif inputvars[0] == 'sensitivity':
|
|
||||||
args.sensitivity = float(inputvars[1])
|
|
||||||
elif inputvars[0] == 'min_conf':
|
|
||||||
args.min_conf = float(inputvars[1])
|
|
||||||
elif inputvars[0] == 'lat':
|
|
||||||
args.lat = float(inputvars[1])
|
|
||||||
elif inputvars[0] == 'lon':
|
|
||||||
args.lon = float(inputvars[1])
|
|
||||||
|
|
||||||
# Load custom species lists - INCLUDED and EXCLUDED
|
|
||||||
if not args.include_list == 'null':
|
|
||||||
INCLUDE_LIST = loadCustomSpeciesList(args.include_list)
|
|
||||||
else:
|
|
||||||
INCLUDE_LIST = []
|
|
||||||
|
|
||||||
if not args.exclude_list == 'null':
|
|
||||||
EXCLUDE_LIST = loadCustomSpeciesList(args.exclude_list)
|
|
||||||
else:
|
|
||||||
EXCLUDE_LIST = []
|
|
||||||
|
|
||||||
birdweather_id = args.birdweather_id
|
|
||||||
|
|
||||||
# Read audio data
|
|
||||||
audioData = readAudioData(args.i, args.overlap)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
file_date = file_name.split('-birdnet-')[0]
|
|
||||||
file_time = file_name.split('-birdnet-')[1]
|
|
||||||
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())
|
|
||||||
# print('Time:', date_time_obj.time())
|
|
||||||
print('Date-time:', date_time_obj)
|
|
||||||
now = date_time_obj
|
|
||||||
current_date = now.strftime("%Y-%m-%d")
|
|
||||||
current_time = now.strftime("%H:%M:%S")
|
|
||||||
current_iso8601 = now.astimezone(get_localzone()).isoformat()
|
|
||||||
|
|
||||||
week_number = int(now.strftime("%V"))
|
|
||||||
week = max(1, min(week_number, 48))
|
|
||||||
|
|
||||||
sensitivity = max(
|
|
||||||
0.5, min(1.0 - (args.sensitivity - 1.0), 1.5))
|
|
||||||
|
|
||||||
# Process audio data and get detections
|
|
||||||
detections = analyzeAudioData(
|
|
||||||
audioData, args.lat, args.lon, week, sensitivity, args.overlap)
|
|
||||||
|
|
||||||
# Write detections to output file
|
|
||||||
min_conf = max(0.01, min(args.min_conf, 0.99))
|
|
||||||
writeResultsToFile(detections, min_conf, args.o)
|
|
||||||
|
|
||||||
###################################################################
|
|
||||||
###################################################################
|
|
||||||
|
|
||||||
soundscape_uploaded = False
|
|
||||||
|
|
||||||
# Write detections to Database
|
|
||||||
myReturn = ''
|
|
||||||
for i in detections:
|
|
||||||
myReturn += str(i) + '-' + str(detections[i][0]) + '\n'
|
|
||||||
|
|
||||||
with open(userDir + '/BirdNET-Pi/BirdDB.txt', 'a') as rfile:
|
|
||||||
for d in detections:
|
|
||||||
for entry in detections[d]:
|
|
||||||
if entry[1] >= min_conf and ((entry[0] in INCLUDE_LIST or len(
|
|
||||||
INCLUDE_LIST) == 0) and (entry[0] not in EXCLUDE_LIST or len(EXCLUDE_LIST) == 0)):
|
|
||||||
rfile.write(str(current_date) + ';' + str(current_time) + ';' + entry[0].replace('_', ';') + ';'
|
|
||||||
+ str(entry[1]) + ";" + str(args.lat) + ';' + str(
|
|
||||||
args.lon) + ';' + str(min_conf) + ';' + str(week) + ';'
|
|
||||||
+ str(args.sensitivity) + ';' + str(args.overlap) + '\n')
|
|
||||||
|
|
||||||
Date = str(current_date)
|
|
||||||
Time = str(current_time)
|
|
||||||
species = entry[0]
|
|
||||||
Sci_Name, Com_Name = species.split('_')
|
|
||||||
score = entry[1]
|
|
||||||
Confidence = str(round(score * 100))
|
|
||||||
Lat = str(args.lat)
|
|
||||||
Lon = str(args.lon)
|
|
||||||
Cutoff = str(args.min_conf)
|
|
||||||
Week = str(args.week)
|
|
||||||
Sens = str(args.sensitivity)
|
|
||||||
Overlap = str(args.overlap)
|
|
||||||
Com_Name = Com_Name.replace("'", "")
|
|
||||||
File_Name = Com_Name.replace(" ", "_") + '-' + Confidence + '-' + \
|
|
||||||
Date.replace(
|
|
||||||
"/", "-") + '-birdnet-' + Time + audiofmt
|
|
||||||
|
|
||||||
# Connect to SQLite Database
|
|
||||||
try:
|
|
||||||
con = sqlite3.connect(
|
|
||||||
userDir + '/BirdNET-Pi/scripts/birds.db')
|
|
||||||
cur = con.cursor()
|
|
||||||
cur.execute(
|
|
||||||
"INSERT INTO detections VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
(Date,
|
|
||||||
Time,
|
|
||||||
Sci_Name,
|
|
||||||
Com_Name,
|
|
||||||
str(score),
|
|
||||||
Lat,
|
|
||||||
Lon,
|
|
||||||
Cutoff,
|
|
||||||
Week,
|
|
||||||
Sens,
|
|
||||||
Overlap,
|
|
||||||
File_Name))
|
|
||||||
|
|
||||||
con.commit()
|
|
||||||
con.close()
|
|
||||||
except BaseException:
|
|
||||||
print("Database busy")
|
|
||||||
time.sleep(2)
|
|
||||||
print(str(current_date) +
|
|
||||||
';' +
|
|
||||||
str(current_time) +
|
|
||||||
';' +
|
|
||||||
entry[0].replace('_', ';') +
|
|
||||||
';' +
|
|
||||||
str(entry[1]) +
|
|
||||||
';' +
|
|
||||||
str(args.lat) +
|
|
||||||
';' +
|
|
||||||
str(args.lon) +
|
|
||||||
';' +
|
|
||||||
str(min_conf) +
|
|
||||||
';' +
|
|
||||||
str(week) +
|
|
||||||
';' +
|
|
||||||
str(args.sensitivity) +
|
|
||||||
';' +
|
|
||||||
str(args.overlap) +
|
|
||||||
Com_Name.replace(" ", "_") +
|
|
||||||
'-' +
|
|
||||||
str(score) +
|
|
||||||
'-' +
|
|
||||||
str(current_date) +
|
|
||||||
'-birdnet-' +
|
|
||||||
str(current_time) +
|
|
||||||
audiofmt +
|
|
||||||
'\n')
|
|
||||||
|
|
||||||
if birdweather_id != "99999":
|
|
||||||
try:
|
|
||||||
|
|
||||||
if soundscape_uploaded is False:
|
|
||||||
# POST soundscape to server
|
|
||||||
soundscape_url = "https://app.birdweather.com/api/v1/stations/" + \
|
|
||||||
birdweather_id + "/soundscapes" + "?timestamp=" + current_iso8601
|
|
||||||
|
|
||||||
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'})
|
|
||||||
print(
|
|
||||||
"Soundscape POST Response Status - ", response.status_code)
|
|
||||||
sdata = response.json()
|
|
||||||
soundscape_id = sdata['soundscape']['id']
|
|
||||||
soundscape_uploaded = True
|
|
||||||
|
|
||||||
# POST detection to server
|
|
||||||
detection_url = "https://app.birdweather.com/api/v1/stations/" + \
|
|
||||||
birdweather_id + "/detections"
|
|
||||||
start_time = d.split(';')[0]
|
|
||||||
end_time = d.split(';')[1]
|
|
||||||
post_begin = "{ "
|
|
||||||
now_p_start = now + \
|
|
||||||
datetime.timedelta(
|
|
||||||
seconds=float(start_time))
|
|
||||||
current_iso8601 = now_p_start.astimezone(
|
|
||||||
get_localzone()).isoformat()
|
|
||||||
post_timestamp = "\"timestamp\": \"" + current_iso8601 + "\","
|
|
||||||
post_lat = "\"lat\": " + \
|
|
||||||
str(args.lat) + ","
|
|
||||||
post_lon = "\"lon\": " + \
|
|
||||||
str(args.lon) + ","
|
|
||||||
post_soundscape_id = "\"soundscapeId\": " + \
|
|
||||||
str(soundscape_id) + ","
|
|
||||||
post_soundscape_start_time = "\"soundscapeStartTime\": " + start_time + ","
|
|
||||||
post_soundscape_end_time = "\"soundscapeEndTime\": " + end_time + ","
|
|
||||||
post_commonName = "\"commonName\": \"" + \
|
|
||||||
entry[0].split('_')[1] + "\","
|
|
||||||
post_scientificName = "\"scientificName\": \"" + \
|
|
||||||
entry[0].split('_')[0] + "\","
|
|
||||||
post_algorithm = "\"algorithm\": " + "\"alpha\"" + ","
|
|
||||||
post_confidence = "\"confidence\": " + \
|
|
||||||
str(entry[1])
|
|
||||||
post_end = " }"
|
|
||||||
|
|
||||||
post_json = post_begin + \
|
|
||||||
post_timestamp + post_lat + post_lon + \
|
|
||||||
post_soundscape_id + post_soundscape_start_time + \
|
|
||||||
post_soundscape_end_time + post_commonName + post_scientificName + \
|
|
||||||
post_algorithm + post_confidence + post_end
|
|
||||||
print(post_json)
|
|
||||||
response = requests.post(
|
|
||||||
detection_url, json=json.loads(post_json))
|
|
||||||
print(
|
|
||||||
"Detection POST Response Status - ", response.status_code)
|
|
||||||
except BaseException:
|
|
||||||
print("Cannot POST right now")
|
|
||||||
conn.send(myReturn.encode(FORMAT))
|
|
||||||
|
|
||||||
# time.sleep(3)
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
# Load model
|
|
||||||
global INTERPRETER, INCLUDE_LIST, EXCLUDE_LIST
|
|
||||||
INTERPRETER = loadModel()
|
|
||||||
server.listen()
|
|
||||||
print(f"[LISTENING] Server is listening on {SERVER}")
|
|
||||||
while True:
|
|
||||||
conn, addr = server.accept()
|
|
||||||
thread = threading.Thread(target=handle_client, args=(conn, addr))
|
|
||||||
thread.start()
|
|
||||||
print(f"[ACTIVE CONNECTIONS] {threading.activeCount() - 1}")
|
|
||||||
|
|
||||||
|
|
||||||
print("[STARTING] server is starting...")
|
|
||||||
start()
|
|
||||||
+63
-23
@@ -1,17 +1,20 @@
|
|||||||
from pathlib import Path
|
import argparse
|
||||||
from tzlocal import get_localzone
|
|
||||||
import datetime
|
|
||||||
import sqlite3
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
import librosa
|
|
||||||
import operator
|
import operator
|
||||||
import socket
|
import librosa
|
||||||
import threading
|
import numpy as np
|
||||||
import os
|
import math
|
||||||
|
import time
|
||||||
|
from decimal import Decimal
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import sqlite3
|
||||||
|
import datetime
|
||||||
|
from time import sleep
|
||||||
|
import pytz
|
||||||
|
from tzlocal import get_localzone
|
||||||
|
from pathlib import Path
|
||||||
|
import apprise
|
||||||
|
|
||||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
||||||
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
os.environ['CUDA_VISIBLE_DEVICES'] = ''
|
||||||
|
|
||||||
@@ -20,7 +23,6 @@ try:
|
|||||||
except BaseException:
|
except BaseException:
|
||||||
from tensorflow import lite as tflite
|
from tensorflow import lite as tflite
|
||||||
|
|
||||||
|
|
||||||
HEADER = 64
|
HEADER = 64
|
||||||
PORT = 5050
|
PORT = 5050
|
||||||
SERVER = socket.gethostbyname(socket.gethostname())
|
SERVER = socket.gethostbyname(socket.gethostname())
|
||||||
@@ -41,6 +43,7 @@ userDir = os.path.expanduser('~')
|
|||||||
with open(userDir + '/BirdNET-Pi/scripts/thisrun.txt', 'r') as f:
|
with open(userDir + '/BirdNET-Pi/scripts/thisrun.txt', 'r') as f:
|
||||||
this_run = f.readlines()
|
this_run = f.readlines()
|
||||||
audiofmt = "." + str(str(str([i for i in this_run if i.startswith('AUDIOFMT')]).split('=')[1]).split('\\')[0])
|
audiofmt = "." + str(str(str([i for i in this_run if i.startswith('AUDIOFMT')]).split('=')[1]).split('\\')[0])
|
||||||
|
priv_thresh = float("." + str(str(str([i for i in this_run if i.startswith('PRIVACY_THRESHOLD')]).split('=')[1]).split('\\')[0]))/10
|
||||||
|
|
||||||
|
|
||||||
def loadModel():
|
def loadModel():
|
||||||
@@ -165,13 +168,19 @@ def predict(sample, sensitivity):
|
|||||||
# Sort by score
|
# Sort by score
|
||||||
p_sorted = sorted(p_labels.items(), key=operator.itemgetter(1), reverse=True)
|
p_sorted = sorted(p_labels.items(), key=operator.itemgetter(1), reverse=True)
|
||||||
|
|
||||||
# Remove species that are on blacklist
|
# #print("DATABASE SIZE:", len(p_sorted))
|
||||||
for i in range(min(10, len(p_sorted))):
|
# #print("HUMAN-CUTOFF AT:", int(len(p_sorted)*priv_thresh)/10)
|
||||||
if p_sorted[i][0] in ['Human_Human', 'Non-bird_Non-bird', 'Noise_Noise']:
|
#
|
||||||
p_sorted[i] = (p_sorted[i][0], 0.0)
|
# # Remove species that are on blacklist
|
||||||
|
|
||||||
# Only return first the top ten results
|
human_cutoff = max(10,int(len(p_sorted)*priv_thresh))
|
||||||
return p_sorted[:10]
|
|
||||||
|
for i in range(min(10, len(p_sorted))):
|
||||||
|
if p_sorted[i][0]=='Human_Human':
|
||||||
|
with open(userDir + '/BirdNET-Pi/HUMAN.txt', 'a') as rfile:
|
||||||
|
rfile.write(str(datetime.datetime.now())+str(p_sorted[i])+ ' ' + str(human_cutoff)+ '\n')
|
||||||
|
|
||||||
|
return p_sorted[:human_cutoff]
|
||||||
|
|
||||||
|
|
||||||
def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,):
|
def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,):
|
||||||
@@ -194,16 +203,47 @@ def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,):
|
|||||||
|
|
||||||
# Make prediction
|
# Make prediction
|
||||||
p = predict([sig, mdata], sensitivity)
|
p = predict([sig, mdata], sensitivity)
|
||||||
|
# print("PPPPP",p)
|
||||||
|
HUMAN_DETECTED=False
|
||||||
|
|
||||||
|
#Catch if Human is recognized
|
||||||
|
for x in range(len(p)):
|
||||||
|
if "Human" in p[x][0]:
|
||||||
|
HUMAN_DETECTED=True
|
||||||
|
|
||||||
# Save result and timestamp
|
# Save result and timestamp
|
||||||
pred_end = pred_start + 3.0
|
pred_end = pred_start + 3.0
|
||||||
|
|
||||||
|
#If human detected set all detections to human to make sure voices are not saved
|
||||||
|
if HUMAN_DETECTED == True:
|
||||||
|
p=[('Human_Human',0.0)]*10
|
||||||
|
|
||||||
detections[str(pred_start) + ';' + str(pred_end)] = p
|
detections[str(pred_start) + ';' + str(pred_end)] = p
|
||||||
|
|
||||||
pred_start = pred_end - overlap
|
pred_start = pred_end - overlap
|
||||||
|
|
||||||
print('DONE! Time', int((time.time() - start) * 10) / 10.0, 'SECONDS')
|
print('DONE! Time', int((time.time() - start) * 10) / 10.0, 'SECONDS')
|
||||||
|
# print('DETECTIONS:::::',detections)
|
||||||
return detections
|
return detections
|
||||||
|
|
||||||
|
def sendAppriseNotifications(species,confidence):
|
||||||
|
if os.path.exists(userDir + '/BirdNET-Pi/apprise.txt') and os.path.getsize(userDir + '/BirdNET-Pi/apprise.txt') > 0:
|
||||||
|
with open(userDir + '/BirdNET-Pi/scripts/thisrun.txt', 'r') as f:
|
||||||
|
this_run = f.readlines()
|
||||||
|
title = str(str(str([i for i in this_run if i.startswith('APPRISE_NOTIFICATION_TITLE')]).split('=')[1]).split('\\')[0]).replace('"', '')
|
||||||
|
body = str(str(str([i for i in this_run if i.startswith('APPRISE_NOTIFICATION_BODY')]).split('=')[1]).split('\\')[0]).replace('"', '')
|
||||||
|
|
||||||
|
if str(str(str([i for i in this_run if i.startswith('APPRISE_NOTIFY_EACH_DETECTION')]).split('=')[1]).split('\\')[0]) == "1":
|
||||||
|
|
||||||
|
apobj = apprise.Apprise()
|
||||||
|
config = apprise.AppriseConfig()
|
||||||
|
config.add(userDir + '/BirdNET-Pi/apprise.txt')
|
||||||
|
apobj.add(config)
|
||||||
|
|
||||||
|
apobj.notify(
|
||||||
|
body=body.replace("$sciname",species.split("_")[0]).replace("$comname",species.split("_")[1]).replace("$confidence",confidence),
|
||||||
|
title=title,
|
||||||
|
)
|
||||||
|
|
||||||
def writeResultsToFile(detections, min_conf, path):
|
def writeResultsToFile(detections, min_conf, path):
|
||||||
|
|
||||||
@@ -213,8 +253,8 @@ def writeResultsToFile(detections, min_conf, path):
|
|||||||
rfile.write('Start (s);End (s);Scientific name;Common name;Confidence\n')
|
rfile.write('Start (s);End (s);Scientific name;Common name;Confidence\n')
|
||||||
for d in detections:
|
for d in detections:
|
||||||
for entry in detections[d]:
|
for entry in detections[d]:
|
||||||
if entry[1] >= min_conf and ((entry[0] in INCLUDE_LIST or len(INCLUDE_LIST) == 0)
|
if entry[1] >= min_conf and ((entry[0] in INCLUDE_LIST or len(INCLUDE_LIST) == 0) and (entry[0] not in EXCLUDE_LIST or len(EXCLUDE_LIST) == 0) ):
|
||||||
and (entry[0] not in EXCLUDE_LIST or len(EXCLUDE_LIST) == 0)):
|
sendAppriseNotifications(str(entry[0]),str(entry[1]));
|
||||||
rfile.write(d + ';' + entry[0].replace('_', ';') + ';' + str(entry[1]) + '\n')
|
rfile.write(d + ';' + entry[0].replace('_', ';') + ';' + str(entry[1]) + '\n')
|
||||||
rcnt += 1
|
rcnt += 1
|
||||||
print('DONE! WROTE', rcnt, 'RESULTS.')
|
print('DONE! WROTE', rcnt, 'RESULTS.')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Sends a notification if a new species is detected
|
# Sends a notification if a new species is detected
|
||||||
#set -x
|
|
||||||
trap 'rm -f $lastcheck' EXIT
|
trap 'rm -f $lastcheck' EXIT
|
||||||
source /etc/birdnet/birdnet.conf
|
source /etc/birdnet/birdnet.conf
|
||||||
|
|
||||||
@@ -21,13 +20,8 @@ if ! diff ${IDFILE} ${lastcheck} &> /dev/null;then
|
|||||||
echo "Sending the following notification:
|
echo "Sending the following notification:
|
||||||
${NOTIFICATION}"
|
${NOTIFICATION}"
|
||||||
|
|
||||||
if [ ! -z ${PUSHED_APP_KEY} ];then
|
if [ ! -s $HOME/BirdNET-Pi/apprise.txt ];then
|
||||||
curl -X POST \
|
$HOME/BirdNET-Pi/birdnet/bin/apprise -vv -t 'New Species Detected' -b "${NOTIFICATION}" --config=$HOME/BirdNET-Pi/apprise.txt
|
||||||
--form-string "app_key=${PUSHED_APP_KEY}" \
|
|
||||||
--form-string "app_secret=${PUSHED_APP_SECRET}" \
|
|
||||||
--form-string "target_type=app" \
|
|
||||||
--form-string "content=${NOTIFICATION}" \
|
|
||||||
https://api.pushed.co/1/push
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -52,6 +52,11 @@ if(isset($_GET['species'])){
|
|||||||
}
|
}
|
||||||
$result3 = $statement3->execute();
|
$result3 = $statement3->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||||
|
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||||
|
$home = trim($home);
|
||||||
|
file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "");
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -157,6 +162,10 @@ while($results=$result->fetchArray(SQLITE3_ASSOC))
|
|||||||
$comname = preg_replace('/ /', '_', $results['Com_Name']);
|
$comname = preg_replace('/ /', '_', $results['Com_Name']);
|
||||||
$comname = preg_replace('/\'/', '', $comname);
|
$comname = preg_replace('/\'/', '', $comname);
|
||||||
$filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
|
$filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
|
||||||
|
|
||||||
|
$excludefile = fopen($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "a") or die("Unable to open file!");
|
||||||
|
$txt = $results['Date']."/".$comname."/".$results['File_Name']."\n".$results['Date']."/".$comname."/".$results['File_Name'].".png\n";
|
||||||
|
fwrite($excludefile, $txt);
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<form action="" method="GET">
|
<form action="" method="GET">
|
||||||
@@ -175,4 +184,3 @@ $filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,30 @@ if ! grep python3 <(head -n1 $my_dir/analyze.py) &>/dev/null;then
|
|||||||
echo "Ensure all python scripts use the virtual environment"
|
echo "Ensure all python scripts use the virtual environment"
|
||||||
sudo -u$USER sed -si "1 i\\#\!$HOME/BirdNET-Pi/birdnet/bin/python3" $my_dir/*.py
|
sudo -u$USER sed -si "1 i\\#\!$HOME/BirdNET-Pi/birdnet/bin/python3" $my_dir/*.py
|
||||||
fi
|
fi
|
||||||
if ! grep PRIVACY_MODE /etc/birdnet/birdnet.conf &>/dev/null;then
|
if ! grep PRIVACY_THRESHOLD /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||||
sudo -u$USER echo "PRIVACY_MODE=off" >> /etc/birdnet/birdnet.conf
|
sudo -u$USER echo "PRIVACY_THRESHOLD=0" >> /etc/birdnet/birdnet.conf
|
||||||
|
git -C $HOME/BirdNET-Pi rm $my_dir/privacy_server.py
|
||||||
|
fi
|
||||||
|
if grep privacy ~/BirdNET-Pi/templates/birdnet_server.service &>/dev/null;then
|
||||||
|
sudo -E sed -i 's/privacy_server.py/server.py/g' \
|
||||||
|
~/BirdNET-Pi/templates/birdnet_server.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
restart_services.sh
|
||||||
|
fi
|
||||||
|
if ! grep APPRISE_NOTIFICATION_TITLE /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||||
|
sudo -u$USER echo "APPRISE_NOTIFICATION_TITLE=\"New BirdNET-Pi Detection\"" >> /etc/birdnet/birdnet.conf
|
||||||
|
fi
|
||||||
|
if ! grep APPRISE_NOTIFICATION_BODY /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||||
|
sudo -u$USER echo "APPRISE_NOTIFICATION_BODY=\"A \$sciname \$comname was just detected with a confidence of \$confidence\"" >> /etc/birdnet/birdnet.conf
|
||||||
|
fi
|
||||||
|
if ! grep APPRISE_NOTIFY_EACH_DETECTION /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||||
|
sudo -u$USER echo "APPRISE_NOTIFY_EACH_DETECTION=false" >> /etc/birdnet/birdnet.conf
|
||||||
fi
|
fi
|
||||||
if ! which lsof &>/dev/null;then
|
if ! which lsof &>/dev/null;then
|
||||||
sudo apt update && sudo apt -y install lsof
|
sudo apt update && sudo apt -y install lsof
|
||||||
fi
|
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
|
||||||
|
~/BirdNET-Pi/birdnet/bin/pip3 install -U pip
|
||||||
|
~/BirdNET-Pi/birdnet/bin/pip3 install apprise
|
||||||
|
fi
|
||||||
@@ -2,4 +2,6 @@
|
|||||||
# Update the species list
|
# Update the species list
|
||||||
#set -x
|
#set -x
|
||||||
source /etc/birdnet/birdnet.conf
|
source /etc/birdnet/birdnet.conf
|
||||||
|
if [ -f $HOME/BirdNET-Pi/scripts/birds.db ];then
|
||||||
sqlite3 $HOME/BirdNET-Pi/scripts/birds.db "SELECT DISTINCT(Com_Name) FROM detections" | sort > ${IDFILE}
|
sqlite3 $HOME/BirdNET-Pi/scripts/birds.db "SELECT DISTINCT(Com_Name) FROM detections" | sort > ${IDFILE}
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user