Skip to content

Commit

Permalink
Add birdweather.py module
Browse files Browse the repository at this point in the history
  • Loading branch information
tvoirand committed Dec 6, 2024
1 parent 0afd5ba commit eb554f4
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 46 deletions.
6 changes: 3 additions & 3 deletions scripts/birdnet_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

from server import load_global_model, run_analysis
from utils.helpers import get_settings, ParseFileName, get_wav_files, ANALYZING_NOW, setup_logging
from utils.reporting import extract_detection, summary, write_to_file, write_to_db, apprise, bird_weather, heartbeat, \
update_json_file
from utils.reporting import extract_detection, summary, write_to_file, write_to_db, apprise, \
post_current_detections_to_birdweather, heartbeat, update_json_file

shutdown = False

Expand Down Expand Up @@ -114,7 +114,7 @@ def handle_reporting_queue(queue):
write_to_file(file, detection)
write_to_db(file, detection)
apprise(file, detections)
bird_weather(file, detections)
post_current_detections_to_birdweather(file, detections)
heartbeat()
os.remove(file.file_name)
except BaseException as e:
Expand Down
123 changes: 123 additions & 0 deletions scripts/utils/birdweather.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Module to handle communication with the BirdWeather API."""

import requests
import logging
import datetime

import gzip
from typing import Any, Dict, List, Optional
from .helpers import Detection

log = logging.getLogger(__name__)


def get_birdweather_species_id(sci_name: str, com_name: str) -> int:
"""Lookup a BirdWeather species ID based on the species scientific and common names."""
species_url = "https://app.birdweather.com/api/v1/species/lookup"
try:
resp = requests.post(
url=species_url,
json={"species": [f"{sci_name}_{com_name}"]},
timeout=20,
)
data = resp.json()
if not data["success"] or len(data["species"]) != 1:
raise
species = next(iter(data["species"].values()))
return species["id"]
except Exception as e:
log.error(f"Couldn't find BirdWeather species ID for {sci_name}_{com_name}: {e}")
raise


def query_birdweather_detections(
birdweather_id: str,
species_id: int,
detection_datetime: datetime.datetime,
) -> List[Dict[str, Any]]:
"""Query detections from the BirdWeather API for specific station, species and time."""
detections_url = (
f"https://app.birdweather.com/api/v1/stations/{birdweather_id}/detections"
)
try:
resp = requests.get(
url=detections_url,
data={
"speciesId": species_id,
"from": detection_datetime.isoformat(),
"to": detection_datetime.isoformat(),
},
timeout=20,
)
data = resp.json()
if not data["success"]:
raise
return data["detections"]
except Exception as e:
log.error(f"Could not lookup detections from BirdWeather: {e}")
raise


def post_soundscape_to_birdweather(
birdweather_id: str, detection_datetime: datetime.datetime, soundscape_file: str
) -> Optional[int]:
"""Upload a soundscape file to BirdWeather."""
soundscape_url = (
f"https://app.birdweather.com/api/v1/stations/{birdweather_id}/"
f"soundscapes?timestamp={detection_datetime.isoformat()}"
)
with open(soundscape_file, "rb") as f:
mp3_data = f.read()
gzip_mp3_data = gzip.compress(mp3_data)
try:
resp = requests.post(
url=soundscape_url,
data=gzip_mp3_data,
timeout=20,
headers={
"Content-Type": "application/octet-stream",
"Content-Encoding": "gzip",
},
)
data = resp.json()
if not data.get("success"):
log.error(data.get("message"))
raise
return data["soundscape"]["id"]
except Exception as e:
log.error(f"Cannot POST soundscape: {e}")
return


def post_detection_to_birdweather(
detection: Detection,
soundscape_id: str,
soundscape_datetime: datetime.datetime,
birdweather_id: str,
latitude: float,
longitude: float,
model: str
):
"""Upload a detection to BirdWeather."""

detection_url = f'https://app.birdweather.com/api/v1/stations/{birdweather_id}/detections'

data = {
'timestamp': detection.iso8601,
'lat': latitude,
'lon': longitude,
'soundscapeId': soundscape_id,
'soundscapeStartTime': (detection.start_datetime - soundscape_datetime).seconds,
'soundscapeEndTime': (detection.stop_datetime - soundscape_datetime).seconds,
'commonName': detection.common_name,
'scientificName': detection.scientific_name,
'algorithm': '2p4' if model == 'BirdNET_GLOBAL_6K_V2.4_Model_FP16' else 'alpha',
'confidence': detection.confidence
}

log.debug(data)
try:
response = requests.post(detection_url, json=data, timeout=20)
log.info("Detection POST Response Status - %d", response.status_code)
except Exception as e:
log.error("Cannot POST detection: %s", e)
65 changes: 22 additions & 43 deletions scripts/utils/reporting.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import glob
import gzip
import json
import logging
import os
import sqlite3
import subprocess
from time import sleep

from tzlocal import get_localzone
import requests

from .helpers import get_settings, ParseFileName, Detection, DB_PATH
from .notifications import sendAppriseNotifications
from .birdweather import post_soundscape_to_birdweather, post_detection_to_birdweather

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -160,54 +161,32 @@ def apprise(file: ParseFileName, detections: [Detection]):
species_apprised_this_run.append(detection.species)


def bird_weather(file: ParseFileName, detections: [Detection]):
def post_current_detections_to_birdweather(file: ParseFileName, detections: [Detection]):
"""Post to BirdWeather detections that were just performed.
This function relies on the .wav audio file temporarily stored in "StreamData" to post a
soundscape to BirdWeather.
"""
conf = get_settings()
if conf['BIRDWEATHER_ID'] == "":
return
if detections:
# POST soundscape to server
soundscape_url = (f'https://app.birdweather.com/api/v1/stations/'
f'{conf["BIRDWEATHER_ID"]}/soundscapes?timestamp={file.iso8601}')

with open(file.file_name, 'rb') as f:
wav_data = f.read()
gzip_wav_data = gzip.compress(wav_data)
try:
response = requests.post(url=soundscape_url, data=gzip_wav_data, timeout=30,
headers={'Content-Type': 'application/octet-stream', 'Content-Encoding': 'gzip'})
log.info("Soundscape POST Response Status - %d", response.status_code)
sdata = response.json()
except BaseException as e:
log.error("Cannot POST soundscape: %s", e)
return
if not sdata.get('success'):
log.error(sdata.get('message'))
soundscape_id = post_soundscape_to_birdweather(
conf["BIRDWEATHER_ID"], file.file_date.astimezone(get_localzone()), file.file_name
)
if soundscape_id is None:
return
soundscape_id = sdata['soundscape']['id']

for detection in detections:
# POST detection to server
detection_url = f'https://app.birdweather.com/api/v1/stations/{conf["BIRDWEATHER_ID"]}/detections'

data = {
'timestamp': detection.iso8601,
'lat': conf['LATITUDE'],
'lon': conf['LONGITUDE'],
'soundscapeId': soundscape_id,
'soundscapeStartTime': (detection.start_datetime - file.file_date).seconds,
'soundscapeEndTime': (detection.stop_datetime - file.file_date).seconds,
'commonName': detection.common_name,
'scientificName': detection.scientific_name,
'algorithm': '2p4' if conf['MODEL'] == 'BirdNET_GLOBAL_6K_V2.4_Model_FP16' else 'alpha',
'confidence': detection.confidence,
}

log.debug(data)
try:
response = requests.post(detection_url, json=data, timeout=20)
log.info("Detection POST Response Status - %d", response.status_code)
except BaseException as e:
log.error("Cannot POST detection: %s", e)

post_detection_to_birdweather(
detection,
soundscape_id,
file.file_date,
conf["BIRDWEATHER_ID"],
conf['LATITUDE'],
conf['LONGITUDE'],
conf['MODEL'],
)


def heartbeat():
Expand Down

0 comments on commit eb554f4

Please sign in to comment.