diff --git a/TODO b/TODO index e69de29..73a22e5 100644 --- a/TODO +++ b/TODO @@ -0,0 +1 @@ +- Fix install of venv diff --git a/config/analyzer.conf b/config/analyzer.conf index 80ef0a6..d4875f3 100644 --- a/config/analyzer.conf +++ b/config/analyzer.conf @@ -7,7 +7,7 @@ LOCATION="Maison ORTION - Saint-Gervais-en-Belin" # Species selection list SPECIES_LIST="./config/species_list.txt" # Minimal confidence threshold -CONFIDENCE=0.25 +CONFIDENCE=0.1 # Recording duration (in seconds) RECORDING_DURATION=15 # Chunk folder location diff --git a/daemon/birdnet_clean.sh b/daemon/birdnet_clean.sh index b9efb00..f6a4c03 100755 --- a/daemon/birdnet_clean.sh +++ b/daemon/birdnet_clean.sh @@ -28,9 +28,9 @@ mem() { string=$2 substring=$1 if [[ "$string" == *"$substring"* ]]; then - echo "true" + true else - echo "false" + false fi } @@ -49,7 +49,7 @@ junk() { # Get all empty treatment directories junk="$junk $(find ${CHUNK_FOLDER}/out -type d -empty)" # Get all empty record directories - treatement_folder=$(find "${CHUNK_FOLDER}/out" -type d ! -empty) + treatement_folder=$(find "${CHUNK_FOLDER}/out/*" -type d ! -empty) if [[ ! -z ${treatement_folder} ]]; then for folder in $treatement_folder; do echo $folder diff --git a/daemon/birdnet_miner.sh b/daemon/birdnet_miner.sh new file mode 100755 index 0000000..4802e22 --- /dev/null +++ b/daemon/birdnet_miner.sh @@ -0,0 +1,120 @@ +#! /usr/bin/env bash +# Extract observations from a model output folder +# + +DEBUG=${DEBUG:-0} + +set -e + +debug() { + if [ $DEBUG -eq 1 ]; then + echo "$1" + fi +} + +# Load bash library to deal with BirdNET-stream database +source ./daemon/database/scripts/database.sh + +# Load config +source ./config/analyzer.conf +# Check config +if [[ -z ${CHUNK_FOLDER} ]]; then + echo "CHUNK_FOLDER is not set" + exit 1 +else + if [[ ! -d ${CHUNK_FOLDER}/out ]]; then + echo "CHUNK_FOLDER does not exist: ${CHUNK_FOLDER}/out" + echo "Cannot extract observations." + exit 1 + fi +fi + +if [[ -z ${LATITUDE} ]]; then + echo "LATITUDE is not set" + exit 1 +fi + +if [[ -z ${LONGITUDE} ]]; then + echo "LONGITUDE is not set" + exit 1 +fi + +model_outputs() { + find -name "model.out.csv" -type f ! -empty +} + +source_wav() { + model_output_path=$1 + model_output_dir=$(dirname $model_output_path) + source_wav=$(basename $model_output_dir | rev | cut --complement -d"." -f1 | rev) + echo $source_wav +} + +record_datetime() { + source_wav=$1 + source_base=$(basename $source_wav .wav) + record_date=$(echo $source_base | cut -d"_" -f2) + record_time=$(echo $source_base | cut -d"_" -f3) + YYYY=$(echo $record_date | cut -c 1-4) + MM=$(echo $record_date | cut -c 5-6) + DD=$(echo $record_date | cut -c 7-8) + HH=$(echo $record_time | cut -c 1-2) + MM=$(echo $record_time | cut -c 3-4) + SS=$(echo $record_time | cut -c 5-6) + SSS="000" + date="$YYYY-$MM-$DD $HH:$MM:$SS.$SSS" + echo $date +} + +save_observations() { + model_output_path=$1 + source_audio=$(source_wav $model_output_path) + debug "Audio source: $source_audio" + observations=$(cat $model_output_path | tail -n +2) + IFS=$'\n' + for observation in $observations; do + if [[ -z "$observation" ]]; then + continue + fi + # debug "Observation: $observation" + start=$(echo "$observation" | cut -d"," -f1) + end=$(echo "$observation" | cut -d"," -f2) + scientific_name=$(echo "$observation" | cut -d"," -f3) + common_name=$(echo "$observation" | cut -d"," -f4) + confidence=$(echo "$observation" | cut -d"," -f5) + debug "Observation: $scientific_name ($common_name) from $start to $end with confidence $confidence" + taxon_id=$(get_taxon_id "$scientific_name") + if [[ -z $taxon_id ]]; then + debug "Taxon not found: $scientific_name" + debug "Inserting taxon..." + insert_taxon "$scientific_name" "$common_name" + taxon_id=$(get_taxon_id "$scientific_name") + fi + location_id=$(get_location_id "$LATITUDE" "$LONGITUDE") + if [[ -z $location_id ]]; then + debug "Location not found: $LATITUDE, $LONGITUDE" + debug "Inserting location..." + insert_location "$LATITUDE" "$LONGITUDE" + location_id=$(get_location_id "$LATITUDE" "$LONGITUDE") + fi + datetime=$(record_datetime $source_audio) + if [[ $(observation_exists "$source_audio" "$start" "$end" "$taxon_id" "$location_id") = "true" ]]; then + debug "Observation already exists: $source_audio, $start, $end, $taxon_id, $location_id" + exit 1 + else + debug "Inserting observation: $source_audio, $start, $end, $taxon_id, $location_id" + insert_observation "$source_audio" "$start" "$end" "$taxon_id" "$location_id" "$confidence" "$datetime" + fi + done +} + +main() { + # Remove all junk observations + ./daemon/birdnet_clean.sh + # Get model outputs + for model_output in $(model_outputs); do + save_observations $model_output + done +} + +main diff --git a/daemon/birdnet_recording.sh b/daemon/birdnet_recording.sh index 0dc09b6..c8448cf 100755 --- a/daemon/birdnet_recording.sh +++ b/daemon/birdnet_recording.sh @@ -32,7 +32,7 @@ record() { DEVICE=$1 DURATION=$2 debug "Recording from $DEVICE for $DURATION seconds" - echo "" | ffmpeg -nostdin -f pulse -i ${DEVICE} -t ${DURATION} -vn -acodec pcm_s16le -ac 1 -ar 48000 file:${CHUNK_FOLDER}/in/birdnet_$(date "+%Y%m%d_%H%M%S").wav + ffmpeg -nostdin -f pulse -i ${DEVICE} -t ${DURATION} -vn -acodec pcm_s16le -ac 1 -ar 48000 file:${CHUNK_FOLDER}/in/birdnet_$(date "+%Y%m%d_%H%M%S").wav } config_filepath="./config/analyzer.conf" diff --git a/.ideas/create_database.sh b/daemon/database/scripts/create.sh similarity index 100% rename from .ideas/create_database.sh rename to daemon/database/scripts/create.sh diff --git a/daemon/database/scripts/database.sh b/daemon/database/scripts/database.sh new file mode 100755 index 0000000..7746614 --- /dev/null +++ b/daemon/database/scripts/database.sh @@ -0,0 +1,36 @@ +#! /usr/bin/env bash +# SQLite library to deal with BirdNET-stream database + +set -e + +source ./config/analyzer.conf + +# Create database in case it was not created yet +./daemon/database/scripts/create.sh + +DATABASE=${DATABASE:-"./var/db.sqlite"} + +get_location_id() { + sqlite3 $DATABASE "SELECT location_id FROM location WHERE latitude=$1 AND longitude=$2" +} + +get_taxon_id() { + sqlite3 $DATABASE "SELECT taxon_id FROM taxon WHERE scientific_name='$1'" +} + +insert_taxon() { + sqlite3 $DATABASE "INSERT INTO taxon (scientific_name, common_name) VALUES ('$1', '$2')" +} + +insert_location() { + sqlite3 $DATABASE "INSERT INTO location (latitude, longitude) VALUES ($1, $2)" +} + +insert_observation() { + sqlite3 $DATABASE "INSERT INTO observation (audio_file, start, end, taxon_id, location_id, confidence, date) VALUES ('$1', '$2', '$3', '$4', '$5', '$6', '$7')" +} + +# Check if the observation already exists in the database +observation_exists() { + sqlite3 $DATABASE "SELECT EXISTS(SELECT observation_id FROM observation WHERE audio_file='$1' AND start='$2' AND end='$3' AND taxon_id='$4' AND location_id='$5')" +} \ No newline at end of file diff --git a/daemon/database/structure.sql b/daemon/database/structure.sql index fd736e0..2397785 100644 --- a/daemon/database/structure.sql +++ b/daemon/database/structure.sql @@ -7,23 +7,24 @@ CREATE TABLE IF NOT EXISTS taxon ( common_name TEXT NOT NULL ); -/** Locality table */ -CREATE TABLE IF NOT EXISTS locality ( - locality_id INTEGER PRIMARY KEY, - name TEXT NOT NULL, +/** Location table */ +CREATE TABLE IF NOT EXISTS location ( + location_id INTEGER PRIMARY KEY, latitude REAL NOT NULL, longitude REAL NOT NULL ); /** Observation table */ CREATE TABLE IF NOT EXISTS observation ( - observation_id INTEGER PRIMARY KEY, - taxon_id INTEGER NOT NULL, - locality_id INTEGER NOT NULL, - date TEXT NOT NULL, - time TEXT NOT NULL, - notes TEXT, - confidence REAL NOT NULL, + `observation_id` INTEGER PRIMARY KEY, + `audio_file` TEXT NOT NULL, + `start` REAL NOT NULL, + `end` REAL NOT NULL, + `taxon_id` INTEGER NOT NULL, + `location_id` INTEGER NOT NULL, + `date` TEXT NOT NULL, + `notes` TEXT, + `confidence` REAL NOT NULL, FOREIGN KEY(taxon_id) REFERENCES taxon(taxon_id), - FOREIGN KEY(locality_id) REFERENCES locality(locality_id) + FOREIGN KEY(location_id) REFERENCES location(location_id) ); \ No newline at end of file diff --git a/install.sh b/install.sh index 406b62c..632106e 100755 --- a/install.sh +++ b/install.sh @@ -43,12 +43,12 @@ install_birdnetstream() { workdir=$(pwd) if [ -d "$workdir/BirdNET-stream" ]; then debug "BirdNET-stream is already installed" - return + else + # Clone BirdNET-stream + debug "Cloning BirdNET-stream from $REPOSITORY" + git clone --recurse-submodules $REPOSITORY + # Install BirdNET-stream fi - # Clone BirdNET-stream - debug "Cloning BirdNET-stream from $REPOSITORY" - git clone --recurse-submodules $REPOSITORY - # Install BirdNET-stream cd BirdNET-stream debug "Creating python3 virtual environment '$PYTHON_VENV'" python3 -m venv $PYTHON_VENV diff --git a/www/assets/app.js b/www/assets/app.js index bb0a6aa..1d730eb 100644 --- a/www/assets/app.js +++ b/www/assets/app.js @@ -10,3 +10,5 @@ import './styles/app.css'; // start the Stimulus application import './bootstrap'; + +import './utils/spectro'; \ No newline at end of file diff --git a/www/assets/styles/app.css b/www/assets/styles/app.css index a392848..015ca5c 100644 --- a/www/assets/styles/app.css +++ b/www/assets/styles/app.css @@ -81,3 +81,9 @@ nav ul li a:hover { box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); z-index: 1; } + +canvas { + display: block; + height: 100%; + width: 100%; +} \ No newline at end of file diff --git a/www/assets/utils/spectro.js b/www/assets/utils/spectro.js new file mode 100644 index 0000000..b7f56f4 --- /dev/null +++ b/www/assets/utils/spectro.js @@ -0,0 +1,63 @@ +/** + * Credits to: + * https://codepen.io/jakealbaugh/pen/jvQweW + */ + +// UPDATE: there is a problem in chrome with starting audio context +// before a user gesture. This fixes it. +var started = false; +var spectro_button = document.getElementById('spectro-button'); +spectro_button.addEventListener('click', () => { + if (started) return; + started = true; + console.log("starting spectro"); + initialize(); +}) + +function initialize() { + const CVS = document.getElementById('spectro-canvas'); + const CTX = CVS.getContext('2d'); + const W = CVS.width = window.innerWidth; + const H = CVS.height = window.innerHeight; + + const ACTX = new AudioContext(); + const ANALYSER = ACTX.createAnalyser(); + + ANALYSER.fftSize = 4096; + + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then(process); + + function process(stream) { + const SOURCE = ACTX.createMediaStreamSource(stream); + SOURCE.connect(ANALYSER); + const DATA = new Uint8Array(ANALYSER.frequencyBinCount); + const LEN = DATA.length; + const h = H / LEN; + const x = W - 1; + CTX.fillStyle = 'hsl(280, 100%, 10%)'; + CTX.fillRect(0, 0, W, H); + + loop(); + + function loop() { + window.requestAnimationFrame(loop); + let imgData = CTX.getImageData(1, 0, W - 1, H); + CTX.fillRect(0, 0, W, H); + CTX.putImageData(imgData, 0, 0); + ANALYSER.getByteFrequencyData(DATA); + for (let i = 0; i < LEN; i++) { + let rat = DATA[i] / 255; + let hue = Math.round((rat * 120) + 280 % 360); + let sat = '100%'; + let lit = 10 + (70 * rat) + '%'; + CTX.beginPath(); + CTX.strokeStyle = `hsl(${hue}, ${sat}, ${lit})`; + CTX.moveTo(x, H - (i * h)); + CTX.lineTo(x, H - (i * h + h)); + CTX.stroke(); + } + } + } +} diff --git a/www/nginx.conf b/www/nginx.conf index 5661aa7..263024d 100644 --- a/www/nginx.conf +++ b/www/nginx.conf @@ -4,9 +4,8 @@ server { root /var/www/html; location / { - return 302 https://$host$request_uri; + return 302 https://$server_name$request_uri; } - } server { diff --git a/www/src/Controller/SpectroController.php b/www/src/Controller/SpectroController.php new file mode 100644 index 0000000..771b453 --- /dev/null +++ b/www/src/Controller/SpectroController.php @@ -0,0 +1,21 @@ +render('spectro/index.html.twig', [ + + ]); + } +} \ No newline at end of file diff --git a/www/templates/spectro/index.html.twig b/www/templates/spectro/index.html.twig new file mode 100644 index 0000000..293885c --- /dev/null +++ b/www/templates/spectro/index.html.twig @@ -0,0 +1,9 @@ +{% extends "base.html.twig" %} + +{% block content %} +