Spaces:
Sleeping
Sleeping
| 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") | |
| ) | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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() | |
| ] | |
| 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) |