"""Prometheus API — FastAPI service backing the React dashboard. Run from the project root: uvicorn api.main:app --reload --port 8000 Design notes: * Detection reuses the existing pipeline (src.detection.detector via api.services.detection_service) — one source of truth for inference. * Heavy deps (torch/ultralytics) are imported lazily inside the detection service, so /api/health, /api/metrics and the catalog endpoints respond instantly even on a machine without a model loaded. """ from __future__ import annotations import os # Anaconda + torch + OpenCV each ship their own OpenMP runtime (libiomp5md.dll); # loading them together (as the video job does) collides and crashes the whole # process natively — uncatchable, the server just dies mid-job. Allowing the # duplicate runtime is the standard pragmatic fix for inference. MUST be set # before torch/cv2/numpy are imported anywhere, so it lives at the very top. os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE") import sys # noqa: E402 from pathlib import Path # noqa: E402 from fastapi import FastAPI # noqa: E402 from fastapi.middleware.cors import CORSMiddleware # noqa: E402 PROJECT_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT_ROOT)) from api.routes import catalog, detection, population, survey, telemetry # noqa: E402 from api.schemas import SystemInfo # noqa: E402 from api.services import detection_service # noqa: E402 app = FastAPI( title="Prometheus API", description="Aerial wildlife intelligence for the Malilangwe Trust.", version="0.1.0", ) # CORS origins are configurable via the ALLOWED_ORIGINS env var (comma-separated) # so production can lock the API to just the dashboard's domain WITHOUT a code # change — set it at deploy time. Defaults to "*" for local dev. # e.g. ALLOWED_ORIGINS=https://prometheus.pages.dev,https://app.example.com _origins_env = os.environ.get("ALLOWED_ORIGINS", "*").strip() _allow_origins = ["*"] if _origins_env in ("", "*") else [ o.strip() for o in _origins_env.split(",") if o.strip() ] app.add_middleware( CORSMiddleware, allow_origins=_allow_origins, allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) app.include_router(detection.router) app.include_router(catalog.router) app.include_router(population.router) app.include_router(survey.router) app.include_router(telemetry.router) @app.get("/api/health", response_model=SystemInfo, tags=["system"]) def health() -> SystemInfo: cuda = False device = "cpu" try: import torch cuda = bool(torch.cuda.is_available()) device = "cuda" if cuda else "cpu" except Exception: # noqa: BLE001 — torch absent is a valid (CPU-only) state pass return SystemInfo( status="ok", version="0.1.0", device=device, torch_cuda=cuda, models_available=len(detection_service.available_models()), project="Prometheus · Malilangwe Trust", ) # Single-app deploy: if a built dashboard is present in web/, serve it from this # same server so UI + API + weights live at one origin (one private host). The # dashboard build uses a relative API base, so its /api/* calls are same-origin # and carry the host's auth. Mounted LAST so the /api/* routes above win. # Locally without web/, this is skipped and the server is API-only. _WEB = PROJECT_ROOT / "web" if (_WEB / "index.html").exists(): from fastapi.staticfiles import StaticFiles # noqa: E402 app.mount("/", StaticFiles(directory=str(_WEB), html=True), name="web")