ecg-smartread / app.py
adelfr2009's picture
Upload folder using huggingface_hub
27b730f verified
Raw
History Blame Contribute Delete
6.54 kB
"""
ECG SmartRead AI Microservice — Pure Gradio for Hugging Face ZeroGPU.
Exposes two API endpoints via Gradio:
- predict_manual: BioLinkBERT classification
- generate_report: SciFive T5 report generation
Called from the VPS backend via the Gradio Python Client.
"""
import json
import os
from functools import lru_cache
import spaces
import gradio as gr
import torch
from transformers import AutoTokenizer, T5ForConditionalGeneration, AutoModelForSequenceClassification
from huggingface_hub import snapshot_download
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_REPO_ID = "adelfr2009/ecg-smartread-models"
MODELS_DIR = os.path.join(BASE_DIR, "models")
GENERATION_DIR = os.path.join(MODELS_DIR, "generation")
MANUAL_TEST_DIR = os.path.join(MODELS_DIR, "test")
# ---------------------------------------------------------------------------
# Model loading
# ---------------------------------------------------------------------------
def _ensure_models_downloaded():
gen_ok = os.path.exists(os.path.join(GENERATION_DIR, "model.safetensors"))
test_ok = os.path.exists(os.path.join(MANUAL_TEST_DIR, "model.safetensors"))
if not gen_ok or not test_ok:
print(f"Downloading models from {MODEL_REPO_ID} ...")
snapshot_download(repo_id=MODEL_REPO_ID, local_dir=MODELS_DIR)
print("Download complete.")
@lru_cache(maxsize=1)
def _load_generation():
_ensure_models_downloaded()
tok = AutoTokenizer.from_pretrained(GENERATION_DIR)
mdl = T5ForConditionalGeneration.from_pretrained(GENERATION_DIR)
mdl.eval()
dev = "cuda" if torch.cuda.is_available() else "cpu"
mdl.to(dev)
return tok, mdl, dev
@lru_cache(maxsize=1)
def _load_classification():
_ensure_models_downloaded()
tok = AutoTokenizer.from_pretrained(MANUAL_TEST_DIR)
mdl = AutoModelForSequenceClassification.from_pretrained(MANUAL_TEST_DIR)
mdl.eval()
dev = "cuda" if torch.cuda.is_available() else "cpu"
mdl.to(dev)
with open(os.path.join(MANUAL_TEST_DIR, "thresholds.json")) as f:
th = json.load(f)
return tok, mdl, dev, th["labels"], th["thresholds"], th.get("weak_classes", [])
# ---------------------------------------------------------------------------
# GPU-decorated inference functions (ZeroGPU requirement)
# ---------------------------------------------------------------------------
@spaces.GPU
def predict_manual(
heart_rate, qrs_duration, qt_interval, qtc_interval,
pr_interval, p_axis, qrs_axis, t_axis, age, sex
):
"""Classify ECG from manual global metrics via BioLinkBERT."""
def fmt(v):
return "unknown" if v is None or str(v).strip() == "" else v
text = (
f"Heart Rate: {fmt(heart_rate)} BPM. "
f"QRS Duration: {fmt(qrs_duration)} ms. "
f"QT Interval: {fmt(qt_interval)} ms. "
f"QTc Interval: {fmt(qtc_interval)} ms. "
f"PR Interval: {fmt(pr_interval)} ms. "
f"P axis: {fmt(p_axis)} degrees. QRS axis: {fmt(qrs_axis)} degrees. T axis: {fmt(t_axis)} degrees. "
f"Age: {fmt(age)}. Sex: {fmt(sex)}."
)
tok, mdl, dev, labels, thresholds, weak_classes = _load_classification()
inputs = tok(text, truncation=True, padding="max_length",
max_length=64, return_tensors="pt").to(dev)
with torch.no_grad():
logits = mdl(**inputs).logits
probs = torch.sigmoid(logits)[0].cpu().numpy()
probabilities = {}
predictions = {}
for label, prob, thresh in zip(labels, probs, thresholds):
probabilities[label] = round(float(prob) * 100, 1)
predictions[label] = bool(prob > thresh)
return json.dumps({
"input_text": text,
"probabilities": probabilities,
"predictions": predictions,
"weak_classes": weak_classes,
})
@spaces.GPU
def generate_report(input_text, max_new_tokens=200):
"""Generate medical report from clinical summary via SciFive T5."""
tok, mdl, dev = _load_generation()
max_tok = int(max_new_tokens) if max_new_tokens else 200
inputs = tok(input_text, return_tensors="pt", truncation=True,
max_length=512).to(dev)
with torch.no_grad():
ids = mdl.generate(**inputs, max_new_tokens=max_tok,
num_beams=4, early_stopping=True)
generated = tok.decode(ids[0], skip_special_tokens=True)
return json.dumps({
"input_text": input_text,
"generated_report": generated,
})
# ---------------------------------------------------------------------------
# Gradio interface (required for ZeroGPU to detect @spaces.GPU functions)
# ---------------------------------------------------------------------------
with gr.Blocks(title="ECG SmartRead AI") as demo:
gr.Markdown("# 🫀 ECG SmartRead AI Microservice")
gr.Markdown("API-only service. Use the API tab below or call via `gradio_client`.")
with gr.Tab("Classification"):
with gr.Row():
hr = gr.Number(label="Heart Rate (BPM)")
qrs = gr.Number(label="QRS Duration (ms)")
qt = gr.Number(label="QT Interval (ms)")
qtc = gr.Number(label="QTc Interval (ms)")
with gr.Row():
pr = gr.Number(label="PR Interval (ms)")
pa = gr.Number(label="P Axis (°)")
qa = gr.Number(label="QRS Axis (°)")
ta = gr.Number(label="T Axis (°)")
with gr.Row():
age = gr.Number(label="Age")
sex = gr.Textbox(label="Sex", placeholder="male / female")
classify_btn = gr.Button("Classify", variant="primary")
classify_out = gr.Textbox(label="Result (JSON)", lines=10)
classify_btn.click(
fn=predict_manual,
inputs=[hr, qrs, qt, qtc, pr, pa, qa, ta, age, sex],
outputs=classify_out,
)
with gr.Tab("Report Generation"):
gen_input = gr.Textbox(label="Clinical Summary Input", lines=5,
placeholder="Heart Rate: 75 BPM. QRS Duration: ...")
gen_tokens = gr.Number(label="Max New Tokens", value=200)
gen_btn = gr.Button("Generate Report", variant="primary")
gen_out = gr.Textbox(label="Generated Report (JSON)", lines=10)
gen_btn.click(
fn=generate_report,
inputs=[gen_input, gen_tokens],
outputs=gen_out,
)
demo.launch()