diff --git a/scripts/analyze.py b/scripts/analyze.py index 39b598e..f45845d 100644 --- a/scripts/analyze.py +++ b/scripts/analyze.py @@ -1,192 +1,24 @@ -# BirdWeather edits by @timsterc -# Other edits by @CaiusX and @mcguirepr89 -import os -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' -os.environ['CUDA_VISIBLE_DEVICES'] = '' - -try: - import tflite_runtime.interpreter as tflite -except: - from tensorflow import lite as tflite - import argparse -import operator -import librosa -import numpy as np -import math -import time -from decimal import Decimal -import json -import requests -import mysql.connector -import datetime -import pytz -from tzlocal import get_localzone -from pathlib import Path +import socket -def loadModel(): +HEADER = 64 +PORT = 5050 +FORMAT = 'utf-8' +DISCONNECT_MESSAGE = "!DISCONNECT" +SERVER = "127.0.1.1" +ADDR = (SERVER, PORT) - global INPUT_LAYER_INDEX - global OUTPUT_LAYER_INDEX - global MDATA_INPUT_INDEX - global CLASSES +client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +client.connect(ADDR) - print('LOADING TF LITE MODEL...', end=' ') - - # Load TFLite model and allocate tensors. - interpreter = tflite.Interpreter(model_path='../model/BirdNET_6K_GLOBAL_MODEL.tflite',num_threads=2) - interpreter.allocate_tensors() - - # Get input and output tensors. - input_details = interpreter.get_input_details() - output_details = interpreter.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('../model/labels.txt', 'r') as lfile: - for line in lfile.readlines(): - CLASSES.append(line.replace('\n', '')) - - print('DONE!') - - return interpreter - -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, interpreter, sensitivity): - - # 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 ['Human_Human', 'Non-bird_Non-bird', 'Noise_Noise']: - p_sorted[i] = (p_sorted[i][0], 0.0) - - # Only return first the top ten results - return p_sorted[:10] - -def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap, 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], interpreter, sensitivity) - - # Save result and timestamp - pred_end = pred_start + 3.0 - detections[str(pred_start) + ';' + str(pred_end)] = p - pred_start = pred_end - overlap - - print('DONE! Time', int((time.time() - start) * 10) / 10.0, 'SECONDS') - - 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.') +def send(msg): + message = msg.encode(FORMAT) + msg_length = len(message) + send_length = str(msg_length).encode(FORMAT) + send_length += b' ' * (HEADER - len(send_length)) + client.send(send_length) + client.send(message) + print(client.recv(2048).decode(FORMAT)) def main(): @@ -203,145 +35,39 @@ def main(): parser.add_argument('--overlap', type=float, default=0.0, help='Overlap in seconds between extracted spectrograms. Values in [0.0, 2.9]. Defaults tp 0.0.') parser.add_argument('--sensitivity', type=float, default=1.0, help='Detection sensitivity; Higher values result in higher sensitivity. Values in [0.5, 1.5]. Defaults to 1.0.') parser.add_argument('--min_conf', type=float, default=0.1, help='Minimum confidence threshold. Values in [0.01, 0.99]. Defaults to 0.1.') - parser.add_argument('--include_list', default='', help='Path to text file containing a list of included species. Not used if not provided.') - parser.add_argument('--exclude_list', default='', help='Path to text file containing a list of excluded species. Not used if not provided.') + parser.add_argument('--include_list', default='null', help='Path to text file containing a list of included species. Not used if not provided.') + parser.add_argument('--exclude_list', default='null', help='Path to text file containing a list of excluded species. Not used if not provided.') parser.add_argument('--birdweather_id', default='99999', help='Private Station ID for BirdWeather.') args = parser.parse_args() - # Load model - interpreter = loadModel() - - # Load custom species lists - INCLUDED and EXCLUDED - if not args.include_list == '': - INCLUDE_LIST = loadCustomSpeciesList(args.include_list) - else: - INCLUDE_LIST = [] + sockParams = '' + if args.i: + sockParams += 'i=' + args.i + '||' + if args.o: + sockParams += 'o=' + args.o + '||' + if args.birdweather_id: + sockParams += 'birdweather_id=' + args.birdweather_id + '||' + if args.include_list: + sockParams += 'include_list=' + args.include_list + '||' + if args.exclude_list: + sockParams += 'exclude_list=' + args.exclude_list + '||' + if args.overlap: + sockParams += 'overlap=' + args.overlap + '||' + if args.week: + sockParams += 'week=' + str(args.week) + '||' + if args.sensitivity: + sockParams += 'sensitivity=' + str(args.sensitivity) + '||' + if args.min_conf: + sockParams += 'min_conf=' + str(args.min_conf) + '||' + if args.lat: + sockParams += 'lat=' + str(args.lat) + '||' + if args.lon: + sockParams += 'lon=' + str(args.lon) + '||' - if not args.exclude_list == '': - 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 - 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, interpreter) - # 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 - for i in detections: - print("\n", detections[i][0],"\n") - with open('../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(sensitivity) +';' + str(args.overlap) + '\n') - - def insert_variables_into_table(Date, Time, Sci_Name, Com_Name, Confidence, Lat, Lon, Cutoff, Week, Sens, Overlap): - try: - connection = mysql.connector.connect(host='localhost', - database='birds', - user='birder', - password='databasepassword') - cursor = connection.cursor() - mySql_insert_query = """INSERT INTO detections (Date, Time, Sci_Name, Com_Name, Confidence, Lat, Lon, Cutoff, Week, Sens, Overlap) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ - - record = (Date, Time, Sci_Name, Com_Name, Confidence, Lat, Lon, Cutoff, Week, Sens, Overlap) - - cursor.execute(mySql_insert_query, record) - connection.commit() - print("Record inserted successfully into detections table") - - - except mysql.connector.Error as error: - print("Failed to insert record into detections table {}".format(error)) - - finally: - if connection.is_connected(): - connection.close() - print("MySQL connection is closed") - - species = entry[0] - sci_name,com_name = species.split('_') - insert_variables_into_table(str(current_date), str(current_time), sci_name, com_name, \ - str(entry[1]), str(args.lat), str(args.lon), str(min_conf), str(week), \ - str(args.sensitivity), str(args.overlap)) - - 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) + '\n') - - if birdweather_id != "99999": - - 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) + send(sockParams) + send(DISCONNECT_MESSAGE) #time.sleep(3) ############################################################################### @@ -354,4 +80,3 @@ if __name__ == '__main__': # Example calls # python3 analyze.py --i 'example/XC558716 - Soundscape.mp3' --lat 35.4244 --lon -120.7463 --week 18 # python3 analyze.py --i 'example/XC563936 - Soundscape.mp3' --lat 47.6766 --lon -122.294 --week 11 --overlap 1.5 --min_conf 0.25 --sensitivity 1.25 --custom_list 'example/custom_species_list.txt' - diff --git a/scripts/createdb_bullseye.sh b/scripts/createdb_bullseye.sh index fc920a1..8453358 100755 --- a/scripts/createdb_bullseye.sh +++ b/scripts/createdb_bullseye.sh @@ -39,7 +39,7 @@ GRANT ALL ON birds.* TO 'birder'@'localhost' IDENTIFIED BY '${DB_PWD}' WITH GRAN FLUSH PRIVILEGES; exit EOF -sudo -u${USER} sed -i "s/databasepassword/${DB_PWD}/g" /home/pi/BirdNET-Pi/scripts/analyze.py +sudo -u${USER} sed -i "s/databasepassword/${DB_PWD}/g" /home/pi/BirdNET-Pi/scripts/server.py sed -i "s/mysqli.default_host =.*/mysqli.default_host = localhost/g" /etc/php/7.4/fpm/php.ini sed -i "s/mysqli.default_user =.*/mysqli.default_user = birder/g" /etc/php/7.4/fpm/php.ini sed -i "s/mysqli.default_pw =.*/mysqli.default_pw = ${DB_PWD}/g" /etc/php/7.4/fpm/php.ini diff --git a/scripts/install_services.sh b/scripts/install_services.sh index 248d57d..8aaf29a 100755 --- a/scripts/install_services.sh +++ b/scripts/install_services.sh @@ -57,9 +57,10 @@ install_birdnet_analysis() { cat << EOF > /etc/systemd/system/birdnet_analysis.service [Unit] Description=BirdNET Analysis +After=birdnet_server.service +Requires=birdnet_server.service [Service] Restart=always -RuntimeMaxSec=10800 Type=simple RestartSec=2 User=${USER} @@ -70,6 +71,24 @@ EOF systemctl enable birdnet_analysis.service } +install_birdnet_server() { + echo "Installing the birdnet_server.service" + cat << EOF > /etc/systemd/system/birdnet_server.service +[Unit] +Description=BirdNET Analysis Server +Before=birdnet_analysis.service +[Service] +Restart=always +Type=simple +RestartSec=10 +User=${USER} +ExecStart=/user/local/bin/server.py +[Install] +WantedBy=multi-user.target +EOF + systemctl enable birdnet_server.service +} + install_extraction_service() { echo "Installing the extraction.service and extraction.timer" cat << EOF > /etc/systemd/system/extraction.service @@ -392,7 +411,7 @@ RestartSec=3 Type=simple User=${USER} Environment=TERM=xterm-256color -ExecStart=/usr/local/bin/gotty -p 8080 --title-format "BirdNET-Pi Log" journalctl -o cat -fu birdnet_analysis.service +ExecStart=/usr/local/bin/gotty -p 8080 --title-format "BirdNET-Pi Log" journalctl -o cat -fu birdnet_server.service [Install] WantedBy=multi-user.target EOF @@ -517,6 +536,7 @@ install_selected_services() { update_system install_scripts install_birdnet_analysis + install_birdnet_server if [[ "${DO_EXTRACTIONS}" =~ [Yy] ]];then install_extraction_service diff --git a/scripts/server.py b/scripts/server.py new file mode 100755 index 0000000..94a738c --- /dev/null +++ b/scripts/server.py @@ -0,0 +1,424 @@ +#!/home/pi/BirdNET-Pi/birdnet/bin/python3 +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: + from tensorflow import lite as tflite + +import argparse +import operator +import librosa +import numpy as np +import math +import time +from decimal import Decimal +import json +############################################################################### +import requests +import mysql.connector +############################################################################### +import datetime +import pytz +from tzlocal import get_localzone +from pathlib import Path + + +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) +server.bind(ADDR) + +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. + myinterpreter = tflite.Interpreter(model_path='/home/pi/BirdNET-Pi/model/BirdNET_6K_GLOBAL_MODEL.tflite',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('/home/pi/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 ['Human_Human', 'Non-bird_Non-bird', 'Noise_Noise']: + p_sorted[i] = (p_sorted[i][0], 0.0) + + # Only return first the top ten results + return p_sorted[:10] + +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) + + # Save result and timestamp + pred_end = pred_start + 3.0 + detections[str(pred_start) + ';' + str(pred_end)] = p + pred_start = pred_end - overlap + + print('DONE! Time', int((time.time() - start) * 10) / 10.0, 'SECONDS') + + 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 = '/home/pi/test.wav' + args.o = '/home/pi/test.wav.csv' + 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: + print("\n", detections[i][0],"\n") + myReturn += str(detections[i][0]) + '||' + + + + + with open('/home/pi/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(sensitivity) +';' + str(args.overlap) + '\n') + + def insert_variables_into_table(Date, Time, Sci_Name, Com_Name, Confidence, Lat, Lon, Cutoff, Week, Sens, Overlap): + try: + connection = mysql.connector.connect(host='localhost', + database='birds', + user='birder', + password='databasepassword') + cursor = connection.cursor() + mySql_insert_query = """INSERT INTO detections (Date, Time, Sci_Name, Com_Name, Confidence, Lat, Lon, Cutoff, Week, Sens, Overlap) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ + + record = (Date, Time, Sci_Name, Com_Name, Confidence, Lat, Lon, Cutoff, Week, Sens, Overlap) + + cursor.execute(mySql_insert_query, record) + connection.commit() + print("Record inserted successfully into detections table") + + + except mysql.connector.Error as error: + print("Failed to insert record into detections table {}".format(error)) + + finally: + if connection.is_connected(): + connection.close() + print("MySQL connection is closed") + + species = entry[0] + sci_name,com_name = species.split('_') + insert_variables_into_table(str(current_date), str(current_time), sci_name, com_name, \ + str(entry[1]), str(args.lat), str(args.lon), str(min_conf), str(week), \ + str(args.sensitivity), str(args.overlap)) + + 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) + '\n') + + if birdweather_id != "99999": + + 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) + + + conn.send("Msg received".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() diff --git a/scripts/update_db_pwd_bullseye.sh b/scripts/update_db_pwd_bullseye.sh index d0bfccc..16f0aab 100755 --- a/scripts/update_db_pwd_bullseye.sh +++ b/scripts/update_db_pwd_bullseye.sh @@ -5,9 +5,9 @@ source /etc/birdnet/birdnet.conf mysql -e " SET PASSWORD FOR 'birder'@'localhost' = PASSWORD('${DB_PWD}'); FLUSH PRIVILEGES"; -sudo -u ${USER} git -C /home/pi/BirdNET-Pi checkout -f scripts/analyze.py +sudo -u ${USER} git -C /home/pi/BirdNET-Pi checkout -f scripts/server.py sudo -u ${USER} git -C /home/pi/BirdNET-Pi checkout -f scripts/viewdb.php -sudo -u ${USER} sed -i "s/databasepassword/${DB_PWD}/g" /home/pi/BirdNET-Pi/scripts/analyze.py +sudo -u ${USER} sed -i "s/databasepassword/${DB_PWD}/g" /home/pi/BirdNET-Pi/scripts/server.py sed -i "s/mysqli.default_host =.*/mysqli.default_host = localhost/g" /etc/php/7.4/fpm/php.ini sed -i "s/mysqli.default_user =.*/mysqli.default_user = birder/g" /etc/php/7.4/fpm/php.ini sed -i "s/mysqli.default_pw =.*/mysqli.default_pw = ${DB_PWD}/g" /etc/php/7.4/fpm/php.ini diff --git a/scripts/update_services.sh b/scripts/update_services.sh index 0078483..21f55ac 100755 --- a/scripts/update_services.sh +++ b/scripts/update_services.sh @@ -43,9 +43,10 @@ install_birdnet_analysis() { cat << EOF > /etc/systemd/system/birdnet_analysis.service [Unit] Description=BirdNET Analysis +After=birdnet_server.service +Requires=birdnet_server.service [Service] Restart=always -RuntimeMaxSec=10800 Type=simple RestartSec=2 User=${USER} @@ -56,6 +57,25 @@ EOF systemctl enable birdnet_analysis.service } +install_birdnet_server() { + echo "Installing the birdnet_server.service" + cat << EOF > /etc/systemd/system/birdnet_server.service +[Unit] +Description=BirdNET Analysis Server +Before=birdnet_analysis.service +[Service] +Restart=always +Type=simple +RestartSec=2 +User=${USER} +ExecStart=/usr/local/bin/server.py +[Install] +WantedBy=multi-user.target +EOF + systemctl enable birdnet_server.service +} + + install_extraction_service() { echo "Installing the extraction.service and extraction.timer" cat << EOF > /etc/systemd/system/extraction.service @@ -386,7 +406,7 @@ RestartSec=3 Type=simple User=${USER} Environment=TERM=xterm-256color -ExecStart=/usr/local/bin/gotty -p 8080 --title-format "BirdNET-Pi Log" journalctl -o cat -fu birdnet_analysis.service +ExecStart=/usr/local/bin/gotty -p 8080 --title-format "BirdNET-Pi Log" journalctl -o cat -fu birdnet_server.service [Install] WantedBy=multi-user.target @@ -513,6 +533,7 @@ install_cleanup_cron() { install_selected_services() { install_scripts install_birdnet_analysis + install_birdnet_server if [[ "${DO_EXTRACTIONS}" =~ [Yy] ]];then install_extraction_service