Merge branch 'main' into python-lint

This commit is contained in:
Jake Herbst
2022-05-11 08:47:40 -04:00
17 changed files with 263 additions and 657 deletions
+2 -1
View File
@@ -1,5 +1,5 @@
<h1 align="center">
BirdNET-Pi <img src="https://img.shields.io/badge/Version-0.13-pink" />
BirdNET-Pi
</h1>
<p align="center">
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
- Australia
- Canada
- Switzerland
## Features
* 24/7 recording and BirdNET-Lite analysis
+11 -14
View File
@@ -56,17 +56,11 @@ BIRDNETPI_URL=
RTSP_STREAM=
#------------------- Mobile Notifications via Pushed.co ---------------------#
#____________The two variables below enable mobile notifications_______________#
#_____________See https://pushed.co/quick-start-guide to get___________________#
#_________________________these values for your app.___________________________#
#----------------------- Apprise Miscellanous Configuration -------------------#
# Keep these EMPTY if haven't setup a Pushed.co App yet. #
## Pushed.co App Key and App Secret
PUSHED_APP_KEY=
PUSHED_APP_SECRET=
APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection"
APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence"
APPRISE_NOTIFY_EACH_DETECTION=false
################################################################################
#-------------------------------- Defaults ----------------------------------#
@@ -120,11 +114,14 @@ CHANNELS=2
FULL_DISK=purge
## 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_HUMAN confidence score.
## PRIVACY_THRESHOLD can be set to enable sensitivity to Human sounds. This
## setting is an effort to introduce privacy into the data collection.
## 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
## analyze.
+30
View File
@@ -349,6 +349,36 @@ button:hover {
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 {
# display:none
#}
Binary file not shown.
+1
View File
@@ -10,3 +10,4 @@ pandas
seaborn
streamlit
plotly
apprise
+26 -29
View File
@@ -109,27 +109,12 @@ if(isset($_GET['submit'])) {
}
}
if(isset($_GET["privacy_mode"])) {
$privacy_mode = $_GET["privacy_mode"];
if(strcmp($config['PRIVACY_MODE'], "1") == 0 ) {
$pmode = "on";
}elseif(strcmp($config['PRIVACY_MODE'], "") == 0) {
$pmode = "off";
}
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');
}
if(isset($_GET["privacy_threshold"])) {
$privacy_threshold = $_GET["privacy_threshold"];
if(strcmp($privacy_threshold,$config['PRIVACY_THRESHOLD']) !== 0) {
$contents = preg_replace("/PRIVACY_THRESHOLD=.*/", "PRIVACY_THRESHOLD=$privacy_threshold", $contents);
$contents2 = preg_replace("/PRIVACY_THRESHOLD=.*/", "PRIVACY_THRESHOLD=$privacy_threshold", $contents2);
exec('restart_services.sh');
}
}
@@ -178,6 +163,9 @@ if(isset($_GET['submit'])) {
fwrite($fh, $contents);
fwrite($fh2, $contents2);
}
$count_labels = count(file("./scripts/labels.txt"));
$count = $count_labels;
?>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@@ -194,14 +182,23 @@ if (file_exists('./scripts/thisrun.txt')) {
?>
<h2>Advanced Settings</h2>
<form action="" method="GET">
<label>Privacy Mode: </label>
<label for="on">
<input name="privacy_mode" type="radio" id="on" value="on" <?php if (strcmp($newconfig['PRIVACY_MODE'], "1") == 0) { echo "checked"; }?>>On</label>
<label for="off">
<input name="privacy_mode" type="radio" id="off" value="off" <?php if (strcmp($newconfig['PRIVACY_MODE'], "") == 0) { echo "checked"; }?>>Off</label>
<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.
Please note that changing this setting restarts services and replaces the running server. It will take about 90, so please be patient!</p>
<label>Privacy Threshold: </label><br>
<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>
</div>
<script>
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 for="purge">
<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>
<br><br>
<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
if(isset($_GET['submit'])){
echo "Success!";
+4 -7
View File
@@ -95,11 +95,8 @@ run_analysis() {
echo "${1}/${i}" > $HOME/BirdNET-Pi/analyzing_now.txt
[ -z ${RECORDING_LENGTH} ] && RECORDING_LENGTH=15
echo "RECORDING_LENGTH set to ${RECORDING_LENGTH}"
a=0
until [ -z "$(lsof -t ${1}/${i})" ];do
sleep 2
[ $a -ge ${RECORDING_LENGTH} ] && rm -f ${1}/${i} && break
a=$((a+2))
done
if ! grep 5050 <(netstat -tulpn 2>&1) &> /dev/null 2>&1;then
@@ -108,19 +105,19 @@ run_analysis() {
sleep 1
done
fi
# prepare optional parameters for analyse.py
# prepare optional parameters for analyze.py
if [ -f ${INCLUDE_LIST} ]; then
INCLUDEPARAM="--include_list \"${INCLUDE_LIST}\""
INCLUDEPARAM="--include_list ${INCLUDE_LIST}"
else
INCLUDEPARAM=""
fi
if [ -f ${EXCLUDE_LIST} ]; then
EXCLUDEPARAM="--include_list \"${EXCLUDE_LIST}\""
EXCLUDEPARAM="--exclude_list ${EXCLUDE_LIST}"
else
EXCLUDEPARAM=""
fi
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\""
else
BIRDWEATHER_ID_PARAM=""
+59 -13
View File
@@ -7,28 +7,54 @@ if(isset($_GET["latitude"])){
$latitude = $_GET["latitude"];
$longitude = $_GET["longitude"];
$birdweather_id = $_GET["birdweather_id"];
$pushed_app_key = $_GET["pushed_app_key"];
$pushed_app_secret = $_GET["pushed_app_secret"];
$apprise_input = $_GET['apprise_input'];
$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 = preg_replace("/LATITUDE=.*/", "LATITUDE=$latitude", $contents);
$contents = preg_replace("/LONGITUDE=.*/", "LONGITUDE=$longitude", $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("/PUSHED_APP_SECRET=.*/", "PUSHED_APP_SECRET=$pushed_app_secret", $contents);
$contents = preg_replace("/APPRISE_NOTIFICATION_TITLE=.*/", "APPRISE_NOTIFICATION_TITLE=\"$apprise_notification_title\"", $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 = preg_replace("/LATITUDE=.*/", "LATITUDE=$latitude", $contents2);
$contents2 = preg_replace("/LONGITUDE=.*/", "LONGITUDE=$longitude", $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("/PUSHED_APP_SECRET=.*/", "PUSHED_APP_SECRET=$pushed_app_secret", $contents2);
$contents2 = preg_replace("/APPRISE_NOTIFICATION_TITLE=.*/", "APPRISE_NOTIFICATION_TITLE=\"$apprise_notification_title\"", $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");
$fh2 = fopen("./scripts/thisrun.txt", "w");
fwrite($fh, $contents);
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"];
if ($language != "none"){
$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')) {
$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"');
@@ -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>
<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>
<label for="pushed_app_key">Pushed App Key: </label>
<input name="pushed_app_key" type="text" value="<?php print($config['PUSHED_APP_KEY']);?>" /><br>
<label for="pushed_app_secret">Pushed App Secret: </label>
<input name="pushed_app_secret" type="text" value="<?php print($config['PUSHED_APP_SECRET']);?>" /><br>
<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>
<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>
<h3>Notifications</h3>
<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="apprise_input">Apprise Notifications Configuration: </label>
<textarea placeholder="mailto://{user}:{password}@gmail.com
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>
<select name="language">
<option value="none">Select your language</option>
@@ -120,7 +166,7 @@ if (!isset($_SERVER['PHP_AUTH_USER'])) {
<br><br>
<input type="hidden" name="status" value="success">
<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
if(isset($_GET['status'])){
echo "Success!";
+17 -2
View File
@@ -7,8 +7,23 @@ if [ "${used//%}" -ge 95 ]; then
case $FULL_DISK in
purge) echo "Removing oldest data"
rm -drfv "$(find ${EXTRACTED}/By_Date/* -maxdepth 1 -type d -prune \
| sort -r | tail -n1)";;
cd ${EXTRACTED}/By_Date/
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"
/usr/local/bin/stop_core_services.sh;;
esac
+11 -14
View File
@@ -69,17 +69,11 @@ BIRDNETPI_URL=
RTSP_STREAM=
#------------------- Mobile Notifications via Pushed.co ---------------------#
#____________The two variables below enable mobile notifications_______________#
#_____________See https://pushed.co/quick-start-guide to get___________________#
#_________________________these values for your app.___________________________#
#----------------------- Apprise Miscellanous Configuration -------------------#
# Keep these EMPTY if haven't setup a Pushed.co App yet. #
## Pushed.co App Key and App Secret
PUSHED_APP_KEY=
PUSHED_APP_SECRET=
APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection"
APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence"
APPRISE_NOTIFY_EACH_DETECTION=false
################################################################################
#-------------------------------- Defaults ----------------------------------#
@@ -133,11 +127,14 @@ CHANNELS=2
FULL_DISK=purge
## 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_HUMAN confidence score.
## PRIVACY_THRESHOLD can be set to enable sensitivity to Human sounds. This
## setting is an effort to introduce privacy into the data collection.
## 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
## analyze.
+1 -1
View File
@@ -192,7 +192,7 @@ function loadDetectionIfNewExists(previous_detection_identifier=undefined) {
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) {
if(this.responseText.length > 0 && !this.responseText.includes("Database is busy.")) {
document.getElementById("most_recent_detection").innerHTML = this.responseText;
// only going to load left chart if there's a new detection
-540
View File
@@ -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
View File
@@ -1,17 +1,20 @@
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 argparse
import operator
import socket
import threading
import os
import librosa
import numpy as np
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['CUDA_VISIBLE_DEVICES'] = ''
@@ -20,7 +23,6 @@ try:
except BaseException:
from tensorflow import lite as tflite
HEADER = 64
PORT = 5050
SERVER = socket.gethostbyname(socket.gethostname())
@@ -41,6 +43,7 @@ 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])
priv_thresh = float("." + str(str(str([i for i in this_run if i.startswith('PRIVACY_THRESHOLD')]).split('=')[1]).split('\\')[0]))/10
def loadModel():
@@ -165,13 +168,19 @@ def predict(sample, sensitivity):
# 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 ['Human_Human', 'Non-bird_Non-bird', 'Noise_Noise']:
p_sorted[i] = (p_sorted[i][0], 0.0)
# #print("DATABASE SIZE:", len(p_sorted))
# #print("HUMAN-CUTOFF AT:", int(len(p_sorted)*priv_thresh)/10)
#
# # Remove species that are on blacklist
# Only return first the top ten results
return p_sorted[:10]
human_cutoff = max(10,int(len(p_sorted)*priv_thresh))
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,):
@@ -194,16 +203,47 @@ def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,):
# Make prediction
p = predict([sig, mdata], sensitivity)
# print("PPPPP",p)
HUMAN_DETECTED=False
#Catch if Human is recognized
for x in range(len(p)):
if "Human" in p[x][0]:
HUMAN_DETECTED=True
# Save result and timestamp
pred_end = pred_start + 3.0
#If human detected set all detections to human to make sure voices are not saved
if HUMAN_DETECTED == True:
p=[('Human_Human',0.0)]*10
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 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):
@@ -213,8 +253,8 @@ def writeResultsToFile(detections, min_conf, path):
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)):
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) ):
sendAppriseNotifications(str(entry[0]),str(entry[1]));
rfile.write(d + ';' + entry[0].replace('_', ';') + ';' + str(entry[1]) + '\n')
rcnt += 1
print('DONE! WROTE', rcnt, 'RESULTS.')
+2 -8
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env bash
# Sends a notification if a new species is detected
#set -x
trap 'rm -f $lastcheck' EXIT
source /etc/birdnet/birdnet.conf
@@ -21,13 +20,8 @@ if ! diff ${IDFILE} ${lastcheck} &> /dev/null;then
echo "Sending the following notification:
${NOTIFICATION}"
if [ ! -z ${PUSHED_APP_KEY} ];then
curl -X POST \
--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
if [ ! -s $HOME/BirdNET-Pi/apprise.txt ];then
$HOME/BirdNET-Pi/birdnet/bin/apprise -vv -t 'New Species Detected' -b "${NOTIFICATION}" --config=$HOME/BirdNET-Pi/apprise.txt
fi
fi
+9 -1
View File
@@ -52,6 +52,11 @@ if(isset($_GET['species'])){
}
$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">
@@ -157,6 +162,10 @@ while($results=$result->fetchArray(SQLITE3_ASSOC))
$comname = preg_replace('/ /', '_', $results['Com_Name']);
$comname = preg_replace('/\'/', '', $comname);
$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>
<form action="" method="GET">
@@ -175,4 +184,3 @@ $filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
</div>
</body>
</html>
+23 -2
View File
@@ -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"
sudo -u$USER sed -si "1 i\\#\!$HOME/BirdNET-Pi/birdnet/bin/python3" $my_dir/*.py
fi
if ! grep PRIVACY_MODE /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "PRIVACY_MODE=off" >> /etc/birdnet/birdnet.conf
if ! grep PRIVACY_THRESHOLD /etc/birdnet/birdnet.conf &>/dev/null;then
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
if ! which lsof &>/dev/null;then
sudo apt update && sudo apt -y install lsof
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
View File
@@ -2,4 +2,6 @@
# Update the species list
#set -x
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}
fi