Reunite / main.py
Shoraky's picture
Complete backend folder
7e710af
Raw
History Blame Contribute Delete
9.49 kB
"""
main.py β€” FastAPI REST API for Reunite
───────────────────────────────────────────────────────────────────────────────
Start: uvicorn main:app --reload --host 0.0.0.0 --port 8000
Docs: http://localhost:8000/docs
Route map:
GET /api/health β€” liveness probe + registry count
GET /api/stats β€” totals: registered / missing / found
POST /api/register β€” register a new missing person
POST /api/search β€” face-similarity search
GET /api/persons β€” list all registered persons
GET /api/persons/{id}/image β€” serve the stored photo
PATCH /api/persons/{id}/status β€” mark as missing / found
DELETE /api/persons/{id} β€” remove from registry
/ β†’ served from ../frontend/ (StaticFiles, html=True)
───────────────────────────────────────────────────────────────────────────────
"""
from __future__ import annotations
from pathlib import Path
import logging
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import core
logger = logging.getLogger("uvicorn.error")
# ─────────────────────────────────────────────────────────────────────────────
app = FastAPI(
title="Reunite β€” Missing Persons AI",
version="2.0.0",
docs_url="/docs",
redoc_url="/redoc",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
PERSONS_DIR = Path(__file__).parent / "Storage" / "persons"
_frontend_root = Path(__file__).parent.parent
FRONTEND_DIR = (
_frontend_root / "Frontend"
if (_frontend_root / "Frontend").exists()
else (_frontend_root / "frontend")
)
# ─────────────────────────────────────────────────────────────────────────────
@app.on_event("startup")
async def on_startup():
core.ensure_dirs()
# Warm up model on CPU so first request is faster (weights may still download).
try:
core.get_face_app()
logger.info("ArcFace model initialized (CPU).")
except Exception as e:
logger.exception("ArcFace model warmup failed: %s", e)
# ─────────────────────────────────────────────────────────────────────────────
# Health & Stats
# ─────────────────────────────────────────────────────────────────────────────
@app.get("/api/health", tags=["meta"])
async def health():
persons = core.get_all_persons()
return {"status": "ok", "registered": len(persons)}
@app.get("/api/stats", tags=["meta"])
async def stats():
persons = core.get_all_persons()
total = len(persons)
found = sum(1 for p in persons if p.get("status") == "found")
return {
"total": total,
"missing": total - found,
"found": found,
}
# ─────────────────────────────────────────────────────────────────────────────
# Register a missing person
# ─────────────────────────────────────────────────────────────────────────────
@app.post("/api/register", tags=["persons"])
async def register(
image: UploadFile = File(..., description="Face photo (JPG / PNG / WEBP)"),
name: str = Form(..., description="Full name"),
phone_contact: str = Form(..., description="Contact phone number"),
age: str = Form("", description="Approximate age"),
gender: str = Form("", description="male | female | other"),
last_seen_date: str = Form("", description="YYYY-MM-DD"),
last_seen_location: str = Form("", description="City / area last seen"),
address: str = Form("", description="Home address"),
national_id: str = Form("", description="National ID (optional)"),
description: str = Form("", description="Additional notes"),
):
image_bytes = await image.read()
result = core.register_missing_person(
image_bytes,
{
"name": name,
"age": age,
"gender": gender,
"last_seen_date": last_seen_date,
"last_seen_location": last_seen_location,
"phone_contact": phone_contact,
"address": address,
"national_id": national_id,
"description": description,
},
)
if not result["success"]:
raise HTTPException(status_code=422, detail=result["error"])
timing = result.get("timing_ms")
if isinstance(timing, dict):
logger.info(
"Registered person %s | embedding=%sms io=%sms total=%sms",
result.get("id"),
timing.get("embedding_ms"),
timing.get("io_ms"),
timing.get("total_ms"),
)
return result
# ─────────────────────────────────────────────────────────────────────────────
# Search by face image
# ─────────────────────────────────────────────────────────────────────────────
@app.post("/api/search", tags=["search"])
async def search(
image: UploadFile = File(..., description="Query face photo"),
top_k: int = Form(5, description="Max candidates to return"),
threshold: float = Form(0.38, description="Minimum cosine similarity"),
):
image_bytes = await image.read()
result = core.search_person(image_bytes, top_k=top_k, threshold=threshold)
if not result["success"]:
raise HTTPException(status_code=422, detail=result["error"])
return result
# ─────────────────────────────────────────────────────────────────────────────
# Directory
# ─────────────────────────────────────────────────────────────────────────────
@app.get("/api/persons", tags=["persons"])
async def list_persons():
return {"persons": core.get_all_persons()}
@app.get("/api/persons/{pid}/image", tags=["persons"])
async def person_image(pid: str):
path = PERSONS_DIR / pid / "photo.jpg"
if not path.exists():
raise HTTPException(status_code=404, detail="Image not found.")
return FileResponse(str(path), media_type="image/jpeg")
@app.patch("/api/persons/{pid}/status", tags=["persons"])
async def update_status(pid: str, status: str = Form(...)):
if status not in ("missing", "found"):
raise HTTPException(status_code=400, detail="status must be 'missing' or 'found'.")
result = core.update_person_status(pid, status)
if not result["success"]:
raise HTTPException(status_code=404, detail=result["error"])
return result
@app.delete("/api/persons/{pid}", tags=["persons"])
async def delete_person(pid: str):
result = core.delete_person(pid)
if not result["success"]:
raise HTTPException(status_code=404, detail=result["error"])
return result
# ─────────────────────────────────────────────────────────────────────────────
# Frontend β€” must be mounted LAST so API routes take priority
# ─────────────────────────────────────────────────────────────────────────────
if FRONTEND_DIR.exists():
app.mount(
"/",
StaticFiles(directory=str(FRONTEND_DIR), html=True),
name="frontend",
)