import io import os import requests import gradio as gr from PIL import Image PORT = os.getenv("PORT", "7860") BACKEND_URL = os.getenv("BACKEND_PREDICT_URL", f"http://127.0.0.1:{PORT}/predict") def call_backend(image: Image.Image, api_url: str) -> dict: buf = io.BytesIO() image.convert("RGB").save(buf, format="PNG") buf.seek(0) resp = requests.post( api_url, files={"file": ("xray.png", buf.getvalue(), "image/png")}, timeout=60, ) resp.raise_for_status() return resp.json() def render_top_metrics(payload: dict) -> tuple[str, str]: model_arm = payload.get("selected_model", "Unknown") latency_ms = max(payload.get("latency_ms", 0), 0) latency_html = f'
{latency_ms} ms
' if latency_ms < 0: latency_html = f'''
{latency_ms} ms
Bug: negative latency
Timing value is invalid — likely a clock sync issue.
Fix the timer before showing this to patients.
''' arm_html = f'''
MODEL (A/B ARM)
{model_arm}
''' lat_html = f'''
INFERENCE LATENCY
{latency_html}
''' return arm_html, lat_html def render_quality(payload: dict) -> str: drift = payload.get("drift", {}) alert = drift.get("drift_alert", "NORMAL") if alert == "DRIFT_DETECTED": return '''
INPUT IMAGE QUALITY CHECK
⚠ Out of Distribution
Distribution differs from training baseline
What this means for the patient
The model has never seen images like this. Predictions may be unreliable. Proceed with extreme caution.
''' return '''
INPUT IMAGE QUALITY CHECK
✓ Acceptable
Distribution matches training baseline
What this means for the patient
This does not mean your lungs are normal. It only means the uploaded X-ray image is technically readable by the model — the contrast, resolution, and framing look similar to images it was trained on.
''' _TIER_COLORS = { "Pneumonia": "#ef4444", "Pneumothorax": "#ef4444", "Mass": "#ef4444", "Nodule": "#ef4444", "Effusion": "#f59e0b", "Cardiomegaly": "#f59e0b", "Consolidation": "#f59e0b", } _TIER_DESC = { "Pneumonia": "Infection inflaming air sacs, potentially filling with fluid.", "Pneumothorax": "Collapsed lung — air leaked into the space between lung and chest wall.", "Mass": "Larger opacity (>3cm). Not actionable at this score.", "Nodule": "A small round opacity — may be benign or require follow-up.", "Effusion": "Fluid build-up in the pleural space.", "Cardiomegaly": "Enlarged heart — could indicate underlying heart conditions.", "Consolidation": "Region of normally compressible lung tissue filled with liquid.", "Infiltration": "Fluid or tissue density where there should be air — could suggest infection, inflammation, or early pneumonia.", } def render_top3(payload: dict) -> str: top3 = payload.get("top_predictions", []) inconclusive = payload.get("inconclusive_scan", False) html = '
' html += '
' html += '
TOP-3 PREDICTED PATHOLOGIES
' html += '
Critical    Significant    Standard
' html += '
' for p in top3: lbl = p["label"] conf = p["confidence"] col = _TIER_COLORS.get(lbl, "#3b82f6") desc = _TIER_DESC.get(lbl, "A generic chest anomaly flagged by the model.") conf_text = f"Confidence is very low ({conf}%)." if conf < 10 else f"Confidence: {conf}%." flagged = 'Flagged finding' if p["detected"] else '' html += f'''
{lbl} {flagged}
{conf}%
{desc} {conf_text}
''' if inconclusive: html += '''
All confidence scores are below 10% — interpret with extreme caution
Scores this low indicate the model is uncertain. A well-calibrated model should ideally show >50% before a finding is clinically noteworthy. These results should not drive any clinical decision without radiologist review.
''' html += '
' return html def render_14_classes(payload: dict) -> str: all_preds = payload.get("all_predictions", []) html = '
' html += '
ALL 14 SCREENED CONDITIONS
' html += '
' for p in all_preds: lbl = p["label"] conf = p["confidence"] det = p["detected"] if det: bg, color = "#78350f", "#fcd34d" # Yellow highlight else: bg, color = "#374151", "#9ca3af" # Gray text = f"{lbl} {conf}%" if det else lbl html += f'
{text}
' html += '
' html += '
Grey chips = below detection threshold. Not shown does not mean absent.
' html += '
' return html def render_calibration_info() -> str: """Provides historical benchmark context as requested in the redesign proposal.""" return '''
MODEL CALIBRATION & BENCHMARKING
Model calibrated on NIH ChestX-ray14. Historical AUC benchmarks: Note for Clinicians: Predictions for "Infiltration" and "Pneumonia" carry higher uncertainty due to lower historical AUC. Radiologist verification is mandatory.
''' def predict(image: Image.Image, api_url: str): if image is None: raise gr.Error("Please upload a chest X-ray image.") try: payload = call_backend(image, api_url) except Exception as exc: raise gr.Error(f"Backend error: {str(exc)}") from exc arm_html, lat_html = render_top_metrics(payload) qc_html = render_quality(payload) top3_html = render_top3(payload) classes_html = render_14_classes(payload) calib_html = render_calibration_info() ethics_html = """
ETHICAL & CLINICAL COMPLIANCE
Data Privacy: HIPAA-compliant processing. No PHI is stored.
Model Bias: Regular audits for demographic parity across age and gender.
Clinical Intent: Screening assistant only. Not for primary diagnosis.
""" summary_html = f'
CLINICAL SUMMARY
{payload.get("summary", "Screening complete.")}
' return arm_html, lat_html, summary_html, ethics_html, qc_html, top3_html, classes_html, calib_html CSS = """ body, .gradio-container { background-color: #1e1e1e !important; color: #e5e5e5 !important; } .custom-card { background: #2b2b2b; border-radius: 12px; padding: 20px; border: 1px solid #3f3f46; margin-bottom: 16px; } .metric-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #9ca3af; margin-bottom: 6px; font-weight: 600; } .metric-value { font-size: 1.25rem; font-weight: 500; color: #f4f4f5; } """ with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="PneumoOps Redesigned") as demo: with gr.Accordion("⚙️ Backend Settings", open=False): api_url = gr.Textbox(label="Backend URL", value=BACKEND_URL) with gr.Row(): with gr.Column(scale=1): image_input = gr.Image(type="pil", label="Upload Chest X-Ray") submit_btn = gr.Button("🔬 Run Screening", variant="primary", size="lg") with gr.Column(scale=2): with gr.Row(): arm_out = gr.HTML() lat_out = gr.HTML() summary_out = gr.HTML() ethics_out = gr.HTML() qc_out = gr.HTML() top3_out = gr.HTML() classes_out = gr.HTML() calib_out = gr.HTML() gr.HTML('''
⚖️ Responsible AI notice
This system assists in prediction, it does not confirm diagnoses. Results are statistical estimates and must be reviewed by a qualified radiologist or clinician before any medical decision is made. Do not use these results to self-diagnose.
''') submit_btn.click( fn=predict, inputs=[image_input, api_url], outputs=[arm_out, lat_out, summary_out, ethics_out, qc_out, top3_out, classes_out, calib_out], ) if __name__ == "__main__": port = int(os.getenv("GRADIO_PORT", "7860")) demo.launch(server_name="0.0.0.0", server_port=port)