Spaces:
Sleeping
Sleeping
| 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") | |
| 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"} | |
| 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) | |