SS_BirdNet / main.py
TheAlly
Default top_n to None, strip metadata row, post-filter by min_conf
be6595e
Raw
History Blame Contribute Delete
2.76 kB
import shutil
import tempfile
import os
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import JSONResponse
import birdnet_analyzer
from birdnet_analyzer import analyze
app = FastAPI(title="SS BirdNet API")
@app.get("/")
def health():
return {
"status": "ok",
"service": "birdnet-analyzer",
"version": birdnet_analyzer.__version__,
}
# Keys we expect on a real detection row — used to filter out the
# metadata block BirdNET appends to its CSV output ("File splitting
# duration", "Segment length", etc., which DictReader otherwise picks
# up as a phantom row).
DETECTION_KEYS = {"Start (s)", "End (s)", "Scientific name", "Common name", "Confidence"}
@app.post("/analyze")
async def analyze_audio(
audio: UploadFile = File(...),
min_conf: float = Form(0.25),
lat: Optional[float] = Form(None),
lon: Optional[float] = Form(None),
week: Optional[int] = Form(None),
# None lets min_conf gate the results. Pass an int to force "top N per
# segment regardless of confidence" — useful for debugging silent files,
# noisy for normal use.
top_n: Optional[int] = Form(None),
):
suffix = Path(audio.filename or "audio.wav").suffix or ".wav"
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp_in:
tmp_in.write(await audio.read())
in_path = tmp_in.name
out_dir = tempfile.mkdtemp()
try:
analyze(
audio_input=in_path,
output=out_dir,
min_conf=min_conf,
lat=lat,
lon=lon,
week=week,
top_n=top_n,
rtype="csv",
)
import csv as csv_module
results = []
for csv_file in Path(out_dir).glob("*.csv"):
with open(csv_file, newline="") as f:
for row in csv_module.DictReader(f):
# Skip the trailing metadata block — its keys don't
# overlap with detection keys.
if not DETECTION_KEYS.issubset(row.keys()):
continue
# Defence-in-depth: drop rows below threshold even
# when top_n is set, so the response is consistent.
try:
if float(row.get("Confidence", 0)) < min_conf:
continue
except (TypeError, ValueError):
continue
results.append(dict(row))
return JSONResponse({"results": results})
finally:
try:
os.unlink(in_path)
except OSError:
pass
shutil.rmtree(out_dir, ignore_errors=True)