Merge branch 'mcguirepr89:main' into main

This commit is contained in:
lloydbayley
2023-02-17 21:14:13 +11:00
committed by GitHub
23 changed files with 599 additions and 24 deletions
+11 -5
View File
@@ -4,7 +4,7 @@
BirdNET-Pi
</h1>
<p align="center">
A realtime acoustic bird classification system for the Raspberry Pi 4B, 3B+, and 0W2
A realtime acoustic bird classification system for the Raspberry Pi 4B, 400, 3B+, and 0W2
</p>
<p align="center">
<img src="https://user-images.githubusercontent.com/60325264/140656397-bf76bad4-f110-467c-897d-992ff0f96476.png" />
@@ -64,9 +64,9 @@ Currently listening in these countries . . . that I know of . . .
- Brazil
## Features
* 24/7 recording and BirdNET-Lite analysis
* Automatic extraction of detected data (creating audio clips of detected bird sounds)
* Spectrograms available for all extractions
* 24/7 recording and automatic identification of bird songs using BirdNET machine learning
* Bird sounds are automatically extracted and catalogued
* Visualize your recorded bird data and analyze trends
* Live audio stream & spectrogram
* Automatic disk space management, where old audio files are periodically purged
* [BirdWeather](https://app.birdweather.com) integration -- you can request a BirdWeather ID from BirdNET-Pi's "Tools" > "Settings" page
@@ -81,7 +81,7 @@ Currently listening in these countries . . . that I know of . . .
* Localization supported
## Requirements
* A Raspberry Pi 4B, Raspberry Pi 3B+, or Raspberry Pi 0W2 (The 3B+ and 0W2 must run on RaspiOS-ARM64-**Lite**)
* A Raspberry Pi 4B, Raspberry Pi 400, Raspberry Pi 3B+, or Raspberry Pi 0W2 (The 3B+ and 0W2 must run on RaspiOS-ARM64-**Lite**)
* An SD Card with the **_64-bit version of RaspiOS_** installed (please use Bullseye) -- Lite is recommended, but the installation works on RaspiOS-ARM64-Full as well. Downloads available within the [Raspberry Pi Imager](https://www.raspberrypi.com/software/).
* A USB Microphone or Sound Card
@@ -106,6 +106,7 @@ The BirdNET-Pi can be accessed from any web browser on the same network:
- Password is empty by default. Set this in "Tools" > "Settings" > "Advanced Settings"
Please take a look at the [wiki](https://github.com/mcguirepr89/BirdNET-Pi/wiki) and [discussions](https://github.com/mcguirepr89/BirdNET-Pi/discussions) for information on
- [BirdNET-Pi's Deep Convolutional Neural Network(s)](https://github.com/mcguirepr89/BirdNET-Pi/wiki/BirdNET-Pi:-some-theory-on-classification-&-some-practical-hints)
- [making your installation public](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi)
- [backing up and restoring your database](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Backup-and-Restore-the-Database)
- [adjusting your sound card settings](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Adjusting-your-sound-card)
@@ -180,5 +181,10 @@ Current database languages include the list below:
| Thai | 5580 | 87.71% |
| Ukrainian | 646 | 10.15% |
## Screenshots
![chrome_olUUgVo1Ka](https://user-images.githubusercontent.com/103586016/219236461-c717e88b-134f-4916-a691-eb7c055c55bf.png)
![chrome_HNMJKSPwV0](https://user-images.githubusercontent.com/103586016/217896322-aee3ecc4-e40e-40df-ade1-79f05ded21f2.png)
## :thinking:
Are you a lucky ducky with an extra Raspberry Pi 4B lying around? [Here's an idea!](https://foldingathome.org/alternative-downloads)
+7
View File
@@ -6,6 +6,13 @@
SITE_NAME=""
#--------------------------------- Model --------------------------------------#
#_____________The variable below configures which BirdNET model is_____________#
#______________________used for detecting bird audio.__________________________#
#_It's recommended that you only change these values through the web interface.#
MODEL=BirdNET_6K_GLOBAL_MODEL
SF_THRESH=0.03
#--------------------- Required: Latitude, and Longitude ----------------------#
+4
View File
@@ -812,4 +812,8 @@ pre#timer.bash {
line-height:5px;
padding:3px;
background-color:#9fe29b
}
#ddnewline::before {
content: none;
}
+5 -1
View File
@@ -6,7 +6,11 @@ $home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
if(!isset($_SESSION['behind'])) {
$fetch = shell_exec("sudo -u".$user." git -C ".$home."/BirdNET-Pi fetch 2>&1");
$_SESSION['behind'] = trim(shell_exec("sudo -u".$user." git -C ".$home."/BirdNET-Pi status | sed -n '2 p' | cut -d ' ' -f 7"));
$str = trim(shell_exec("sudo -u".$user." git -C ".$home."/BirdNET-Pi status"));
if (preg_match("/behind '.*?' by (\d+) commit(s?)\b/", $str, $matches)) {
$num_commits_behind = $matches[1];
$_SESSION['behind'] = $num_commits_behind;
}
if(isset($_SESSION['behind'])&&intval($_SESSION['behind']) >= 99) {?>
<style>
.updatenumber {
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -277,7 +277,7 @@ foreach($formats as $format){
<h3>BirdNET-Pi Password</h3>
<p>This password will protect your "Tools" page and "Live Audio" stream.</p>
<label for="caddy_pwd">Password: </label>
<input style="width:40ch" name="caddy_pwd" id="caddy_pwd" type="password" value="<?php print($newconfig['CADDY_PWD']);?>" /><span id="showpassword" onmouseover="document.getElementById('caddy_pwd').type='text';" onmouseout="document.getElementById('caddy_pwd').type='password';">show</span><br>
<input style="width:40ch" name="caddy_pwd" id="caddy_pwd" type="password" pattern="[A-Za-z0-9]+" title="Password must be alphanumeric (A-Z, 0-9)" value="<?php print($newconfig['CADDY_PWD']);?>" /><span id="showpassword" onmouseover="document.getElementById('caddy_pwd').type='text';" onmouseout="document.getElementById('caddy_pwd').type='password';">show</span><br>
<h3>Custom URL</h3>
<p>When you update the URL below, the web server will reload, so be sure to wait at least 30 seconds and then go to your new URL.</p>
<label for="birdnetpi_url">BirdNET-Pi URL: </label>
+177 -1
View File
@@ -13,6 +13,23 @@ function syslog_shell_exec($cmd, $sudo_user = null) {
}
}
if(isset($_GET['threshold'])) {
$threshold = $_GET['threshold'];
if (!is_numeric($threshold) || $threshold < 0 || $threshold > 1) {
die('Invalid threshold value');
}
$user = trim(shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"));
$home = trim(shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"));
$command = "sudo -u $user ".$home."/BirdNET-Pi/birdnet/bin/python3 ".$home."/BirdNET-Pi/scripts/species.py --threshold $threshold 2>&1";
$output = shell_exec($command);
echo $output;
die();
}
if(isset($_GET['restart_php']) && $_GET['restart_php'] == "true") {
shell_exec("sudo service php7.4-fpm restart");
die();
@@ -34,6 +51,8 @@ if(isset($_GET["latitude"])){
$flickr_filter_email = $_GET["flickr_filter_email"];
$language = $_GET["language"];
$timezone = $_GET["timezone"];
$model = $_GET["model"];
$sf_thresh = $_GET["sf_thresh"];
if(isset($_GET['apprise_notify_each_detection'])) {
$apprise_notify_each_detection = 1;
@@ -100,6 +119,23 @@ if(isset($_GET["latitude"])){
syslog(LOG_INFO, "Successfully changed language to '$language'");
}
if ($model != $lang_config['MODEL']){
$user = trim(shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"));
$home = trim(shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"));
// Archive old language file
syslog_shell_exec("cp -f $home/BirdNET-Pi/model/labels.txt $home/BirdNET-Pi/model/labels.txt.old", $user);
if($model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16"){
// Install new language label file
syslog_shell_exec("sudo chmod +x $home/BirdNET-Pi/scripts/install_language_label_nm.sh && $home/BirdNET-Pi/scripts/install_language_label_nm.sh -l $language", $user);
} else {
syslog_shell_exec("$home/BirdNET-Pi/scripts/install_language_label.sh -l $language", $user);
}
syslog(LOG_INFO, "Successfully changed language to '$language'");
}
$contents = file_get_contents("/etc/birdnet/birdnet.conf");
$contents = preg_replace("/SITE_NAME=.*/", "SITE_NAME=\"$site_name\"", $contents);
@@ -116,6 +152,8 @@ if(isset($_GET["latitude"])){
$contents = preg_replace("/DATABASE_LANG=.*/", "DATABASE_LANG=$language", $contents);
$contents = preg_replace("/FLICKR_FILTER_EMAIL=.*/", "FLICKR_FILTER_EMAIL=$flickr_filter_email", $contents);
$contents = preg_replace("/APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=.*/", "APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=$minimum_time_limit", $contents);
$contents = preg_replace("/MODEL=.*/", "MODEL=$model", $contents);
$contents = preg_replace("/SF_THRESH=.*/", "SF_THRESH=$sf_thresh", $contents);
$contents2 = file_get_contents("./scripts/thisrun.txt");
$contents2 = preg_replace("/SITE_NAME=.*/", "SITE_NAME=\"$site_name\"", $contents2);
@@ -132,6 +170,9 @@ if(isset($_GET["latitude"])){
$contents2 = preg_replace("/DATABASE_LANG=.*/", "DATABASE_LANG=$language", $contents2);
$contents2 = preg_replace("/FLICKR_FILTER_EMAIL=.*/", "FLICKR_FILTER_EMAIL=$flickr_filter_email", $contents2);
$contents2 = preg_replace("/APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=.*/", "APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=$minimum_time_limit", $contents2);
$contents2 = preg_replace("/MODEL=.*/", "MODEL=$model", $contents2);
$contents2 = preg_replace("/SF_THRESH=.*/", "SF_THRESH=$sf_thresh", $contents2);
if($site_name != $config["SITE_NAME"]) {
@@ -239,7 +280,7 @@ if(isset($_GET['sendtest']) && $_GET['sendtest'] == "true") {
$body = str_replace("\$overlap", $overlap, $body);
$body = str_replace("\$flickrimage", $exampleimage, $body);
echo "<pre class=\"bash\">".shell_exec($home."/BirdNET-Pi/birdnet/bin/apprise -vv -t '".$title."' -b '".$body."' ".$attach." ".$cf." ")."</pre>";
echo "<pre class=\"bash\">".shell_exec($home."/BirdNET-Pi/birdnet/bin/apprise -vv --plugin-path ".$home."/.apprise/plugins "." -t '".$title."' -b '".$body."' ".$attach." ".$cf." ")."</pre>";
die();
}
@@ -285,6 +326,15 @@ if (!isset($_SERVER['PHP_AUTH_USER'])) {
?>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('modelsel').addEventListener('change', function() {
if(this.value == "BirdNET_GLOBAL_3K_V2.2_Model_FP16"){
document.getElementById("soft").style.display="unset";
} else {
document.getElementById("soft").style.display="none";
}
});
}, false);
function sendTestNotification(e) {
document.getElementById("testsuccessmsg").innerHTML = "";
e.classList.add("disabled");
@@ -304,6 +354,131 @@ function sendTestNotification(e) {
xmlHttp.send(null);
}
</script>
<table class="settingstable"><tr><td>
<h2>Model</h2>
<label for="model">Select a Model: </label>
<select id="modelsel" name="model">
<?php
$models = array("BirdNET_6K_GLOBAL_MODEL", "BirdNET_GLOBAL_3K_V2.2_Model_FP16");
foreach($models as $modelName){
$isSelected = "";
if($config['MODEL'] == $modelName){
$isSelected = 'selected="selected"';
}
echo "<option value='{$modelName}' $isSelected>$modelName</option>";
}
?>
</select>
<br>
<span <?php if($config['MODEL'] == "BirdNET_6K_GLOBAL_MODEL") { ?>style="display: none"<?php } ?> id="soft">
<label for="sf_thresh">Species Occurence Frequency Threshold [0.0005, 0.99]: </label>
<input name="sf_thresh" type="number" max="0.99" min="0.0005" step="any" value="<?php print($config['SF_THRESH']);?>"/> <span onclick="document.getElementById('sfhelp').style.display='unset'" style="text-decoration:underline;cursor:pointer">[more info]</span><br>
<p id="sfhelp" style='display:none'>This value is used by the model to constrain the list of possible species that it will try to detect, given the minimum occurence frequency. A 0.03 threshold means that for a species to be included in this list, it needs to, on average, be seen on at least 3% of historically submitted eBird checklists for your given lat/lon/current week of year. So, the lower the threshold, the rarer the species it will include.<br><br>If you'd like to tinker with this threshold value and see which species make it onto the list, <?php if($config['MODEL'] == "BirdNET_6K_GLOBAL_MODEL"){ ?>please click "Update Settings" at the very bottom of this page to install the appropriate label file, then come back here and you'll be able to use the Species List Tester.<?php } else { ?>you can use this tool: <button type="button" class="testbtn" id="openModal">Species List Tester</button><?php } ?></p>
</span>
<script src="static/dialog-polyfill.js"></script>
<dialog id="modal">
<div>
<label for="threshold">Threshold:</label>
<input type="number" id="threshold" step="0.01" min="0" max="1" value="">
<button type="button" id="runProcess">Preview Species List</button>
</div>
<pre id="output"></pre>
<button type="button" id="closeModal">Close</button>
</dialog>
<style>
#output {
max-width: 100vw;
word-wrap: break-word;
white-space: pre-wrap;
}
#modal {
max-height: 80vh;
overflow-y: auto;
}
#modal div {
display: flex;
align-items: center;
}
#modal input[type="number"] {
height: 32px;
}
#modal button {
height: 32px;
margin-left: 5px;
padding: 0 10px;
box-sizing: border-box;
}
</style>
<script>
// Get the button and modal elements
const openModalBtn = document.getElementById('openModal');
const modal = document.getElementById('modal');
dialogPolyfill.registerDialog(modal);
const output = document.getElementById('output');
const thresholdInput = document.getElementById('threshold');
const runProcessBtn = document.getElementById('runProcess');
const sfThreshInput = document.getElementsByName('sf_thresh')[0];
const closeModalBtn = document.getElementById('closeModal');
// Add an event listener to the button to open the modal
openModalBtn.addEventListener('click', () => {
// Set the initial value of the threshold input element
thresholdInput.value = sfThreshInput.value;
// Show the modal
modal.showModal();
});
// Add an event listener to the "Preview Species List" button
runProcessBtn.addEventListener('click', () => {
runProcess();
});
// Add an event listener to the "Close" button
closeModalBtn.addEventListener('click', () => {
modal.close();
});
// Function to run the process
function runProcess() {
// Get the value of the threshold input element
const threshold = thresholdInput.value;
// Set the output to "Loading..."
output.innerHTML = "Loading...";
// Make the AJAX request
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
// Handle the response
output.innerHTML = xhr.responseText;
}
};
xhr.open('GET', `scripts/config.php?threshold=${threshold}`);
xhr.send();
}
</script>
<dl>
<dt>BirdNET_6K_GLOBAL_MODEL (2020)</dt><br>
<dd id="ddnewline">This model comes from BirdNET-Lite, with bird sound recognition for more than 6,000 species worldwide. This is the default option and will generally work very well for most use cases.</dd>
<dt>BirdNET_GLOBAL_3K_V2.2_Model_FP16 (2022)</dt><br>
<dd id="ddnewline">This model comes from BirdNET-Analyzer, a newer work-in-progress project with aims to improve on the old model. Currently it only supports about 3,500 species worldwide, so for users in North America, this model is generally much more accurate than the above model, but elsewhere it will be less accurate and possibly useless.</dd>
</dl>
</td></tr></table><br>
<table class="settingstable"><tr><td>
<h2>Location</h2>
@@ -511,3 +686,4 @@ if(isset($_GET['status'])){
<button type="submit" name="view" value="Advanced">Advanced Settings</button>
</form>
</div>
+2
View File
@@ -15,6 +15,8 @@ CREATE TABLE IF NOT EXISTS detections (
Sens FLOAT,
Overlap FLOAT,
File_Name VARCHAR(100) NOT NULL);
CREATE INDEX "detections_Com_Name" ON "detections" ("Com_Name");
CREATE INDEX "detections_Date_Time" ON "detections" ("Date" DESC, "Time" DESC);
EOF
chown $USER:$USER $HOME/BirdNET-Pi/scripts/birds.db
chmod g+w $HOME/BirdNET-Pi/scripts/birds.db
+2
View File
@@ -48,6 +48,8 @@ readings = 10
plt_top10_today = (df_plt_today['Com_Name'].value_counts()[:readings])
df_plt_top10_today = df_plt_today[df_plt_today.Com_Name.isin(plt_top10_today.index)]
if df_plt_top10_today.empty: exit(0)
# Set Palette for graphics
pal = "Greens"
+3
View File
@@ -9,6 +9,9 @@ if [ "${used//%}" -ge 95 ]; then
purge) echo "Removing oldest data"
cd ${EXTRACTED}/By_Date/
curl localhost/views.php?view=Species%20Stats &>/dev/null
if ! grep -qxFe \#\#start $HOME/BirdNET-Pi/scripts/disk_check_exclude.txt; then
exit
fi
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
+8
View File
@@ -27,6 +27,14 @@ SITE_NAME="$HOSTNAME"
LATITUDE=$(curl -s4 ifconfig.co/json | jq .latitude)
LONGITUDE=$(curl -s4 ifconfig.co/json | jq .longitude)
#--------------------------------- Model --------------------------------------#
#_____________The variable below configures which BirdNET model is_____________#
#______________________used for detecting bird audio.__________________________#
#_It's recommended that you only change these values through the web interface.#
MODEL=BirdNET_6K_GLOBAL_MODEL
SF_THRESH=0.03
#--------------------- BirdWeather Station Information -----------------------#
#_____________The variable below can be set to have your BirdNET-Pi____________#
#__________________also act as a BirdWeather listening station_________________#
+7
View File
@@ -23,4 +23,11 @@ unzip -o $HOME/BirdNET-Pi/model/labels_l18n.zip $label_file_name \
&& mv -f $HOME/BirdNET-Pi/model/$label_file_name $HOME/BirdNET-Pi/model/labels.txt \
&& logger "[$0] Changed language label file to '$label_file_name'";
label_file_name_flickr="labels_en.txt"
unzip -o $HOME/BirdNET-Pi/model/labels_l18n.zip $label_file_name_flickr \
-d $HOME/BirdNET-Pi/model \
&& mv -f $HOME/BirdNET-Pi/model/$label_file_name_flickr $HOME/BirdNET-Pi/model/labels_flickr.txt \
&& logger "[$0] Set Flickr labels '$label_file_name_flickr'";
exit 0
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
usage() { echo "Usage: $0 -l <language i18n id>" 1>&2; exit 1; }
while getopts "l:" o; do
case "${o}" in
l)
lang=${OPTARG}
;;
*)
usage
;;
esac
done
shift $((OPTIND-1))
HOME=$(awk -F: '/1000/ {print $6}' /etc/passwd)
label_file_name="labels_${lang}.txt"
unzip -o $HOME/BirdNET-Pi/model/labels_nm.zip $label_file_name \
-d $HOME/BirdNET-Pi/model \
&& mv -f $HOME/BirdNET-Pi/model/$label_file_name $HOME/BirdNET-Pi/model/labels.txt \
&& logger "[$0] Changed language label file to '$label_file_name'";
label_file_name_flickr="labels_en.txt"
unzip -o $HOME/BirdNET-Pi/model/labels_nm.zip $label_file_name_flickr \
-d $HOME/BirdNET-Pi/model \
&& mv -f $HOME/BirdNET-Pi/model/$label_file_name_flickr $HOME/BirdNET-Pi/model/labels_flickr.txt \
&& logger "[$0] Set Flickr labels '$label_file_name_flickr'";
exit 0
+15 -2
View File
@@ -398,6 +398,11 @@ if($statement2 == False){
header("refresh: 0;");
}
$result2 = $statement2->execute();
$num_rows = 0;
while ($result2->fetchArray(SQLITE3_ASSOC)) {
$num_rows++;
}
$result2->reset(); // reset the pointer to the beginning of the result set
echo "<table>
<tr>
<th>$name</th>
@@ -426,6 +431,12 @@ echo "<table>
}
$iter++;
if($num_rows < 100){
$imageelem = "<video onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster=\"$filename_png\" preload=\"none\" title=\"$filename\"><source src=\"$filename\"></video>";
} else {
$imageelem = "<a href=\"$filename\"><img src=\"$filename_png\"></a>";
}
if($config["FULL_DISK"] == "purge") {
if(!in_array($filename_formatted, $disk_check_exclude_arr)) {
$imageicon = "images/unlock.svg";
@@ -455,13 +466,15 @@ echo "<table>
<img style='cursor:pointer;right:45px' onclick='toggleLock(\"".$filename_formatted."\",\"".$type."\", this)' class=\"copyimage\" width=25 title=\"".$title."\" src=\"".$imageicon."\">
<img style='cursor:pointer' onclick='toggleShiftFreq(\"".$filename_formatted."\",\"".$shiftAction."\", this)' class=\"copyimage\" width=25 title=\"".$shiftTitle."\" src=\"".$shiftImageIcon."\"> $date $time<br>$confidence<br>
<video onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster=\"$filename_png\" preload=\"none\" title=\"$filename\"><source src=\"$filename\"></video></td>
".$imageelem."
</td>
</tr>";
} else {
echo "<tr>
<td class=\"relative\">$date $time<br>$confidence
<img style='cursor:pointer' src='images/delete.svg' onclick='deleteDetection(\"".$filename_formatted."\")' class=\"copyimage\" width=25 title='Delete Detection'><br>
<video onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster=\"$filename_png\" preload=\"none\" title=\"$filename\"><source src=\"$filename\"></video></td>
".$imageelem."
</td>
</tr>";
}
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# 1. Service status
services=("caddy" "birdnet_analysis" "birdnet_log" "birdnet_recording" "birdnet_server" "birdnet_stats" "chart_viewer" "extraction" "web_terminal" "spectrogram_viewer" "livestream")
for service in "${services[@]}"; do
echo "========== $service status =========="
sudo service $service status | cat
echo ""
done
# 2. Mounted file systems
echo "========== Mounted File Systems =========="
df -h
echo ""
# 3. Memory usage
echo "========== Memory Usage =========="
free -h
echo ""
# 4. Load averages
echo "========== Load Averages =========="
uptime
echo ""
# 5. CPU usage
echo "========== CPU Info =========="
cat /proc/cpuinfo
echo ""
# 6. System temperature
echo "========== System Temperature =========="
temp_c=$(cat /sys/class/thermal/thermal_zone0/temp | awk '{printf "%.2f", $1/1000}')
temp_f=$(echo "scale=2; 9/5*$temp_c+32" | bc)
echo "CPU Temperature: ${temp_c}°C / ${temp_f}°F"
echo ""
# 7. Output of /usr/local/bin/extra_info.sh
echo "========== Extra Info =========="
sudo /usr/local/bin/extra_info.sh
echo ""
# 8. Microphone devices
echo "========== Connected Microphone Devices =========="
arecord -l
echo ""
arecord -L
echo ""
echo "========= Date and Time =========="
date
echo "==========================================="
+94 -11
View File
@@ -35,6 +35,7 @@ DISCONNECT_MESSAGE = "!DISCONNECT"
userDir = os.path.expanduser('~')
DB_PATH = userDir + '/BirdNET-Pi/scripts/birds.db'
PREDICTED_SPECIES_LIST = []
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -51,7 +52,12 @@ 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
try:
model = str(str(str([i for i in this_run if i.startswith('MODEL')]).split('=')[1]).split('\\')[0])
sf_thresh = str(str(str([i for i in this_run if i.startswith('SF_THRESH')]).split('=')[1]).split('\\')[0])
except Exception as e:
model = "BirdNET_6K_GLOBAL_MODEL"
sf_thresh = 0.03
def loadModel():
@@ -63,7 +69,8 @@ def loadModel():
print('LOADING TF LITE MODEL...', end=' ')
# Load TFLite model and allocate tensors.
modelpath = userDir + '/BirdNET-Pi/model/BirdNET_6K_GLOBAL_MODEL.tflite'
# model will either be BirdNET_GLOBAL_3K_V2.2_Model_FP16 (new) or BirdNET_6K_GLOBAL_MODEL (old)
modelpath = userDir + '/BirdNET-Pi/model/'+model+'.tflite'
myinterpreter = tflite.Interpreter(model_path=modelpath, num_threads=2)
myinterpreter.allocate_tensors()
@@ -73,7 +80,8 @@ def loadModel():
# Get input tensor index
INPUT_LAYER_INDEX = input_details[0]['index']
MDATA_INPUT_INDEX = input_details[1]['index']
if model == "BirdNET_6K_GLOBAL_MODEL":
MDATA_INPUT_INDEX = input_details[1]['index']
OUTPUT_LAYER_INDEX = output_details[0]['index']
# Load labels
@@ -87,6 +95,70 @@ def loadModel():
return myinterpreter
def loadMetaModel():
global M_INTERPRETER
global M_INPUT_LAYER_INDEX
global M_OUTPUT_LAYER_INDEX
# Load TFLite model and allocate tensors.
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.2_MData_Model_FP16.tflite')
M_INTERPRETER.allocate_tensors()
# Get input and output tensors.
input_details = M_INTERPRETER.get_input_details()
output_details = M_INTERPRETER.get_output_details()
# Get input tensor index
M_INPUT_LAYER_INDEX = input_details[0]['index']
M_OUTPUT_LAYER_INDEX = output_details[0]['index']
print("loaded META model")
def predictFilter(lat, lon, week):
global M_INTERPRETER
# Does interpreter exist?
try:
if M_INTERPRETER == None:
loadMetaModel()
except Exception as e:
loadMetaModel()
# Prepare mdata as sample
sample = np.expand_dims(np.array([lat, lon, week], dtype='float32'), 0)
# Run inference
M_INTERPRETER.set_tensor(M_INPUT_LAYER_INDEX, sample)
M_INTERPRETER.invoke()
return M_INTERPRETER.get_tensor(M_OUTPUT_LAYER_INDEX)[0]
def explore(lat, lon, week):
# Make filter prediction
l_filter = predictFilter(lat, lon, week)
# Apply threshold
l_filter = np.where(l_filter >= float(sf_thresh), l_filter, 0)
# Zip with labels
l_filter = list(zip(l_filter, CLASSES))
# Sort by filter value
l_filter = sorted(l_filter, key=lambda x: x[0], reverse=True)
return l_filter
def predictSpeciesList(lat, lon, week):
l_filter = explore(lat, lon, week)
for s in l_filter:
if s[0] >= float(sf_thresh):
#if there's a custom user-made include list, we only want to use the species in that
if(len(INCLUDE_LIST) == 0):
PREDICTED_SPECIES_LIST.append(s[1])
def loadCustomSpeciesList(path):
@@ -162,7 +234,8 @@ 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'))
if model == "BirdNET_6K_GLOBAL_MODEL":
INTERPRETER.set_tensor(MDATA_INPUT_INDEX, np.array(sample[1], dtype='float32'))
INTERPRETER.invoke()
prediction = INTERPRETER.get_tensor(OUTPUT_LAYER_INDEX)[0]
@@ -197,6 +270,11 @@ def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,):
start = time.time()
print('ANALYZING AUDIO...', end=' ', flush=True)
if model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16":
if len(PREDICTED_SPECIES_LIST) == 0 or len(INCLUDE_LIST) != 0:
predictSpeciesList(lat,lon,week)
# Convert and prepare metadata
mdata = convertMetadata(np.array([lat, lon, week]))
mdata = np.expand_dims(mdata, 0)
@@ -242,8 +320,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)):
rfile.write(d + ';' + entry[0].replace('_', ';') + ';' + str(entry[1]) + '\n')
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] in PREDICTED_SPECIES_LIST or len(PREDICTED_SPECIES_LIST) == 0) ):
rfile.write(d + ';' + entry[0].replace('_', ';').split("/")[0] + ';' + str(entry[1]) + '\n')
rcnt += 1
print('DONE! WROTE', rcnt, 'RESULTS.')
return
@@ -364,16 +442,16 @@ def handle_client(conn, addr):
species_apprised_this_run = []
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)):
and (entry[0] not in EXCLUDE_LIST or len(EXCLUDE_LIST) == 0) and (entry[0] in PREDICTED_SPECIES_LIST or len(PREDICTED_SPECIES_LIST) == 0) ):
# Write to text file.
rfile.write(str(current_date) + ';' + str(current_time) + ';' + entry[0].replace('_', ';') + ';'
rfile.write(str(current_date) + ';' + str(current_time) + ';' + entry[0].replace('_', ';').split("/")[0] + ';'
+ str(entry[1]) + ";" + str(args.lat) + ';' + str(args.lon) + ';' + str(min_conf) + ';' + str(week) + ';'
+ str(args.sensitivity) + ';' + str(args.overlap) + '\n')
# Write to database
Date = str(current_date)
Time = str(current_time)
species = entry[0]
species = entry[0].split("/")[0]
Sci_Name, Com_Name = species.split('_')
score = entry[1]
Confidence = str(round(score * 100))
@@ -475,9 +553,14 @@ def handle_client(conn, addr):
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_commonName = "\"commonName\": \"" + entry[0].split('_')[1].split("/")[0] + "\","
post_scientificName = "\"scientificName\": \"" + entry[0].split('_')[0] + "\","
post_algorithm = "\"algorithm\": " + "\"alpha\"" + ","
if model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16":
post_algorithm = "\"algorithm\": " + "\"2p2\"" + ","
else:
post_algorithm = "\"algorithm\": " + "\"alpha\"" + ","
post_confidence = "\"confidence\": " + str(entry[1])
post_end = " }"
+139
View File
@@ -0,0 +1,139 @@
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
import sys
import argparse
import datetime
try:
import tflite_runtime.interpreter as tflite
except BaseException:
from tensorflow import lite as tflite
def loadMetaModel():
global M_INTERPRETER
global M_INPUT_LAYER_INDEX
global M_OUTPUT_LAYER_INDEX
global CLASSES
# Load TFLite model and allocate tensors.
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.2_MData_Model_FP16.tflite')
M_INTERPRETER.allocate_tensors()
# Get input and output tensors.
input_details = M_INTERPRETER.get_input_details()
output_details = M_INTERPRETER.get_output_details()
# Get input tensor index
M_INPUT_LAYER_INDEX = input_details[0]['index']
M_OUTPUT_LAYER_INDEX = output_details[0]['index']
# Load labels
CLASSES = []
labelspath = userDir + '/BirdNET-Pi/model/labels.txt'
with open(labelspath, 'r') as lfile:
for line in lfile.readlines():
CLASSES.append(line.replace('\n', ''))
print("loaded META model")
def predictFilter(lat, lon, week):
global M_INTERPRETER
# Does interpreter exist?
try:
if M_INTERPRETER == None:
loadMetaModel()
except Exception as e:
loadMetaModel()
# Prepare mdata as sample
sample = np.expand_dims(np.array([lat, lon, week], dtype='float32'), 0)
# Run inference
M_INTERPRETER.set_tensor(M_INPUT_LAYER_INDEX, sample)
M_INTERPRETER.invoke()
return M_INTERPRETER.get_tensor(M_OUTPUT_LAYER_INDEX)[0]
def explore(lat, lon, week, threshold):
# Make filter prediction
l_filter = predictFilter(lat, lon, week)
# Apply threshold
l_filter = np.where(l_filter >= threshold, l_filter, 0)
# Zip with labels
l_filter = list(zip(l_filter, CLASSES))
# Sort by filter value
l_filter = sorted(l_filter, key=lambda x: x[0], reverse=True)
return l_filter
def getSpeciesList(lat, lon, week, threshold=0.05, sort=False):
print('Getting species list for {}/{}, Week {}...'.format(lat, lon, week), end='', flush=True)
# Extract species from model
pred = explore(lat, lon, week, threshold)
# Make species list
slist = []
for p in pred:
if p[0] >= threshold:
slist.append([p[1],p[0]])
return slist
userDir = os.path.expanduser('~')
DB_PATH = userDir + '/BirdNET-Pi/scripts/birds.db'
with open(userDir + '/BirdNET-Pi/scripts/thisrun.txt', 'r') as f:
this_run = f.readlines()
lat = str(str(str([i for i in this_run if i.startswith('LATITUDE')]).split('=')[1]).split('\\')[0])
lon = str(str(str([i for i in this_run if i.startswith('LONGITUDE')]).split('=')[1]).split('\\')[0])
weekofyear = datetime.datetime.today().isocalendar()[1]
if __name__ == '__main__':
# Parse arguments
parser = argparse.ArgumentParser(description='Get list of species for a given location with BirdNET. Sorted by occurrence frequency.')
#parser.add_argument('--o', default='/home/pi/BirdNET-Pi/include_species_list.txt', help='Path to output file or folder. If this is a folder, file will be named \'species_list.txt\'.')
#parser.add_argument('--lat', type=float, default=##, help='Recording location latitude. Set -1 to ignore.')
#parser.add_argument('--lon', type=float, default=##, help='Recording location longitude. Set -1 to ignore.')
#parser.add_argument('--week', type=int, default=dayofweek, help='Week of the year when the recording was made. Values in [1, 48] (4 weeks per month). Set -1 for year-round species list.')
parser.add_argument('--threshold', type=float, default=0.05, help='Occurrence frequency threshold. Defaults to 0.05.')
#parser.add_argument('--sortby', default='freq', help='Sort species by occurrence frequency or alphabetically. Values in [\'freq\', \'alpha\']. Defaults to \'freq\'.')
args = parser.parse_args()
LOCATION_FILTER_THRESHOLD = args.threshold
# Get species list
species_list = getSpeciesList(lat, lon, weekofyear, LOCATION_FILTER_THRESHOLD, False)
for x in range(len(species_list)):
print(species_list[x][0] + " - "+ str(species_list[x][1]))
print("\nThe above species list describes all the species that the model will attempt to detect. If you don't see a species you want detected on this list, decrease your threshold.")
print("\nNOTE: no actual changes to your BirdNET-Pi species list were made by running this command. To set your desired frequency threshold, do it through the BirdNET-Pi web interface (Tools -> Settings -> Model)")
+5 -1
View File
@@ -5,7 +5,11 @@ $user = trim($user);
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
$home = trim($home);
$fetch = shell_exec("sudo -u".$user." git -C ".$home."/BirdNET-Pi fetch 2>&1");
$_SESSION['behind'] = trim(shell_exec("sudo -u".$user." git -C ".$home."/BirdNET-Pi status | sed -n '2 p' | cut -d ' ' -f 7"));
$str = trim(shell_exec("sudo -u".$user." git -C ".$home."/BirdNET-Pi status"));
if (preg_match("/behind '.*?' by (\d+) commit(s?)\b/", $str, $matches)) {
$num_commits_behind = $matches[1];
$_SESSION['behind'] = $num_commits_behind;
}
?><html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<br>
+2
View File
@@ -409,6 +409,7 @@ document.getElementById("searchterm").onkeydown = (function(e) {
searchDetections(document.getElementById("searchterm").value);
document.getElementById("searchterm").blur();
} else {
/*
clearTimeout(timer);
timer = setTimeout(function() {
searchDetections(document.getElementById("searchterm").value);
@@ -418,6 +419,7 @@ document.getElementById("searchterm").onkeydown = (function(e) {
document.getElementById("searchterm").blur();
}, 2000);
}, 1000);
*/
}
});
<?php } ?>
+15
View File
@@ -144,5 +144,20 @@ if ! grep HEARTBEAT_URL /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "HEARTBEAT_URL=" >> /etc/birdnet/birdnet.conf
fi
if ! grep MODEL /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "MODEL=BirdNET_6K_GLOBAL_MODEL" >> /etc/birdnet/birdnet.conf
fi
if ! grep SF_THRESH /etc/birdnet/birdnet.conf &>/dev/null;then
sudo -u$USER echo "SF_THRESH=0.03" >> /etc/birdnet/birdnet.conf
fi
sudo chmod +x ~/BirdNET-Pi/scripts/install_language_label_nm.sh
sqlite3 $HOME/BirdNET-Pi/scripts/birds.db << EOF
CREATE INDEX IF NOT EXISTS "detections_Com_Name" ON "detections" ("Com_Name");
CREATE INDEX IF NOT EXISTS "detections_Date_Time" ON "detections" ("Date" DESC, "Time" DESC);
EOF
$HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise==1.2.1
sudo systemctl daemon-reload
restart_services.sh
+14 -2
View File
@@ -13,7 +13,13 @@ DB_PATH = userDir + '/BirdNET-Pi/scripts/birds.db'
flickr_images = {}
species_last_notified = {}
apobj = apprise.Apprise()
asset = apprise.AppriseAsset(
plugin_paths=[
userDir + "/.apprise/plugins",
userDir + "/.config/apprise/plugins",
]
)
apobj = apprise.Apprise(asset=asset)
config = apprise.AppriseConfig()
config.add(APPRISE_CONFIG)
apobj.add(config)
@@ -44,9 +50,14 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES = settings_dict.get('APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES')
if APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES != "0":
if species_last_notified.get(comName) is not None:
if int(timeim.time()) - species_last_notified[comName] < int(APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES):
try:
if int(timeim.time()) - species_last_notified[comName] < int(APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES):
return
except Exception as e:
print("APPRISE NOTIFICATION EXCEPTION: "+str(e))
return
#TODO: this all needs to be changed, we changed the caddy default to allow direct IP access, so birdnetpi.local shouldn't be relied on anymore
try:
websiteurl = settings_dict.get('BIRDNETPI_URL')
if len(websiteurl) == 0:
@@ -63,6 +74,7 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu
# TODO: Make this work with non-english comnames. Implement the "// convert sci name to English name" logic from overview.php here
url = 'https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key='+str(settings_dict.get('FLICKR_API_KEY'))+'&text='+str(comName)+' bird&sort=relevance&per_page=5&media=photos&format=json&license=2%2C3%2C4%2C5%2C6%2C9&nojsoncallback=1'
resp = requests.get(url=url)
resp.encoding = "utf-8"
data = resp.json()["photos"]["photo"][0]
image_url = 'https://farm'+str(data["farm"])+'.static.flickr.com/'+str(data["server"])+'/'+str(data["id"])+'_'+str(data["secret"])+'_n.jpg'