diff --git a/README.md b/README.md index 8c6c5a6..b7a01be 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ BirdNET-Pi

-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

@@ -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) diff --git a/birdnet.conf-defaults b/birdnet.conf-defaults index 9193b9b..e9cbbfb 100644 --- a/birdnet.conf-defaults +++ b/birdnet.conf-defaults @@ -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 ----------------------# diff --git a/homepage/style.css b/homepage/style.css index b152515..bb2ba6f 100644 --- a/homepage/style.css +++ b/homepage/style.css @@ -812,4 +812,8 @@ pre#timer.bash { line-height:5px; padding:3px; background-color:#9fe29b +} + +#ddnewline::before { + content: none; } \ No newline at end of file diff --git a/homepage/views.php b/homepage/views.php index 85f3d84..0544c88 100644 --- a/homepage/views.php +++ b/homepage/views.php @@ -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) {?> + + + + +

+
BirdNET_6K_GLOBAL_MODEL (2020)

+
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.
+
BirdNET_GLOBAL_3K_V2.2_Model_FP16 (2022)

+
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.
+
+

Location

@@ -511,3 +686,4 @@ if(isset($_GET['status'])){ + diff --git a/scripts/createdb.sh b/scripts/createdb.sh index 920fe23..4c66ff3 100755 --- a/scripts/createdb.sh +++ b/scripts/createdb.sh @@ -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 diff --git a/scripts/daily_plot.py b/scripts/daily_plot.py index 59747f9..d42b76c 100755 --- a/scripts/daily_plot.py +++ b/scripts/daily_plot.py @@ -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" diff --git a/scripts/disk_check.sh b/scripts/disk_check.sh index 743233e..c47c453 100755 --- a/scripts/disk_check.sh +++ b/scripts/disk_check.sh @@ -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 diff --git a/scripts/install_config.sh b/scripts/install_config.sh index 09334ad..d69e7d9 100755 --- a/scripts/install_config.sh +++ b/scripts/install_config.sh @@ -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_________________# diff --git a/scripts/install_language_label.sh b/scripts/install_language_label.sh index 4ba0b27..0129288 100755 --- a/scripts/install_language_label.sh +++ b/scripts/install_language_label.sh @@ -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 diff --git a/scripts/install_language_label_nm.sh b/scripts/install_language_label_nm.sh new file mode 100644 index 0000000..4569a0c --- /dev/null +++ b/scripts/install_language_label_nm.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +usage() { echo "Usage: $0 -l " 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 diff --git a/scripts/play.php b/scripts/play.php index c211840..e4c020e 100644 --- a/scripts/play.php +++ b/scripts/play.php @@ -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 " @@ -426,6 +431,12 @@ echo "
$name
} $iter++; + if($num_rows < 100){ + $imageelem = ""; + } else { + $imageelem = ""; + } + if($config["FULL_DISK"] == "purge") { if(!in_array($filename_formatted, $disk_check_exclude_arr)) { $imageicon = "images/unlock.svg"; @@ -455,13 +466,15 @@ echo "
$date $time
$confidence
- + ".$imageelem." + "; } else { echo " + ".$imageelem." + "; } diff --git a/scripts/print_diagnostic_info.sh b/scripts/print_diagnostic_info.sh new file mode 100644 index 0000000..df6a3bb --- /dev/null +++ b/scripts/print_diagnostic_info.sh @@ -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 "===========================================" \ No newline at end of file diff --git a/scripts/server.py b/scripts/server.py index 9d9e242..7e478df 100755 --- a/scripts/server.py +++ b/scripts/server.py @@ -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 = " }" diff --git a/scripts/species.py b/scripts/species.py new file mode 100644 index 0000000..d277b78 --- /dev/null +++ b/scripts/species.py @@ -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)") + + + diff --git a/scripts/system_controls.php b/scripts/system_controls.php index 77a7791..a05ad5f 100644 --- a/scripts/system_controls.php +++ b/scripts/system_controls.php @@ -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; +} ?>
diff --git a/scripts/todays_detections.php b/scripts/todays_detections.php index 280c5c4..04c5c27 100644 --- a/scripts/todays_detections.php +++ b/scripts/todays_detections.php @@ -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); + */ } }); diff --git a/scripts/update_birdnet_snippets.sh b/scripts/update_birdnet_snippets.sh index 5f0170c..227ff73 100755 --- a/scripts/update_birdnet_snippets.sh +++ b/scripts/update_birdnet_snippets.sh @@ -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 diff --git a/scripts/utils/notifications.py b/scripts/utils/notifications.py index 83930d0..304f0d7 100644 --- a/scripts/utils/notifications.py +++ b/scripts/utils/notifications.py @@ -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'
$date $time
$confidence
-