Spaces:
Running
Running
| from __future__ import annotations | |
| import logging | |
| from contextlib import asynccontextmanager | |
| from typing import AsyncIterator | |
| from fastapi import FastAPI, Request, status | |
| from fastapi.exceptions import RequestValidationError | |
| from fastapi.responses import JSONResponse | |
| from app.core.config import get_settings | |
| from app.core.model import build_transform, load_model | |
| from app.routes.predict import router as predict_router | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s %(levelname)-8s %(name)s %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| async def lifespan(app: FastAPI) -> AsyncIterator[None]: | |
| settings = get_settings() | |
| logger.info("Loading model from %s …", settings.checkpoint_path) | |
| model, meta = load_model( | |
| checkpoint_path=settings.checkpoint_path, | |
| num_classes=settings.num_classes, | |
| device=settings.device, | |
| hf_repo_id=settings.hf_repo_id, | |
| hf_filename=settings.hf_filename, | |
| ) | |
| logger.info( | |
| "Model ready — epoch=%d %s=%.4f device=%s", | |
| meta["epoch"], | |
| meta["metric"], | |
| meta["value"], | |
| settings.device, | |
| ) | |
| app.state.model = model | |
| app.state.transform = build_transform(settings.image_size) | |
| app.state.checkpoint_meta = meta | |
| app.state.settings = settings | |
| app.state.device = settings.device | |
| yield | |
| del app.state.model | |
| logger.info("Model unloaded.") | |
| app = FastAPI( | |
| title="DINOv2 Classifier API", | |
| description=( | |
| "Dual-stream DINOv2 ViT-B/14 image classifier. " | |
| "POST an image to **/predict** to get class probabilities." | |
| ), | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| docs_url="/docs", | |
| redoc_url="/redoc", | |
| ) | |
| app.include_router(predict_router, prefix="/api/v1", tags=["inference"]) | |
| async def root() -> dict: | |
| """Hugging Face and browsers hit `/` by default; without this route they would see 404.""" | |
| return { | |
| "service": "VCE capsule endoscopy classifier API", | |
| "status": "running", | |
| "docs": "/docs", | |
| "health": "/health", | |
| "predict": "POST /api/v1/predict (multipart image upload)", | |
| } | |
| async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: | |
| return JSONResponse( | |
| status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, | |
| content={"detail": exc.errors()}, | |
| ) | |
| async def health() -> dict: | |
| return {"status": "ok"} | |
| async def ready(request: Request) -> dict: | |
| if not hasattr(request.app.state, "model"): | |
| return JSONResponse( | |
| status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | |
| content={"status": "model not loaded"}, | |
| ) | |
| return {"status": "ready"} | |