From bd8a7bcbd6a31a7274ad95fbd59ce49fe3f09064 Mon Sep 17 00:00:00 2001 From: Samuel ORTION Date: Fri, 12 Aug 2022 18:01:01 +0200 Subject: [PATCH] BirdNET on the river --- .env | 1 + .gitignore | 3 + .gitmodules | 3 + .ideas/create_database.sh | 21 +++ .ideas/database_entity_model.svg | 202 ++++++++++++++++++++++++ .ideas/fill_taxa.sh | 70 +++++++++ .ideas/insert_observation.sh | 41 +++++ .ideas/observation_template.sql | 3 + INSTALL.md | 37 +++++ README.md | 13 +- analyzer | 1 + config/analyzer.conf | 20 +++ config/species_list.txt | 189 +++++++++++++++++++++++ daemon/analyze-stream.sh | 90 +++++++++++ daemon/database/scripts/database.py | 83 ++++++++++ daemon/database/structure.sql | 29 ++++ daemon/dist/birdnet-stream.service | 0 daemon/miner.sh | 35 +++++ daemon/record-chunks.sh | 28 ++++ daemon/weekof.sh | 9 ++ media/logo.svg | 231 ++++++++++++++++++++++++++++ requirements.txt | 24 +++ utils/purge.sh | 24 +++ 23 files changed, 1156 insertions(+), 1 deletion(-) create mode 100644 .env create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100755 .ideas/create_database.sh create mode 100644 .ideas/database_entity_model.svg create mode 100755 .ideas/fill_taxa.sh create mode 100644 .ideas/insert_observation.sh create mode 100644 .ideas/observation_template.sql create mode 100644 INSTALL.md create mode 160000 analyzer create mode 100644 config/analyzer.conf create mode 100644 config/species_list.txt create mode 100755 daemon/analyze-stream.sh create mode 100755 daemon/database/scripts/database.py create mode 100644 daemon/database/structure.sql create mode 100644 daemon/dist/birdnet-stream.service create mode 100644 daemon/miner.sh create mode 100755 daemon/record-chunks.sh create mode 100755 daemon/weekof.sh create mode 100644 media/logo.svg create mode 100644 requirements.txt create mode 100755 utils/purge.sh diff --git a/.env b/.env new file mode 100644 index 0000000..75af76d --- /dev/null +++ b/.env @@ -0,0 +1 @@ +CUDA_VISIBLE_DEVICES="" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f334a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +var/ +/.venv/ +/analyzer/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ab60bb3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "BirdNET-Analyzer"] + path = analyzer + url = git@github.com:kahst/BirdNET-Analyzer.git diff --git a/.ideas/create_database.sh b/.ideas/create_database.sh new file mode 100755 index 0000000..943e910 --- /dev/null +++ b/.ideas/create_database.sh @@ -0,0 +1,21 @@ +#! /usr/bin/env bash + +# Load config file +config_filepath="./config/analyzer.conf" + +if [ -f "$config_filepath" ]; then + source "$config_filepath" +else + echo "Config file not found: $config_filepath" + exit 1 +fi + +# Check if database location is specified +if [ -z "$DATABASE" ]; then + echo "DATABASE location not specified" + echo "Defaults to ./var/db.sqlite" + DATABASE="./var/db.sqlite" +fi + +# Create database according to schema in structure.sql +sqlite3 "$DATABASE" < ./daemon/database/structure.sql \ No newline at end of file diff --git a/.ideas/database_entity_model.svg b/.ideas/database_entity_model.svg new file mode 100644 index 0000000..c8b2242 --- /dev/null +++ b/.ideas/database_entity_model.svg @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + birdnet_stream_db + taxon + # taxon_idscientific_namecommon_name + + + + + observation + # observation_idtaxon_idlocality_iddatetimenotesconfidence + + + + diff --git a/.ideas/fill_taxa.sh b/.ideas/fill_taxa.sh new file mode 100755 index 0000000..b0348fe --- /dev/null +++ b/.ideas/fill_taxa.sh @@ -0,0 +1,70 @@ +#! /usr/bin/env bash + +set -e + +# Load config file +config_filepath="./config/analyzer.conf" + +if [ -f "$config_filepath" ]; then + source "$config_filepath" +else + echo "Config file not found: $config_filepath" + exit 1 +fi + +# Check if database location is specified +if [ -z "$DATABASE" ]; then + echo "DATABASE location not specified" + echo "Defaults to ./var/db.sqlite" + DATABASE="./var/db.sqlite" +fi + +# Check if species list is specified +if [ -z "$SPECIES_LIST" ]; then + echo "SPECIES_LIST location not specified" + exit 1 +fi + +function insert_taxa() +{ + for taxon in "$(cat $SPECIES_LIST)"; do + taxon_scientific_name=$(echo $taxon | cut -d'_' -f1) + taxon_common_name=$(echo $taxon | cut -d'_' -f2) + if $(taxon_exists $taxon_scientific_name); then + echo "Taxon already exists: $taxon_scientific_name" + else + echo "Inserting taxon: $taxon_scientific_name" + statement="INSERT INTO taxon (scientific_name, common_name) VALUES ('$taxon_scientific_name', '$taxon_common_name')" + echo $statement + result=$(sqlite3 "$DATABASE" "$statement") + echo "$result" + fi + done +} + +function taxon_exists() +{ + taxon_scientific_name="$1" + statement="SELECT scientific_name FROM taxon WHERE scientific_name='$taxon_scientific_name'" + result=$(sqlite3 "$DATABASE" "$statement") + if [ -z "$result" ]; then + return 1 + else + return 0 + fi +} + +function purge_taxa() +{ + statement="DELETE FROM taxon" + result=$(sqlite3 "$DATABASE" "$statement") + echo "$result" +} + +function main() +{ + purge_taxa + insert_taxa +} + +main \ No newline at end of file diff --git a/.ideas/insert_observation.sh b/.ideas/insert_observation.sh new file mode 100644 index 0000000..73123d9 --- /dev/null +++ b/.ideas/insert_observation.sh @@ -0,0 +1,41 @@ +#! /usr/bin/env bash + +# Load config file +config_filepath="./config/analyzer.conf" + +if [ -f "$config_filepath" ]; then + source "$config_filepath" +else + echo "Config file not found: $config_filepath" + exit 1 +fi + +# Check if database location is specified +if [ -z "$DATABASE" ]; then + echo "DATABASE location not specified" + echo "Defaults to ./var/db.sqlite" + DATABASE="./var/db.sqlite" +fi + +function insert_observation() +{ + # Insert observation into database + template=$(cat ./daemon/database/observation_template.sql) + statement=$(echo "$template" | sed "s/:taxon_id/$1/g" | sed "s/:location_id/$2/g" | sed "s/:date/$3/g" | sed "s/:time/$4/g" | sed "s/:confidence/$5/g" | sed "s/:notes/$6/g") + result=$(sqlite3 "$DATABASE" $statement) + echo "$result" +} + +function get_taxon_id() +{ + # Get taxon id from database + statement="SELECT taxon_id FROM taxon WHERE scientific_name='$1'" + result=$(sqlite3 "$DATABASE" "$statement") + echo "$result" +} + +function test() +{ + taxon_scientific_name="Erithacus rubecula" + taxon_id=$(get_taxon_id "$taxon_scientific_name") +} \ No newline at end of file diff --git a/.ideas/observation_template.sql b/.ideas/observation_template.sql new file mode 100644 index 0000000..57c0f14 --- /dev/null +++ b/.ideas/observation_template.sql @@ -0,0 +1,3 @@ +/** Observation database entry template */ + +INSERT INTO observation (taxon_id, locality_id, date, time, confidence) VALUES (:taxon_id, :locality_id, :date, :time, :confidence); \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..7a793ff --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,37 @@ +# Installation Guide for BirdNET-stream + +## Requirements + +- git +- ffmpeg +- python3 + +## Install process + +### Install python requirements + +```bash +sudo apt-get update +sudo apt-get install python3-dev python3-pip +sudo pip3 install --upgrade pip +``` + +### Install ffmpeg + +```bash +sudo apt-get install ffmpeg +``` + +### Clone BirdNET-stream repository + +```bash +git clone https://forge.chapril.org/UncleSamulus/BirdNET-stream.git + +### Setup python virtualenv and packages + +```bash +python3 -m venv .venv/birdnet-stream +source .venv/birdnet-stream + +pip install -r requirements.txt +``` diff --git a/README.md b/README.md index a6aaaae..7a6be76 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,14 @@ # BirdNET-stream -BirdNET powered soundscape analysis for bird song identification \ No newline at end of file +
+ BirdNET-stream logo image IA generated +
+ +Realtime BirdNET powered soundscape analysis for bird song identification. + + +## Acknoledgements + +- [BirdNET](https://birdnet.cornell.edu) on which this project relies +- [BirdNET-Pi](https://birdnetpi.com) the great inspiration of this project + diff --git a/analyzer b/analyzer new file mode 160000 index 0000000..08f0315 --- /dev/null +++ b/analyzer @@ -0,0 +1 @@ +Subproject commit 08f031585265e5bc22e86a60ecbb0310c937a29b diff --git a/config/analyzer.conf b/config/analyzer.conf new file mode 100644 index 0000000..80ef0a6 --- /dev/null +++ b/config/analyzer.conf @@ -0,0 +1,20 @@ +# Configuration file for BirdNET Analyzer + +# Coordinates of the recorder +LATITUDE="47.87842" +LONGITUDE="0.21826" +LOCATION="Maison ORTION - Saint-Gervais-en-Belin" +# Species selection list +SPECIES_LIST="./config/species_list.txt" +# Minimal confidence threshold +CONFIDENCE=0.25 +# Recording duration (in seconds) +RECORDING_DURATION=15 +# Chunk folder location +CHUNK_FOLDER="./var/chunks" +# Audio recording device (pulseaudio) +AUDIO_DEVICE="default" +# Virtual env for BirdNET AI with required packages +PYTHON_VENV="./.venv/birdnet-stream" +# Database location +DATABASE="./var/db.sqlite" diff --git a/config/species_list.txt b/config/species_list.txt new file mode 100644 index 0000000..169c290 --- /dev/null +++ b/config/species_list.txt @@ -0,0 +1,189 @@ +Acanthis cabaret_Lesser Redpoll +Accipiter nisus_Eurasian Sparrowhawk +Acrocephalus palustris_Marsh Warbler +Acrocephalus schoenobaenus_Sedge Warbler +Acrocephalus scirpaceus_Eurasian Reed Warbler +Actitis hypoleucos_Common Sandpiper +Aegithalos caudatus_Long-tailed Tit +Aix galericulata_Mandarin Duck +Alauda arvensis_Eurasian Skylark +Alcedo atthis_Common Kingfisher +Alectoris rufa_Red-legged Partridge +Alopochen aegyptiaca_Egyptian Goose +Anas acuta_Northern Pintail +Anas crecca_Green-winged Teal +Anas platyrhynchos_Mallard +Anser albifrons_Greater White-fronted Goose +Anser anser_Graylag Goose +Anthus petrosus_Rock Pipit +Anthus pratensis_Meadow Pipit +Anthus spinoletta_Water Pipit +Anthus trivialis_Tree Pipit +Apus apus_Common Swift +Aquila chrysaetos_Golden Eagle +Ardea alba_Great Egret +Ardea cinerea_Gray Heron +Ardea purpurea_Purple Heron +Arenaria interpres_Ruddy Turnstone +Aythya ferina_Common Pochard +Aythya fuligula_Tufted Duck +Aythya marila_Greater Scaup +Branta bernicla_Brant +Branta canadensis_Canada Goose +Bubulcus ibis_Cattle Egret +Bucephala clangula_Common Goldeneye +Buteo buteo_Common Buzzard +Calidris alpina_Dunlin +Calidris melanotos_Pectoral Sandpiper +Calidris pugnax_Ruff +Carduelis carduelis_European Goldfinch +Certhia brachydactyla_Short-toed Treecreeper +Certhia familiaris_Eurasian Treecreeper +Cettia cetti_Cetti's Warbler +Charadrius dubius_Little Ringed Plover +Charadrius hiaticula_Common Ringed Plover +Chlidonias hybrida_Whiskered Tern +Chloris chloris_European Greenfinch +Chroicocephalus ridibundus_Black-headed Gull +Ciconia ciconia_White Stork +Cinclus cinclus_White-throated Dipper +Circaetus gallicus_Short-toed Snake-Eagle +Circus aeruginosus_Eurasian Marsh-Harrier +Circus pygargus_Montagu's Harrier +Cisticola juncidis_Zitting Cisticola +Coccothraustes coccothraustes_Hawfinch +Columba livia_Rock Pigeon +Columba oenas_Stock Dove +Columba palumbus_Common Wood-Pigeon +Corvus corax_Common Raven +Corvus corone_Carrion Crow +Corvus frugilegus_Rook +Corvus monedula_Eurasian Jackdaw +Coturnix coturnix_Common Quail +Cuculus canorus_Common Cuckoo +Curruca communis_Greater Whitethroat +Curruca curruca_Lesser Whitethroat +Curruca undata_Dartford Warbler +Cyanistes caeruleus_Eurasian Blue Tit +Cygnus olor_Mute Swan +Delichon urbicum_Common House-Martin +Dendrocopos major_Great Spotted Woodpecker +Dendrocoptes medius_Middle Spotted Woodpecker +Dryobates minor_Lesser Spotted Woodpecker +Dryocopus martius_Black Woodpecker +Egretta garzetta_Little Egret +Emberiza calandra_Corn Bunting +Emberiza cirlus_Cirl Bunting +Emberiza citrinella_Yellowhammer +Emberiza schoeniclus_Reed Bunting +Erithacus rubecula_European Robin +Falco peregrinus_Peregrine Falcon +Falco subbuteo_Eurasian Hobby +Falco tinnunculus_Eurasian Kestrel +Ficedula hypoleuca_European Pied Flycatcher +Fringilla coelebs_Common Chaffinch +Fringilla montifringilla_Brambling +Fulica atra_Eurasian Coot +Gallinago gallinago_Common Snipe +Gallinula chloropus_Eurasian Moorhen +Garrulus glandarius_Eurasian Jay +Grus grus_Common Crane +Haematopus ostralegus_Eurasian Oystercatcher +Himantopus himantopus_Black-winged Stilt +Hippolais polyglotta_Melodious Warbler +Hirundo rustica_Barn Swallow +Ichthyaetus melanocephalus_Mediterranean Gull +Lanius collurio_Red-backed Shrike +Larus argentatus_Herring Gull +Larus canus_Common Gull +Larus fuscus_Lesser Black-backed Gull +Larus marinus_Great Black-backed Gull +Larus michahellis_Yellow-legged Gull +Limosa lapponica_Bar-tailed Godwit +Limosa limosa_Black-tailed Godwit +Linaria cannabina_Eurasian Linnet +Locustella naevia_Common Grasshopper-Warbler +Lophophanes cristatus_Crested Tit +Loxia curvirostra_Red Crossbill +Lullula arborea_Wood Lark +Luscinia megarhynchos_Common Nightingale +Luscinia svecica_Bluethroat +Mareca penelope_Eurasian Wigeon +Mareca strepera_Gadwall +Mergus merganser_Common Merganser +Milvus migrans_Black Kite +Milvus milvus_Red Kite +Morus bassanus_Northern Gannet +Motacilla alba_White Wagtail +Motacilla cinerea_Gray Wagtail +Motacilla flava_Western Yellow Wagtail +Muscicapa striata_Spotted Flycatcher +Numenius arquata_Eurasian Curlew +Nycticorax nycticorax_Black-crowned Night-Heron +Oenanthe oenanthe_Northern Wheatear +Oriolus oriolus_Eurasian Golden Oriole +Pandion haliaetus_Osprey +Panurus biarmicus_Bearded Reedling +Parus major_Great Tit +Passer domesticus_House Sparrow +Passer montanus_Eurasian Tree Sparrow +Perdix perdix_Gray Partridge +Periparus ater_Coal Tit +Pernis apivorus_European Honey-buzzard +Phalacrocorax carbo_Great Cormorant +Phasianus colchicus_Ring-necked Pheasant +Phoenicurus ochruros_Black Redstart +Phoenicurus phoenicurus_Common Redstart +Phylloscopus bonelli_Western Bonelli's Warbler +Phylloscopus collybita_Common Chiffchaff +Phylloscopus ibericus_Iberian Chiffchaff +Phylloscopus trochilus_Willow Warbler +Pica pica_Eurasian Magpie +Picus viridis_Eurasian Green Woodpecker +Pluvialis apricaria_European Golden-Plover +Pluvialis squatarola_Black-bellied Plover +Podiceps cristatus_Great Crested Grebe +Poecile montanus_Willow Tit +Poecile palustris_Marsh Tit +Prunella modularis_Dunnock +Psittacula krameri_Rose-ringed Parakeet +Pyrrhocorax pyrrhocorax_Red-billed Chough +Pyrrhula pyrrhula_Eurasian Bullfinch +Rallus aquaticus_Water Rail +Recurvirostra avosetta_Pied Avocet +Regulus ignicapilla_Common Firecrest +Regulus regulus_Goldcrest +Remiz pendulinus_Eurasian Penduline-Tit +Riparia riparia_Bank Swallow +Saxicola rubetra_Whinchat +Saxicola rubicola_European Stonechat +Serinus serinus_European Serin +Sitta europaea_Eurasian Nuthatch +Spatula clypeata_Northern Shoveler +Spatula querquedula_Garganey +Spinus spinus_Eurasian Siskin +Sterna hirundo_Common Tern +Sternula albifrons_Little Tern +Streptopelia decaocto_Eurasian Collared-Dove +Streptopelia turtur_European Turtle-Dove +Strix aluco_Tawny Owl +Sturnus vulgaris_European Starling +Sylvia atricapilla_Eurasian Blackcap +Sylvia borin_Garden Warbler +Tachybaptus ruficollis_Little Grebe +Tadorna tadorna_Common Shelduck +Thalasseus sandvicensis_Sandwich Tern +Tringa erythropus_Spotted Redshank +Tringa glareola_Wood Sandpiper +Tringa nebularia_Common Greenshank +Tringa ochropus_Green Sandpiper +Tringa totanus_Common Redshank +Troglodytes troglodytes_Eurasian Wren +Turdus iliacus_Redwing +Turdus merula_Eurasian Blackbird +Turdus philomelos_Song Thrush +Turdus pilaris_Fieldfare +Turdus viscivorus_Mistle Thrush +Tyto alba_Barn Owl +Uria aalge_Common Murre +Vanellus vanellus_Northern Lapwing diff --git a/daemon/analyze-stream.sh b/daemon/analyze-stream.sh new file mode 100755 index 0000000..8230e9d --- /dev/null +++ b/daemon/analyze-stream.sh @@ -0,0 +1,90 @@ +#! /usr/bin/env bash +set -e + +config_filepath="./config/analyzer.conf" + +if [ -f "$config_filepath" ]; then + source "$config_filepath" +else + echo "Config file not found: $config_filepath" + exit 1 +fi + +PYTHON_EXECUTABLE="${PYTHON_VENV}/bin/python3" + +check_prerequisites() { + if [[ -z ${LATITUDE} ]]; then + echo "LATITUDE is not set" + exit 1 + fi + if [[ -z ${LONGITUDE} ]]; then + echo "LONGITUDE is not set" + exit 1 + fi + if [[ -z ${CHUNK_FOLDER} ]]; then + echo "CHUNK_FOLDER is not set" + exit 1 + else + if [[ ! -d "${CHUNK_FOLDER}" ]]; then + echo "CHUNK_FOLDER does not exist: ${CHUNK_FOLDER}" + exit 1 + else + if [[ ! -d "${CHUNK_FOLDER}/in" ]]; then + echo "Input dir does not exist: ${CHUNK_FOLDER}/in" + exit 1 + else + if [[ ! -d "${CHUNK_FOLDER}/out" ]]; then + echo "Output dir does not exist: ${CHUNK_FOLDER}/out" + echo "Creating output dir" + mkdir -p "${CHUNK_FOLDER}/out" + fi + fi + fi + fi + fi + if [[ -z ${SPECIES_LIST} ]]; + then + echo "SPECIES_LIST is not set" + exit 1 + fi + if [[ -f $PYTHON_EXECUTABLE ]]; + then + if $verbose; then + echo "Python executable found: $PYTHON_EXECUTABLE" + fi + else + echo "Python executable not found: $PYTHON_EXECUTABLE" + exit 1 + fi +} + +# Get array of audio chunks to be processed +get_chunk_list() { + find "$CHUNK_FOLDER/in" -type f -name '*.wav' -exec basename {} \; ! -size 0 | sort +} + +# Perform audio chunk analysis on one chunk +analyze_chunk() { + chunk_name=$1 + chunk_path="$CHUNK_FOLDER/in/$chunk_name" + output_dir="$CHUNK_FOLDER/out/$chunk_name.d" + mkdir -p "$output_dir" + date=$(echo $chunk_name | cut -d'_' -f2) + week=$(./daemon/weekof.sh $date) + $PYTHON_EXECUTABLE ./analyzer/analyze.py --i $chunk_path --o "$output_dir/model.out.csv" --lat $LATITUDE --lon $LONGITUDE --week $week --min_conf $CONFIDENCE --threads 4 --rtype csv +} + +# Perform audio chunk analysis on all recorded chunks +analyze_chunks() { + for chunk_name in $(get_chunk_list); do + analyze_chunk $chunk_name + chunk_path="$CHUNK_FOLDER/in/$chunk_name" + mv $chunk_path "$CHUNK_FOLDER/out/$chunk_name" + done +} + +# Get list of current chunk in working directory +chunks=$(get_chunk_list) + +# Analyze all chunks in working directory +analyze_chunks $chunks \ No newline at end of file diff --git a/daemon/database/scripts/database.py b/daemon/database/scripts/database.py new file mode 100755 index 0000000..b56ab9e --- /dev/null +++ b/daemon/database/scripts/database.py @@ -0,0 +1,83 @@ +#! /usr/bin/env python3 + +import sqlite3 +import os + +verbose = False + +"""Load config""" +def load_conf(): + with open("./config/analyzer.conf", "r") as f: + conf = f.readlines() + res = dict(map(str.strip, sub.split('=', 1)) for sub in conf if '=' in sub) + return res + +# Singleton database instance +database = None +def get_database(): + global database + if database is None: + database = sqlite3.connect(CONFIG["DATABASE"]) + return database + +"""Create the database if it doesn't exist""" +def create_database(): + # Create the database + database = + + database = get_database() + cursor = database.cursor() + with open("./daemon/database/structure.sql", "r") as f: + cursor.executescript(f.read()) + database.commit() + +"""Insert an observation into the database""" +def insert_observation(observation): + database = get_database() + cursor = database.cursor() + cursor.execute(f"INSERT INTO observation (taxon_id, locality_id, date, time, confidence) VALUES ({observation['taxon_id']}, {observation['locality_id']}, {observation['date']}, {observation['time']}, {observation['confidence']});") + database.commit() + +"""Insert a taxon in database""" +def insert_taxon(taxon): + database = get_database() + cursor = database.cursor() + cursor.execute(f"INSERT INTO taxon (scientific_name, common_name) VALUES ('{taxon['scientific_name']}', '{taxon['common_name']}');") + database.commit() + +"""Insert a location into database""" +def insert_locality(locality): + database = get_database() + cursor = database.cursor() + cursor.execute(f"INSERT INTO locality (name, latitude, longitude) VALUES ('{locality['name']}', {locality['latitude']}, {locality['longitude']});") + database.commit() + +"""Insert all species from list into database""" +def insert_all_species(species): + database = get_database() + cursor = database.cursor() + for sp in species: + # Check if the species already exists in the database + cursor.execute(f"SELECT * FROM taxon WHERE scientific_name = '{sp[0]}';") + # If it doesn't exist, insert it + if cursor.fetchone() is None: + cursor.execute(f"INSERT INTO taxon (scientific_name, common_name) VALUES ('{sp[0]}', '{sp[1]}');") + database.commit() + +CONFIG = load_conf() + +def main(): + # Create the database if it doesn't exist + if not os.path.exists(CONFIG["DATABASE"]): + create_database() + else: + print("Database already exists") + + # Open species list file + with open(CONFIG["SPECIES_LIST"], "r") as f: + species = f.readlines() + species = [sp.split("_") for sp in species] + +if __name__ == "__main__": + main() + database.close() \ No newline at end of file diff --git a/daemon/database/structure.sql b/daemon/database/structure.sql new file mode 100644 index 0000000..fd736e0 --- /dev/null +++ b/daemon/database/structure.sql @@ -0,0 +1,29 @@ +/** Database structure for BirdNET-stream SQLite*/ + +/** Taxon table */ +CREATE TABLE IF NOT EXISTS taxon ( + taxon_id INTEGER PRIMARY KEY, + scientific_name TEXT NOT NULL, + common_name TEXT NOT NULL +); + +/** Locality table */ +CREATE TABLE IF NOT EXISTS locality ( + locality_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + 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, + FOREIGN KEY(taxon_id) REFERENCES taxon(taxon_id), + FOREIGN KEY(locality_id) REFERENCES locality(locality_id) +); \ No newline at end of file diff --git a/daemon/dist/birdnet-stream.service b/daemon/dist/birdnet-stream.service new file mode 100644 index 0000000..e69de29 diff --git a/daemon/miner.sh b/daemon/miner.sh new file mode 100644 index 0000000..997dd16 --- /dev/null +++ b/daemon/miner.sh @@ -0,0 +1,35 @@ +# !/bin/bash + +# Extract data generated with BirdNET on record to get relevant informations and record data in sqlite + +# Load config file +config_filepath="./config/analyzer.conf" +if [ -f "$config_filepath" ]; then + source "$config_filepath" +else + echo "Config file not found: $config_filepath" + exit 1 +fi + +# Verify needed prerequisites +if [[ -z ${CHUNK_FOLDER} ]]; then + echo "CHUNK_FOLDER is not set" + exit 1 +else + if [[ ! -d "${CHUNK_FOLDER}" ]]; then + echo "CHUNK_FOLDER does not exist: ${CHUNK_FOLDER}" + exit 1 + else + if [[ ! -d "${CHUNK_FOLDER}/out" ]]; then + echo "Output dir does not exist: ${CHUNK_FOLDER}/out" + echo "Cannot mine data" + exit 1 + fi + fi + fi +fi + +function list_all_model_outputs() +{ + +} \ No newline at end of file diff --git a/daemon/record-chunks.sh b/daemon/record-chunks.sh new file mode 100755 index 0000000..bfd6e63 --- /dev/null +++ b/daemon/record-chunks.sh @@ -0,0 +1,28 @@ +#! /usr/bin/env bash + +record_chunk() +{ + DEVICE=$1 + DURATION=$2 + ffmpeg -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" + +if [ -f "$config_filepath" ]; then + source "$config_filepath" +else + echo "Config file not found: $config_filepath" + exit 1 +fi + +[ -z $RECORDING_DURATION ] && RECORDING_DURATION=15 + +if [[ -z $AUDIO_DEVICE ]]; then + echo "AUDIO_DEVICE is not set" + exit 1 +fi + +while true; do + record_chunk $AUDIO_DEVICE $RECORDING_DURATION +done \ No newline at end of file diff --git a/daemon/weekof.sh b/daemon/weekof.sh new file mode 100755 index 0000000..e3fb93f --- /dev/null +++ b/daemon/weekof.sh @@ -0,0 +1,9 @@ +#! /usr/bin/env bash + +function weekof() +{ + local date=$1 + date -d "$date" +%W +} + +weekof $1 \ No newline at end of file diff --git a/media/logo.svg b/media/logo.svg new file mode 100644 index 0000000..b3aa02e --- /dev/null +++ b/media/logo.svg @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9bc73ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +appdirs==1.4.4 +audioread==2.1.9 +certifi==2022.6.15 +cffi==1.15.1 +charset-normalizer==2.1.0 +decorator==5.1.1 +idna==3.3 +joblib==1.1.0 +librosa==0.9.2 +llvmlite==0.39.0 +numba==0.56.0 +numpy==1.22.4 +packaging==21.3 +pooch==1.6.0 +pycparser==2.21 +pyparsing==3.0.9 +requests==2.28.1 +resampy==0.3.1 +scikit-learn==1.1.2 +scipy==1.9.0 +SoundFile==0.10.3.post1 +tflite-runtime==2.9.1 +threadpoolctl==3.1.0 +urllib3==1.26.11 diff --git a/utils/purge.sh b/utils/purge.sh new file mode 100755 index 0000000..dcab0ee --- /dev/null +++ b/utils/purge.sh @@ -0,0 +1,24 @@ +#! /usr/bin/env bash + +# Load config file +config_filepath="./config/analyzer.conf" + +if [ -f "$config_filepath" ]; then + source "$config_filepath" +else + echo "Config file not found: $config_filepath" + exit 1 +fi + +# Remove all files from the temporary record directory +function clean_record_dir() +{ + rm -rf "$CHUNK_FOLDER/in/*.wav" +} + +function remove_empty_records() +{ + find $CHUNK_FOLDER/in -maxdepth 1 -name '*wav' -type f -size 0 -delete +} + +remove_empty_records \ No newline at end of file