import sys, os # ── Path fix: allow "python -m uvicorn" from project root ────────────────── sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) from fastapi import FastAPI, UploadFile, File, HTTPException from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager from typing import List from backend.model import load_model, is_model_loaded, predict_image from backend.schemas import ( PredictResponse, PredictionResult, DiseaseListItem, DiseaseDetail, HealthResponse, ) # Load disease info directly by path (avoids package import issues) import importlib.util _spec = importlib.util.spec_from_file_location( "disease_info", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "data", "disease_info.py")) ) _mod = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(_mod) DISEASE_INFO = _mod.DISEASE_INFO ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp", "image/bmp"} MODEL_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "..", "skin_disease_classifier.pkl") ) @asynccontextmanager async def lifespan(app: FastAPI): result = load_model(MODEL_PATH) print(f"STARTUP: load_model returned {result}", flush=True) print(f"STARTUP: model_loaded={is_model_loaded()}", flush=True) yield app = FastAPI( title="DermAI — Skin Disease Classifier API", description="FastAPI backend powering the DermAI skin disease diagnosis web app.", version="1.0.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # ── Health check ──────────────────────────────────────────────────────────── @app.get("/health", response_model=HealthResponse, tags=["Utility"]) async def health_check(): return HealthResponse(status="ok", model_loaded=is_model_loaded()) # ── Prediction ────────────────────────────────────────────────────────────── import asyncio from concurrent.futures import ThreadPoolExecutor _executor = ThreadPoolExecutor(max_workers=1) @app.post("/predict", response_model=PredictResponse, tags=["Inference"]) async def predict(file: UploadFile = File(...)): if file.content_type not in ALLOWED_TYPES: raise HTTPException(status_code=415, detail=f"Unsupported format '{file.content_type}'.") raw = await file.read() if len(raw) > 15 * 1024 * 1024: raise HTTPException(status_code=413, detail="Image too large. Maximum 15 MB.") loop = asyncio.get_event_loop() try: top3 = await loop.run_in_executor(_executor, predict_image, raw) except Exception as exc: raise HTTPException(status_code=500, detail=f"Inference error: {str(exc)}") if top3 is None: raise HTTPException(status_code=500, detail="Prediction failed.") results = [ PredictionResult( label=p["medical_name"], friendly_name=p["friendly_name"], confidence=p["confidence"], ) for p in top3 ] return PredictResponse(top3=results) # ── Disease encyclopedia ──────────────────────────────────────────────────── @app.get("/diseases", response_model=List[DiseaseListItem], tags=["Encyclopedia"]) async def list_diseases(): """Summary info for all 21 classifiable skin conditions.""" return [ DiseaseListItem( key=key, friendly_name=info["friendly_name"], category=info["category"], severity=info["severity"], emoji=info["emoji"], ) for key, info in DISEASE_INFO.items() ] @app.get("/disease/{disease_key}", response_model=DiseaseDetail, tags=["Encyclopedia"]) async def get_disease(disease_key: str): """Full detail for a single disease by its key name.""" info = DISEASE_INFO.get(disease_key) if info is None: for k, v in DISEASE_INFO.items(): if k.lower() == disease_key.lower(): disease_key, info = k, v break if info is None: raise HTTPException(status_code=404, detail=f"Disease '{disease_key}' not found.") return DiseaseDetail(key=disease_key, **info)