FaceCheck / app /main.py
Mukhammadali Bakhodirov
Deploy detection fixes
9b5157d
"""
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
# Silence TensorFlow noise before any TF import
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", # keep docs accessible for demo
redoc_url="/redoc" if settings.DEBUG else "/redoc",
lifespan=lifespan,
)
# --- Rate limiter state ---
app.state.limiter = limiter
# --- Middleware (last added = outermost on request) ---
app.add_middleware(CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_methods=["*"], allow_headers=["*"])
app.add_middleware(TimingMiddleware)
app.add_middleware(CorrelationIdMiddleware)
# --- Exception handlers ---
@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.")
# --- Routers ---
app.include_router(health_router.router, prefix="/api/v1")
app.include_router(verification.router, prefix="/api/v1")
# --- Prometheus metrics ---
setup_metrics(app)
# --- Static UI (must be mounted last so API routes take priority) ---
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()