diff --git a/scripts/config.php b/scripts/config.php index 72cfb8a..e0625dc 100644 --- a/scripts/config.php +++ b/scripts/config.php @@ -519,6 +519,8 @@ https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}
Common Name
$confidence
Confidence Score
+
$confidencepct
+
Confidence Score as a percentage (eg. 0.91 => 91)
$listenurl
A link to the detection
$date
@@ -542,9 +544,9 @@ https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}

Use the variables defined above to customize your notification title and body.

-
+
- ' />
+ ' />
>
> diff --git a/scripts/daily_plot.py b/scripts/daily_plot.py index db989ae..ff9c839 100755 --- a/scripts/daily_plot.py +++ b/scripts/daily_plot.py @@ -48,7 +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) +if df_plt_top10_today.empty: + exit(0) # Set Palette for graphics pal = "Greens" diff --git a/scripts/plotly_streamlit.py b/scripts/plotly_streamlit.py index 467e353..f9910a8 100755 --- a/scripts/plotly_streamlit.py +++ b/scripts/plotly_streamlit.py @@ -95,7 +95,7 @@ else: def date_filter(df, start_date, end_date): filt = (df2.index >= pd.Timestamp(start_date)) & (df2.index <= pd.Timestamp(end_date + timedelta(days=1))) df = df[filt] - return(df) + return (df) df2 = date_filter(df2, start_date, end_date) @@ -142,7 +142,7 @@ def time_resample(df, resample_time): else: df_resample = df.resample(resample_time)['Com_Name'].aggregate('unique').explode() - return(df_resample) + return (df_resample) top_bird = df2['Com_Name'].mode()[0] @@ -236,10 +236,10 @@ if daily is False: if resample_time != '1D': specie = st.selectbox( - 'Which bird would you like to explore for the dates ' - + str(start_date) + ' to ' + str(end_date) + '?', - species, - index=0) + 'Which bird would you like to explore for the dates ' + + str(start_date) + ' to ' + str(end_date) + '?', + species, + index=0) # filt = df2['Com_Name'] == specie if specie == 'All': @@ -256,7 +256,7 @@ if daily is False: # '{:.2f}%'.format(max(df2[df2['Com_Name'] == specie]['Confidence']) * 100)) + # ' ' + ' Median:' + str( # '{:.2f}%'.format(np.median(df2[df2['Com_Name'] == specie]['Confidence']) * 100)) - ) + ) ) fig.layout.annotations[1].update(x=0.7, y=0.25, font_size=15) @@ -316,7 +316,7 @@ if daily is False: fig = make_subplots( rows=3, cols=1, specs=[[{"type": "polar", "rowspan": 2}], [{"rowspan": 1}], [{"type": "xy", "rowspan": 1}]] - ) + ) # Set 360 degrees, 24 hours for polar plot theta = np.linspace(0.0, 360, 24, endpoint=False) diff --git a/scripts/server.py b/scripts/server.py index ccae348..8108e3a 100755 --- a/scripts/server.py +++ b/scripts/server.py @@ -57,10 +57,11 @@ with open(userDir + '/BirdNET-Pi/scripts/thisrun.txt', 'r') as f: 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: + except Exception: model = "BirdNET_6K_GLOBAL_MODEL" sf_thresh = 0.03 + def loadModel(): global INPUT_LAYER_INDEX @@ -97,6 +98,7 @@ def loadModel(): return myinterpreter + def loadMetaModel(): global M_INTERPRETER @@ -117,15 +119,16 @@ def loadMetaModel(): print("loaded META model") + def predictFilter(lat, lon, week): global M_INTERPRETER # Does interpreter exist? try: - if M_INTERPRETER == None: + if M_INTERPRETER is None: loadMetaModel() - except Exception as e: + except Exception: loadMetaModel() # Prepare mdata as sample @@ -137,6 +140,7 @@ def predictFilter(lat, lon, week): return M_INTERPRETER.get_tensor(M_OUTPUT_LAYER_INDEX)[0] + def explore(lat, lon, week): # Make filter prediction @@ -153,15 +157,17 @@ def explore(lat, lon, week): 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): + # 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): slist = [] @@ -274,8 +280,7 @@ def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,): if model == "BirdNET_GLOBAL_3K_V2.3_Model_FP16": if len(PREDICTED_SPECIES_LIST) == 0 or len(INCLUDE_LIST) != 0: - predictSpeciesList(lat,lon,week) - + predictSpeciesList(lat, lon, week) # Convert and prepare metadata mdata = convertMetadata(np.array([lat, lon, week])) @@ -322,7 +327,9 @@ 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) and (entry[0] in PREDICTED_SPECIES_LIST or len(PREDICTED_SPECIES_LIST) == 0) ): + if entry[1] >= min_conf and ((entry[0] in INCLUDE_LIST or len(INCLUDE_LIST) == 0) + and (entry[0] not in EXCLUDE_LIST or len(EXCLUDE_LIST) == 0) + and (entry[0] 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.') @@ -468,7 +475,8 @@ 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] in PREDICTED_SPECIES_LIST or len(PREDICTED_SPECIES_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('_', ';').split("/")[0] + ';' + str(entry[1]) + ";" + str(args.lat) + ';' + str(args.lon) + ';' + str(min_conf) + ';' + str(week) + ';' @@ -511,6 +519,7 @@ def handle_client(conn, addr): settings_dict = config_to_settings(userDir + '/BirdNET-Pi/scripts/thisrun.txt') sendAppriseNotifications(species, str(score), + str(round(score * 100)), File_Name, Date, Time, diff --git a/scripts/species.py b/scripts/species.py index 7a13287..0f171fd 100644 --- a/scripts/species.py +++ b/scripts/species.py @@ -1,18 +1,5 @@ -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 @@ -22,7 +9,6 @@ except BaseException: from tensorflow import lite as tflite - def loadMetaModel(): global M_INTERPRETER @@ -51,15 +37,16 @@ def loadMetaModel(): print("loaded META model") + def predictFilter(lat, lon, week): global M_INTERPRETER # Does interpreter exist? try: - if M_INTERPRETER == None: + if M_INTERPRETER is None: loadMetaModel() - except Exception as e: + except Exception: loadMetaModel() # Prepare mdata as sample @@ -71,6 +58,7 @@ def predictFilter(lat, lon, week): return M_INTERPRETER.get_tensor(M_OUTPUT_LAYER_INDEX)[0] + def explore(lat, lon, week, threshold): # Make filter prediction @@ -87,6 +75,7 @@ def explore(lat, lon, week, threshold): 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) @@ -98,7 +87,7 @@ def getSpeciesList(lat, lon, week, threshold=0.05, sort=False): slist = [] for p in pred: if p[0] >= threshold: - slist.append([p[1],p[0]]) + slist.append([p[1], p[0]]) return slist @@ -112,17 +101,14 @@ with open(userDir + '/BirdNET-Pi/scripts/thisrun.txt', 'r') as f: 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__': +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 = argparse.ArgumentParser( + description='Get list of species for a given location with BirdNET. Sorted by occurrence frequency.' + ) 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 @@ -130,10 +116,9 @@ if __name__ == '__main__': # 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)") - - + 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/utils/notifications.py b/scripts/utils/notifications.py index 99265a5..94df122 100644 --- a/scripts/utils/notifications.py +++ b/scripts/utils/notifications.py @@ -24,6 +24,7 @@ config = apprise.AppriseConfig() config.add(APPRISE_CONFIG) apobj.add(config) + def notify(body, title, attached=""): if attached != "": apobj.notify( @@ -38,7 +39,9 @@ def notify(body, title, attached=""): ) -def sendAppriseNotifications(species, confidence, path, date, time, week, latitude, longitude, cutoff, sens, overlap, settings_dict, db_path=DB_PATH): +def sendAppriseNotifications(species, confidence, confidencepct, path, + date, time, week, latitude, longitude, cutoff, + sens, overlap, settings_dict, db_path=DB_PATH): # print(sendAppriseNotifications) # print(settings_dict) if os.path.exists(APPRISE_CONFIG) and os.path.getsize(APPRISE_CONFIG) > 0: @@ -67,7 +70,7 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu 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 + # 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: @@ -79,27 +82,28 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu image_url = "" if len(settings_dict.get('FLICKR_API_KEY')) > 0 and "$flickrimage" in body: - if not comName in flickr_images: + if comName not in flickr_images: try: # TODO: Make this work with non-english comnames. Implement the "// convert sci name to English name" logic from overview.php here headers = {'User-Agent': 'Python_Flickr/1.0'} 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, headers=headers) + 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' flickr_images[comName] = image_url except Exception as e: - print("FLICKR API ERROR: "+str(e)) + print("FLICKR API ERROR: "+str(e)) image_url = "" else: image_url = flickr_images[comName] - if settings_dict.get('APPRISE_NOTIFY_EACH_DETECTION') == "1": notify_body = body.replace("$sciname", sciName)\ .replace("$comname", comName)\ + .replace("$confidencepct", confidencepct)\ .replace("$confidence", confidence)\ .replace("$listenurl", listenurl)\ .replace("$date", date)\ @@ -113,6 +117,7 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu .replace("$overlap", overlap) notify_title = title.replace("$sciname", sciName)\ .replace("$comname", comName)\ + .replace("$confidencepct", confidencepct)\ .replace("$confidence", confidence)\ .replace("$listenurl", listenurl)\ .replace("$date", date)\ @@ -143,6 +148,7 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu print("send the notification") notify_body = body.replace("$sciname", sciName)\ .replace("$comname", comName)\ + .replace("$confidencepct", confidencepct)\ .replace("$confidence", confidence)\ .replace("$listenurl", listenurl)\ .replace("$date", date)\ @@ -157,6 +163,7 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu + " (first time today)" notify_title = title.replace("$sciname", sciName)\ .replace("$comname", comName)\ + .replace("$confidencepct", confidencepct)\ .replace("$confidence", confidence)\ .replace("$listenurl", listenurl)\ .replace("$date", date)\ @@ -191,6 +198,7 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu if numberDetections > 0 and numberDetections <= 5: notify_body = body.replace("$sciname", sciName)\ .replace("$comname", comName)\ + .replace("$confidencepct", confidencepct)\ .replace("$confidence", confidence)\ .replace("$listenurl", listenurl)\ .replace("$date", date)\ @@ -205,6 +213,7 @@ def sendAppriseNotifications(species, confidence, path, date, time, week, latitu + " (only seen " + str(int(numberDetections)) + " times in last 7d)" notify_title = title.replace("$sciname", sciName)\ .replace("$comname", comName)\ + .replace("$confidencepct", confidencepct)\ .replace("$confidence", confidence)\ .replace("$listenurl", listenurl)\ .replace("$date", date)\ diff --git a/scripts/utils/parse_settings.py b/scripts/utils/parse_settings.py index d0e2166..723560e 100644 --- a/scripts/utils/parse_settings.py +++ b/scripts/utils/parse_settings.py @@ -6,7 +6,7 @@ def config_to_settings(path): this_run = f.readlines() for i in this_run: key = i.split("=")[0] - value = i.split("=")[1][:-1] + value = "=".join(i.split("=")[1:])[:-1] # Trim for strings, not ideal if value.startswith('"') and value.endswith('"'): value = value[1:-1] diff --git a/tests/test_apprise_notifications.py b/tests/test_apprise_notifications.py index 0041e43..15a81ad 100644 --- a/tests/test_apprise_notifications.py +++ b/tests/test_apprise_notifications.py @@ -50,6 +50,7 @@ def test_notifications(mocker): "APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY": "0" } sendAppriseNotifications("Myiarchus crinitus_Great Crested Flycatcher", + "0.91", "91", "filename", "1666-06-06", @@ -64,12 +65,13 @@ def test_notifications(mocker): "test.db") # No active apprise notifcations configured. Confirm no notifications. - assert(notify_call.call_count == 0) # No notification should be sent. + assert (notify_call.call_count == 0) # No notification should be sent. # Add daily notification. notify_call.reset_mock() settings_dict["APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY"] = "1" sendAppriseNotifications("Myiarchus crinitus_Great Crested Flycatcher", + "0.91", "91", "filename", "1666-06-06", @@ -83,8 +85,8 @@ def test_notifications(mocker): settings_dict, "test.db") - assert(notify_call.call_count == 1) - assert( + assert (notify_call.call_count == 1) + assert ( notify_call.call_args_list[0][0][0] == "A Great Crested Flycatcher (Myiarchus crinitus) was just detected with a confidence of 91 (first time today)" ) @@ -92,6 +94,7 @@ def test_notifications(mocker): notify_call.reset_mock() settings_dict["APPRISE_NOTIFY_NEW_SPECIES"] = "1" sendAppriseNotifications("Myiarchus crinitus_Great Crested Flycatcher", + "0.91", "91", "filename", "1666-06-06", @@ -105,18 +108,20 @@ def test_notifications(mocker): settings_dict, "test.db") - assert(notify_call.call_count == 2) - assert( + assert (notify_call.call_count == 2) + assert ( notify_call.call_args_list[0][0][0] == "A Great Crested Flycatcher (Myiarchus crinitus) was just detected with a confidence of 91 (first time today)" ) - assert( - notify_call.call_args_list[1][0][0] == "A Great Crested Flycatcher (Myiarchus crinitus) was just detected with a confidence of 91 (only seen 1 times in last 7d)" + assert ( + notify_call.call_args_list[1][0][0] == "A Great Crested Flycatcher (Myiarchus crinitus) was just detected with a confidence \ + of 91 (only seen 1 times in last 7d)" ) # Add each species notification. notify_call.reset_mock() settings_dict["APPRISE_NOTIFY_EACH_DETECTION"] = "1" sendAppriseNotifications("Myiarchus crinitus_Great Crested Flycatcher", + "0.91", "91", "filename", "1666-06-06", @@ -130,4 +135,4 @@ def test_notifications(mocker): settings_dict, "test.db") - assert(notify_call.call_count == 3) + assert (notify_call.call_count == 3) diff --git a/tests/test_settings_parse.py b/tests/test_settings_parse.py index 82b5678..303338c 100644 --- a/tests/test_settings_parse.py +++ b/tests/test_settings_parse.py @@ -39,6 +39,6 @@ IDFILE=/home/pi/BirdNET-Pi/IdentifiedSoFar.txt""" f.write(text) settings = config_to_settings(filename.name) - assert(settings["APPRISE_NOTIFICATION_TITLE"] == "Bird!") - assert(settings["FULL_DISK"] == "purge") - assert(settings["OVERLAP"] == "0.0") # Yes, it's a string at this point. + assert (settings["APPRISE_NOTIFICATION_TITLE"] == "Bird!") + assert (settings["FULL_DISK"] == "purge") + assert (settings["OVERLAP"] == "0.0") # Yes, it's a string at this point.