CoolFace
Apppublic

Shahd1sayed/heart-attack-risk-predictor

sourceHugging Faceupdated 3mo agoView on Hugging Face
1likes
app.py135 linesDownload Raw Back to root
1"""Multimodal heart-attack risk app — ensemble router.2 3One ``POST /predict`` endpoint accepts multipart/form-data carrying *optional*4Framingham tabular fields and an *optional* ECG image.  An internal router picks5the path:6 7    tabular only   -> Model A (Framingham CHD)8    ECG only       -> Model B (ResNet ECG)9    both           -> A + B, averaged10    neither        -> HTTP 40011 12The predictor modules are imported lazily so the app still boots (and serves the13frontend) before the models have been trained.14 15Run:  uvicorn app:app --reload16"""17from __future__ import annotations18 19from pathlib import Path20from typing import Optional21 22from fastapi import FastAPI, File, Form, HTTPException, UploadFile23from fastapi.concurrency import run_in_threadpool24from fastapi.responses import FileResponse25from fastapi.staticfiles import StaticFiles26 27from inference.fusion import band_for, combine28from inference.validation import validate_tabular29 30# Canonical Framingham feature order (mirrors train_framingham.FEATURES).31FEATURES = [32    "male", "age", "education", "currentSmoker", "cigsPerDay", "BPMeds",33    "prevalentStroke", "prevalentHyp", "diabetes", "totChol", "sysBP",34    "diaBP", "BMI", "heartRate", "glucose",35]36 37app = FastAPI(title="Multimodal Heart Attack Risk — Ensemble Router")38 39 40def _has_value(v: Optional[str]) -> bool:41    return v is not None and str(v).strip() != ""42 43 44@app.post("/predict")45async def predict(46    # ── Tabular branch (all optional → blanks are KNN-imputed) ───────────────47    male: Optional[str] = Form(None),48    age: Optional[str] = Form(None),49    education: Optional[str] = Form(None),50    currentSmoker: Optional[str] = Form(None),51    cigsPerDay: Optional[str] = Form(None),52    BPMeds: Optional[str] = Form(None),53    prevalentStroke: Optional[str] = Form(None),54    prevalentHyp: Optional[str] = Form(None),55    diabetes: Optional[str] = Form(None),56    totChol: Optional[str] = Form(None),57    sysBP: Optional[str] = Form(None),58    diaBP: Optional[str] = Form(None),59    BMI: Optional[str] = Form(None),60    heartRate: Optional[str] = Form(None),61    glucose: Optional[str] = Form(None),62    # ── Image branch (optional) ──────────────────────────────────────────────63    ecg: Optional[UploadFile] = File(None),64):65    fields = {66        "male": male, "age": age, "education": education,67        "currentSmoker": currentSmoker, "cigsPerDay": cigsPerDay, "BPMeds": BPMeds,68        "prevalentStroke": prevalentStroke, "prevalentHyp": prevalentHyp,69        "diabetes": diabetes, "totChol": totChol, "sysBP": sysBP, "diaBP": diaBP,70        "BMI": BMI, "heartRate": heartRate, "glucose": glucose,71    }72 73    has_tabular = any(_has_value(fields[f]) for f in FEATURES)74    has_ecg = ecg is not None and bool(ecg.filename)75 76    if not has_tabular and not has_ecg:77        raise HTTPException(78            status_code=400,79            detail="Provide tabular patient data, an ECG image, or both.",80        )81 82    # Validate any provided tabular values (blanks are skipped → KNN-imputed).83    if has_tabular:84        errors = validate_tabular(fields)85        if errors:86            raise HTTPException(status_code=422, detail="; ".join(errors))87 88    branches: dict = {}89 90    # CPU-bound inference is offloaded to a threadpool so concurrent requests91    # don't block the event loop (torch/sklearn release the GIL).92    if has_tabular:93        try:94            from inference.framingham import predict_tabular95            branches["tabular"] = await run_in_threadpool(predict_tabular, fields)96        except FileNotFoundError as exc:97            raise HTTPException(status_code=503, detail=str(exc)) from exc98 99    if has_ecg:100        image_bytes = await ecg.read()101        try:102            from inference.ecg import predict_ecg103            branches["ecg"] = await run_in_threadpool(predict_ecg, image_bytes)104        except FileNotFoundError as exc:105            raise HTTPException(status_code=503, detail=str(exc)) from exc106        except ValueError as exc:107            raise HTTPException(status_code=400, detail=str(exc)) from exc108 109    # ── Fuse / select headline ───────────────────────────────────────────────110    if has_tabular and has_ecg:111        p = combine(branches["tabular"]["p_risk"], branches["ecg"]["p_risk"])112        mode, p_head = "multimodal", p113    elif has_tabular:114        mode, p_head = "tabular", branches["tabular"]["p_risk"]115    else:116        mode, p_head = "ecg", branches["ecg"]["p_risk"]117 118    return {119        "mode": mode,120        "risk_level": band_for(p_head),121        "p_risk": round(p_head, 4),122        "branches": branches,123    }124 125 126# ── Serve frontend ───────────────────────────────────────────────────────────127STATIC_DIR = Path(__file__).parent / "static"128STATIC_DIR.mkdir(exist_ok=True)129app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")130 131 132@app.get("/")133def serve_frontend():134    return FileResponse(str(STATIC_DIR / "index.html"))135