| """ |
| Photo Verification API — FastAPI app factory. |
| """ |
|
|
| import logging |
| import os |
| import time |
|
|
| from contextlib import asynccontextmanager |
| from fastapi import FastAPI, Request |
| from fastapi.exceptions import RequestValidationError |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import JSONResponse, RedirectResponse |
| from fastapi.staticfiles import StaticFiles |
| from slowapi import _rate_limit_exceeded_handler |
| from slowapi.errors import RateLimitExceeded |
|
|
| from app.core.config import settings |
| from app.core.exceptions import AppError |
| from app.core.logging_config import configure_logging |
| from app.middleware.correlation_id import CorrelationIdMiddleware, get_request_id |
| from app.middleware.rate_limit import limiter |
| from app.middleware.timing import TimingMiddleware |
| from app.metrics.prometheus import setup_metrics |
| from app.routers import verification |
| from app.routers import health as health_router |
| from app.schemas.errors import ErrorResponse, ErrorDetail |
|
|
| |
| os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3") |
| os.environ.setdefault("TF_ENABLE_ONEDNN_OPTS", "0") |
|
|
| configure_logging(settings.LOG_LEVEL, settings.LOG_FORMAT) |
| logger = logging.getLogger(__name__) |
|
|
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| """Load all CV models at startup; drain thread pool on shutdown.""" |
| from app.services.face_analysis import face_analysis_service |
| from app.services.liveness import liveness_service |
| from app.core.dependencies import cv_executor |
|
|
| logger.info("starting up", extra={"version": settings.VERSION}) |
| face_analysis_service.load() |
| liveness_service.load() |
| logger.info("all models loaded — API ready") |
|
|
| yield |
|
|
| logger.info("shutting down — draining CV thread pool") |
| cv_executor.shutdown(wait=True, cancel_futures=False) |
| logger.info("shutdown complete") |
|
|
|
|
| def _make_error_response(status_code: int, error_code: str, message: str, context: dict = {}) -> JSONResponse: |
| return JSONResponse( |
| status_code=status_code, |
| content=ErrorResponse( |
| error=ErrorDetail( |
| error_code=error_code, |
| message=message, |
| request_id=get_request_id(), |
| context=context, |
| ) |
| ).model_dump(), |
| ) |
|
|
|
|
| def create_app() -> FastAPI: |
| app = FastAPI( |
| title=settings.APP_NAME, |
| version=settings.VERSION, |
| description=""" |
| ## Photo Verification API |
| |
| Simulates the pose-challenge verification system used in apps like Bumble/Tinder. |
| |
| ### Pipeline |
| 1. **Liveness detection** — MiniFASNet (Silent-Face-Anti-Spoofing) via DeepFace |
| 2. **Face landmark extraction** — MediaPipe FaceLandmarker (478 3D points) |
| 3. **Head pose estimation** — PnP solver (yaw/pitch/roll in degrees) |
| 4. **Challenge matching** — threshold-based pose verification |
| |
| ### No training needed — all models are pretrained. |
| """, |
| contact={"name": "API Support"}, |
| openapi_tags=[ |
| {"name": "Verification v1", "description": "Pose challenge verification endpoints"}, |
| {"name": "Health", "description": "Service health and readiness"}, |
| ], |
| docs_url="/docs" if settings.DEBUG else "/docs", |
| redoc_url="/redoc" if settings.DEBUG else "/redoc", |
| lifespan=lifespan, |
| ) |
|
|
| |
| app.state.limiter = limiter |
|
|
| |
| app.add_middleware(CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_methods=["*"], allow_headers=["*"]) |
| app.add_middleware(TimingMiddleware) |
| app.add_middleware(CorrelationIdMiddleware) |
|
|
| |
| @app.exception_handler(AppError) |
| async def app_error_handler(request: Request, exc: AppError): |
| return _make_error_response(exc.status_code, exc.error_code, exc.message, exc.context) |
|
|
| @app.exception_handler(RequestValidationError) |
| async def validation_error_handler(request: Request, exc: RequestValidationError): |
| return _make_error_response( |
| 422, |
| "VALIDATION_ERROR", |
| "Request validation failed.", |
| context={"errors": exc.errors()}, |
| ) |
|
|
| @app.exception_handler(RateLimitExceeded) |
| async def rate_limit_handler(request: Request, exc: RateLimitExceeded): |
| return _make_error_response(429, "RATE_LIMIT_EXCEEDED", "Too many requests. Please slow down.") |
|
|
| @app.exception_handler(Exception) |
| async def unhandled_error_handler(request: Request, exc: Exception): |
| logger.exception("unhandled exception", extra={"path": request.url.path}) |
| return _make_error_response(500, "INTERNAL_ERROR", "An unexpected error occurred.") |
|
|
| |
| app.include_router(health_router.router, prefix="/api/v1") |
| app.include_router(verification.router, prefix="/api/v1") |
|
|
| |
| setup_metrics(app) |
|
|
| |
| import os |
| static_dir = os.path.join(os.path.dirname(__file__), "static") |
| if os.path.isdir(static_dir): |
| app.mount("/", StaticFiles(directory=static_dir, html=True), name="static") |
|
|
| return app |
|
|
|
|
| app = create_app() |
|
|