Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import numpy as np | |
| import torch | |
| from fastapi import FastAPI | |
| from pydantic import BaseModel | |
| from typing import List | |
| from models.hmt_ecgnet import HMT_ECGNet | |
| from config import N_LEADS | |
| from transforms import preprocess_signal | |
| # Setup | |
| DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| DIAG_CLASSES = ["NORM", "MI", "STTC", "CD", "HYP"] | |
| with open("artifacts/multilabel_thresholds.json") as f: | |
| MULTI_THRESHOLDS = json.load(f) | |
| MI_BINARY_THRESHOLD = 0.05 | |
| app = FastAPI(title="ECG Diagnostic API") | |
| # Find latest checkpoint automatically | |
| def find_ckpt(): | |
| for root, _, files in os.walk("artifacts"): | |
| if "multilabel_best.pth" in files: | |
| return os.path.join(root, "multilabel_best.pth") | |
| raise FileNotFoundError("multilabel_best.pth not found in artifacts/") | |
| # Load model ONCE at startup | |
| def load_model(): | |
| ckpt_path = find_ckpt() | |
| model = HMT_ECGNet(num_classes=5, num_leads=N_LEADS).to(DEVICE) | |
| # IMPORTANT FIX for PyTorch 2.6 | |
| ckpt = torch.load(ckpt_path, map_location=DEVICE, weights_only=False) | |
| model.load_state_dict(ckpt["model_state_dict"]) | |
| model.eval() | |
| return model | |
| model = load_model() | |
| # ECG Request Schema (real device style) | |
| class ECGRequest(BaseModel): | |
| lead1: List[float] | |
| lead2: List[float] | |
| lead3: List[float] | |
| lead4: List[float] | |
| lead5: List[float] | |
| lead6: List[float] | |
| lead7: List[float] | |
| lead8: List[float] | |
| lead9: List[float] | |
| lead10: List[float] | |
| lead11: List[float] | |
| lead12: List[float] | |
| # Prediction Endpoint | |
| def predict_ecg(req: ECGRequest): | |
| try: | |
| # Combine leads into array | |
| raw = np.array([ | |
| req.lead1, req.lead2, req.lead3, req.lead4, | |
| req.lead5, req.lead6, req.lead7, req.lead8, | |
| req.lead9, req.lead10, req.lead11, req.lead12 | |
| ]) | |
| # Auto-select correct 10s window (like real systems) | |
| fs = 500 | |
| window = 10 * fs | |
| if raw.shape[1] > window: | |
| start = raw.shape[1] // 2 - window // 2 | |
| raw = raw[:, start:start + window] | |
| # Preprocess exactly like training | |
| ecg = preprocess_signal(raw) | |
| x = torch.tensor(ecg, dtype=torch.float32).unsqueeze(0).to(DEVICE) | |
| # Model inference | |
| with torch.no_grad(): | |
| probs = torch.sigmoid(model(x)).cpu().numpy()[0] | |
| result = {} | |
| predicted = [] | |
| for cls, p in zip(DIAG_CLASSES, probs): | |
| thr = MULTI_THRESHOLDS[cls] | |
| result[cls] = float(p) | |
| if p >= thr: | |
| predicted.append(cls) | |
| mi_prob = float(probs[1]) | |
| return { | |
| "probabilities": result, | |
| "predicted_classes": predicted, | |
| "mi_probability": mi_prob, | |
| "mi_risk": mi_prob >= MI_BINARY_THRESHOLD, | |
| } | |
| except Exception as e: | |
| return {"error": str(e)} | |