Add spectro webpage (not the audio of the server yet) & dataminer script

This commit is contained in:
Samuel Ortion 2022-08-13 16:00:08 +02:00
parent 0399fa085e
commit 6b37e1cfc5
15 changed files with 282 additions and 24 deletions

1
TODO
View File

@ -0,0 +1 @@
- Fix install of venv

View File

@ -7,7 +7,7 @@ LOCATION="Maison ORTION - Saint-Gervais-en-Belin"
# Species selection list # Species selection list
SPECIES_LIST="./config/species_list.txt" SPECIES_LIST="./config/species_list.txt"
# Minimal confidence threshold # Minimal confidence threshold
CONFIDENCE=0.25 CONFIDENCE=0.1
# Recording duration (in seconds) # Recording duration (in seconds)
RECORDING_DURATION=15 RECORDING_DURATION=15
# Chunk folder location # Chunk folder location

View File

@ -28,9 +28,9 @@ mem() {
string=$2 string=$2
substring=$1 substring=$1
if [[ "$string" == *"$substring"* ]]; then if [[ "$string" == *"$substring"* ]]; then
echo "true" true
else else
echo "false" false
fi fi
} }
@ -49,7 +49,7 @@ junk() {
# Get all empty treatment directories # Get all empty treatment directories
junk="$junk $(find ${CHUNK_FOLDER}/out -type d -empty)" junk="$junk $(find ${CHUNK_FOLDER}/out -type d -empty)"
# Get all empty record directories # 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 if [[ ! -z ${treatement_folder} ]]; then
for folder in $treatement_folder; do for folder in $treatement_folder; do
echo $folder echo $folder

120
daemon/birdnet_miner.sh Executable file
View File

@ -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

View File

@ -32,7 +32,7 @@ record() {
DEVICE=$1 DEVICE=$1
DURATION=$2 DURATION=$2
debug "Recording from $DEVICE for $DURATION seconds" 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" config_filepath="./config/analyzer.conf"

View File

@ -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')"
}

View File

@ -7,23 +7,24 @@ CREATE TABLE IF NOT EXISTS taxon (
common_name TEXT NOT NULL common_name TEXT NOT NULL
); );
/** Locality table */ /** Location table */
CREATE TABLE IF NOT EXISTS locality ( CREATE TABLE IF NOT EXISTS location (
locality_id INTEGER PRIMARY KEY, location_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
latitude REAL NOT NULL, latitude REAL NOT NULL,
longitude REAL NOT NULL longitude REAL NOT NULL
); );
/** Observation table */ /** Observation table */
CREATE TABLE IF NOT EXISTS observation ( CREATE TABLE IF NOT EXISTS observation (
observation_id INTEGER PRIMARY KEY, `observation_id` INTEGER PRIMARY KEY,
taxon_id INTEGER NOT NULL, `audio_file` TEXT NOT NULL,
locality_id INTEGER NOT NULL, `start` REAL NOT NULL,
date TEXT NOT NULL, `end` REAL NOT NULL,
time TEXT NOT NULL, `taxon_id` INTEGER NOT NULL,
notes TEXT, `location_id` INTEGER NOT NULL,
confidence REAL NOT NULL, `date` TEXT NOT NULL,
`notes` TEXT,
`confidence` REAL NOT NULL,
FOREIGN KEY(taxon_id) REFERENCES taxon(taxon_id), FOREIGN KEY(taxon_id) REFERENCES taxon(taxon_id),
FOREIGN KEY(locality_id) REFERENCES locality(locality_id) FOREIGN KEY(location_id) REFERENCES location(location_id)
); );

View File

@ -43,12 +43,12 @@ install_birdnetstream() {
workdir=$(pwd) workdir=$(pwd)
if [ -d "$workdir/BirdNET-stream" ]; then if [ -d "$workdir/BirdNET-stream" ]; then
debug "BirdNET-stream is already installed" 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 fi
# Clone BirdNET-stream
debug "Cloning BirdNET-stream from $REPOSITORY"
git clone --recurse-submodules $REPOSITORY
# Install BirdNET-stream
cd BirdNET-stream cd BirdNET-stream
debug "Creating python3 virtual environment '$PYTHON_VENV'" debug "Creating python3 virtual environment '$PYTHON_VENV'"
python3 -m venv $PYTHON_VENV python3 -m venv $PYTHON_VENV

View File

@ -10,3 +10,5 @@ import './styles/app.css';
// start the Stimulus application // start the Stimulus application
import './bootstrap'; import './bootstrap';
import './utils/spectro';

View File

@ -81,3 +81,9 @@ nav ul li a:hover {
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 1; z-index: 1;
} }
canvas {
display: block;
height: 100%;
width: 100%;
}

View File

@ -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();
}
}
}
}

View File

@ -4,9 +4,8 @@ server {
root /var/www/html; root /var/www/html;
location / { location / {
return 302 https://$host$request_uri; return 302 https://$server_name$request_uri;
} }
} }
server { server {

View File

@ -0,0 +1,21 @@
<?php
// src/Controller/AboutController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class SpectroController extends AbstractController
{
/**
* @Route("/spectro", name="spectro")
*/
public function about()
{
return $this->render('spectro/index.html.twig', [
]);
}
}

View File

@ -0,0 +1,9 @@
{% extends "base.html.twig" %}
{% block content %}
<h2>{{ "Spectrogram" | trans }}</h2>
<div>
<button id="spectro-button">{{ "Launch Live Spectrogram" | trans }}</button>
<canvas id="spectro-canvas"></canvas>
</div>
{% endblock %}