File size: 3,617 Bytes
58aefd4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"""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")