| """ |
| 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() |
| |
| try: |
| core.get_face_app() |
| logger.info("ArcFace model initialized (CPU).") |
| except Exception as e: |
| logger.exception("ArcFace model warmup failed: %s", e) |
|
|
|
|
| |
| |
| |
|
|
| @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, |
| } |
|
|
|
|
| |
| |
| |
|
|
| @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 |
|
|
|
|
| |
| |
| |
|
|
| @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 |
|
|
|
|
| |
| |
| |
|
|
| @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 |
|
|
|
|
| |
| |
| |
| if FRONTEND_DIR.exists(): |
| app.mount( |
| "/", |
| StaticFiles(directory=str(FRONTEND_DIR), html=True), |
| name="frontend", |
| ) |
|
|