160 lines
5.8 KiB
Python
160 lines
5.8 KiB
Python
import datetime
|
|
import glob
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from collections import OrderedDict
|
|
from configparser import ConfigParser
|
|
from itertools import chain
|
|
|
|
from tzlocal import get_localzone
|
|
|
|
_settings = None
|
|
|
|
DB_PATH = os.path.expanduser('~/BirdNET-Pi/scripts/birds.db')
|
|
MODEL_PATH = os.path.expanduser('~/BirdNET-Pi/model')
|
|
ANALYZING_NOW = os.path.expanduser('~/BirdSongs/StreamData/analyzing_now.txt')
|
|
FONT_DIR = os.path.expanduser('~/BirdNET-Pi/homepage/static')
|
|
|
|
|
|
def get_font():
|
|
conf = get_settings()
|
|
if conf['DATABASE_LANG'] == 'ar':
|
|
ret = {'font.family': 'Noto Sans Arabic', 'path': os.path.join(FONT_DIR, 'NotoSansArabic-Regular.ttf')}
|
|
elif conf['DATABASE_LANG'] in ['ja', 'zh']:
|
|
ret = {'font.family': 'Noto Sans JP', 'path': os.path.join(FONT_DIR, 'NotoSansJP-Regular.ttf')}
|
|
elif conf['DATABASE_LANG'] == 'ko':
|
|
ret = {'font.family': 'Noto Sans KR', 'path': os.path.join(FONT_DIR, 'NotoSansKR-Regular.ttf')}
|
|
elif conf['DATABASE_LANG'] == 'th':
|
|
ret = {'font.family': 'Noto Sans Thai', 'path': os.path.join(FONT_DIR, 'NotoSansThai-Regular.ttf')}
|
|
else:
|
|
ret = {'font.family': 'Roboto Flex', 'path': os.path.join(FONT_DIR, 'RobotoFlex-Regular.ttf')}
|
|
return ret
|
|
|
|
|
|
class PHPConfigParser(ConfigParser):
|
|
def get(self, section, option, *, raw=False, vars=None, fallback=None):
|
|
value = super().get(section, option, raw=raw, vars=vars, fallback=fallback)
|
|
if raw:
|
|
return value
|
|
else:
|
|
return value.strip('"')
|
|
|
|
|
|
def _load_settings(settings_path='/etc/birdnet/birdnet.conf', force_reload=False):
|
|
global _settings
|
|
if _settings is None or force_reload:
|
|
with open(settings_path) as f:
|
|
parser = PHPConfigParser(interpolation=None)
|
|
# preserve case
|
|
parser.optionxform = lambda option: option
|
|
lines = chain(("[top]",), f)
|
|
parser.read_file(lines)
|
|
_settings = parser['top']
|
|
return _settings
|
|
|
|
|
|
def get_settings(settings_path='/etc/birdnet/birdnet.conf', force_reload=False):
|
|
settings = _load_settings(settings_path, force_reload)
|
|
return settings
|
|
|
|
|
|
class Detection:
|
|
def __init__(self, file_date, start_time, stop_time, species, confidence):
|
|
self.start = float(start_time)
|
|
self.stop = float(stop_time)
|
|
self.datetime = file_date + datetime.timedelta(seconds=self.start)
|
|
self.date = self.datetime.strftime("%Y-%m-%d")
|
|
self.time = self.datetime.strftime("%H:%M:%S")
|
|
self.iso8601 = self.datetime.astimezone(get_localzone()).isoformat()
|
|
self.week = self.datetime.isocalendar()[1]
|
|
self.confidence = round(float(confidence), 4)
|
|
self.confidence_pct = round(self.confidence * 100)
|
|
self.species = species
|
|
self.scientific_name = species.split('_')[0]
|
|
self.common_name = species.split('_')[1]
|
|
self.common_name_safe = self.common_name.replace("'", "").replace(" ", "_")
|
|
self.file_name_extr = None
|
|
|
|
|
|
class ParseFileName:
|
|
def __init__(self, file_name):
|
|
self.file_name = file_name
|
|
name = os.path.splitext(os.path.basename(file_name))[0]
|
|
date_created = re.search('^[0-9]+-[0-9]+-[0-9]+', name).group()
|
|
time_created = re.search('[0-9]+:[0-9]+:[0-9]+$', name).group()
|
|
self.file_date = datetime.datetime.strptime(f'{date_created}T{time_created}', "%Y-%m-%dT%H:%M:%S")
|
|
self.root = name
|
|
|
|
ident_match = re.search("RTSP_[0-9]+-", file_name)
|
|
self.RTSP_id = ident_match.group() if ident_match is not None else ""
|
|
|
|
@property
|
|
def iso8601(self):
|
|
current_iso8601 = self.file_date.astimezone(get_localzone()).isoformat()
|
|
return current_iso8601
|
|
|
|
@property
|
|
def week(self):
|
|
week = self.file_date.isocalendar()[1]
|
|
return week
|
|
|
|
|
|
def get_open_files_in_dir(dir_name):
|
|
result = subprocess.run(['lsof', '-w', '-Fn', '+D', f'{dir_name}'], check=False, capture_output=True)
|
|
ret = result.stdout.decode('utf-8')
|
|
err = result.stderr.decode('utf-8')
|
|
if err:
|
|
raise RuntimeError(f'{ret}:\n {err}')
|
|
names = [line.lstrip('n') for line in ret.splitlines() if line.startswith('n')]
|
|
return names
|
|
|
|
|
|
def get_wav_files():
|
|
conf = get_settings()
|
|
files = (glob.glob(os.path.join(conf['RECS_DIR'], '*/*/*.wav')) +
|
|
glob.glob(os.path.join(conf['RECS_DIR'], 'StreamData/*.wav')))
|
|
files.sort()
|
|
files = [os.path.join(conf['RECS_DIR'], file) for file in files]
|
|
rec_dir = os.path.join(conf['RECS_DIR'], 'StreamData')
|
|
open_recs = get_open_files_in_dir(rec_dir)
|
|
files = [file for file in files if file not in open_recs]
|
|
return files
|
|
|
|
|
|
def get_language(language=None):
|
|
if language is None:
|
|
language = get_settings()['DATABASE_LANG']
|
|
file_name = os.path.join(MODEL_PATH, f'l18n/labels_{language}.json')
|
|
with open(file_name) as f:
|
|
ret = json.loads(f.read())
|
|
return ret
|
|
|
|
|
|
def save_language(labels, language):
|
|
file_name = os.path.join(MODEL_PATH, f'l18n/labels_{language}.json')
|
|
with open(file_name, 'w') as f:
|
|
f.write(json.dumps(OrderedDict(sorted(labels.items())), indent=2, ensure_ascii=False))
|
|
|
|
|
|
def get_model_labels(model=None):
|
|
if model is None:
|
|
model = get_settings()['MODEL']
|
|
file_name = os.path.join(MODEL_PATH, f'{model}_Labels.txt')
|
|
with open(file_name) as f:
|
|
labels = [line.strip() for line in f.readlines()]
|
|
if labels and labels[0].count('_') == 1:
|
|
labels = [re.sub(r'_.+$', '', label) for label in labels]
|
|
return labels
|
|
|
|
|
|
def set_label_file():
|
|
lang = get_language()
|
|
labels = [f'{label}_{lang[label]}\n' for label in get_model_labels()]
|
|
file_name = os.path.join(MODEL_PATH, 'labels.txt')
|
|
if os.path.islink(file_name):
|
|
os.remove(file_name)
|
|
with open(file_name, 'w') as f:
|
|
f.writelines(labels)
|