aigis-api / src /api.py
omarkashif's picture
Adopt corrected detector.py, sh17.yaml, and vlm_caption.py from AiGis-AI repo
f4d376a
Raw History Blame Contribute Delete
5.59 kB
"""
FastAPI service wrapping the AiGiS pipeline for use from a website or another app.
POST /caption (multipart file upload: "image")
-> runs YOLO (detector.py) + a VLM (vlm_caption.py) on the image
-> returns JSON: detections, compliance findings, VLM caption/observation,
and the annotated image (boxes drawn) as base64 JPEG
Query params:
skip_vlm (bool) detector only, no VLM call (no API cost)
skip_detector (bool) VLM only — image judged without YOLO detections
model (str) registry key from vlm_caption.MODEL_REGISTRY
("sonnet", "opus", "haiku", "qwen-8b", "qwen-30b",
"qwen-32b") or a raw model id. Defaults to "sonnet".
The YOLO weights are pulled once at startup from the Hugging Face model repo
Run:
cd src
uvicorn api:app --reload --host 0.0.0.0 --port 8000
"""
from __future__ import annotations
import base64
import mimetypes
import os
from contextlib import asynccontextmanager
from pathlib import Path
import cv2
import numpy as np
from dotenv import load_dotenv
from fastapi import FastAPI, File, Header, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from huggingface_hub import HfApi, hf_hub_download
import vlm_caption
from detector import SafetyDetector
REPO_ROOT = Path(__file__).resolve().parent.parent
load_dotenv(REPO_ROOT / ".env")
HF_REPO_ID = os.environ.get("AIGIS_HF_REPO_ID", "ZahraFatima/aigis-yolo")
HF_FILENAME = os.environ.get("AIGIS_HF_FILENAME", "best.pt")
DATA_YAML = os.environ.get("AIGIS_DATA_YAML", str(REPO_ROOT / "config" / "sh17.yaml"))
CONF = float(os.environ.get("AIGIS_CONF", "0.25"))
# For the /admin/restart endpoint only (this Space, not the local/AWS deployment):
# HF_TOKEN needs "write" access to THIS_SPACE_ID, and RESTART_SECRET is a shared
# password the BE team uses instead of needing their own HF account.
HF_TOKEN = os.environ.get("HF_TOKEN")
THIS_SPACE_ID = os.environ.get("SPACE_ID", "DarkthornLabs1/aigis-api")
RESTART_SECRET = os.environ.get("RESTART_SECRET")
# Which env var each VLM provider needs, so a missing key gives a clean 500
# instead of a raw SDK exception.
_PROVIDER_KEY = {"anthropic": "ANTHROPIC_API_KEY", "dashscope": "DASHSCOPE_API_KEY"}
detector: SafetyDetector | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global detector
weights_path = hf_hub_download(repo_id=HF_REPO_ID, filename=HF_FILENAME)
detector = SafetyDetector(weights_path, DATA_YAML, conf=CONF)
yield
app = FastAPI(title="AiGiS Safety Captioning API", version="0.1.0", lifespan=lifespan)
# Permissive by default so any site/MVP can call this during development.
# For production, set AIGIS_CORS_ORIGINS to a comma-separated allowlist.
app.add_middleware(
CORSMiddleware,
allow_origins=os.environ.get("AIGIS_CORS_ORIGINS", "*").split(","),
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
def health():
return {"status": "ok", "model_loaded": detector is not None}
@app.post("/admin/restart")
def restart(x_restart_token: str = Header(default="")):
if not RESTART_SECRET or x_restart_token != RESTART_SECRET:
raise HTTPException(status_code=403, detail="Invalid or missing X-Restart-Token header")
if not HF_TOKEN:
raise HTTPException(status_code=500, detail="HF_TOKEN not configured on server")
try:
HfApi(token=HF_TOKEN).restart_space(repo_id=THIS_SPACE_ID)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Restart request failed: {exc}") from exc
return {"status": "restart triggered"}
@app.post("/caption")
async def caption_image(
image: UploadFile = File(...),
skip_vlm: bool = False,
skip_detector: bool = False,
model: str = vlm_caption.DEFAULT_MODEL,
):
if detector is None:
raise HTTPException(status_code=503, detail="Model not loaded yet")
raw = await image.read()
if not raw:
raise HTTPException(status_code=400, detail="Empty file upload")
frame = cv2.imdecode(np.frombuffer(raw, dtype=np.uint8), cv2.IMREAD_COLOR)
if frame is None:
raise HTTPException(status_code=400, detail="Could not decode image")
record: dict = {}
det_dict: dict | None = None
annotated = frame
if skip_detector:
record["detector"] = {"note": "detector skipped (skip_detector=true)"}
else:
det_result, annotated = detector.detect_and_annotate(frame, name=image.filename or "upload.jpg")
det_dict = det_result.to_dict()
record["detector"] = det_dict
if not skip_vlm:
_, provider, _ = vlm_caption._resolve(model)
needed_key = _PROVIDER_KEY.get(provider)
if needed_key and needed_key not in os.environ:
raise HTTPException(
status_code=500,
detail=f"{needed_key} not set on server (required for model '{model}')",
)
media_type = image.content_type or mimetypes.guess_type(image.filename or "")[0] or "image/jpeg"
record["vlm"] = vlm_caption.caption_bytes(
raw, media_type, detection_result=det_dict, model=model, use_detector=not skip_detector
)
ok, buf = cv2.imencode(".jpg", annotated)
if not ok:
raise HTTPException(status_code=500, detail="Failed to encode annotated image")
record["annotated_image_base64"] = base64.b64encode(buf.tobytes()).decode()
return record
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)