File size: 13,286 Bytes
2e552e9
 
 
e7e6abe
2e552e9
 
be96fd0
 
2e552e9
 
 
 
 
 
 
 
 
 
 
 
 
e7e6abe
 
68bb893
 
e7e6abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e552e9
 
e7e6abe
 
 
 
 
 
 
 
 
 
 
 
2e552e9
 
e7e6abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e552e9
e7e6abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e552e9
 
 
 
 
 
e7e6abe
 
 
 
 
 
 
 
 
478712e
 
 
 
 
 
 
 
 
bdd5f1a
2e552e9
 
e7e6abe
 
 
2e552e9
e7e6abe
 
50d6286
e7e6abe
 
 
 
 
 
 
 
 
 
 
50d6286
e7e6abe
 
 
 
50d6286
2e552e9
 
e7e6abe
2e552e9
 
e7e6abe
2e552e9
0481484
e7e6abe
 
 
0481484
 
e7e6abe
 
 
478712e
 
e7e6abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e552e9
 
 
 
bdd5f1a
2e552e9
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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'<div class="metric-value">{latency_ms} ms</div>'
    if latency_ms < 0:
        latency_html = f'''
        <div class="metric-value" style="color: #ef4444;">{latency_ms} ms</div>
        <div style="background: #452424; border-left: 4px solid #ef4444; padding: 8px 12px; border-radius: 4px; margin-top: 8px;">
            <div style="color:#fca5a5;font-weight:600;font-size:0.85rem;">Bug: negative latency</div>
            <div style="color:#fecaca;font-size:0.8rem;">Timing value is invalid β€” likely a clock sync issue.<br>Fix the timer before showing this to patients.</div>
        </div>
        '''
        
    arm_html = f'''
    <div class="custom-card">
        <div class="metric-title">MODEL (A/B ARM)</div>
        <div class="metric-value">{model_arm}</div>
    </div>
    '''
    lat_html = f'''
    <div class="custom-card">
        <div class="metric-title">INFERENCE LATENCY</div>
        {latency_html}
    </div>
    '''
    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 '''
        <div class="custom-card" style="border-left: 4px solid #ef4444;">
            <div style="display:flex; justify-content:space-between; align-items:center;">
                <div class="metric-title">INPUT IMAGE QUALITY CHECK</div>
                <div style="background:#452424; color:#fca5a5; padding:4px 10px; border-radius:12px; font-size:0.75rem;">⚠ Out of Distribution</div>
            </div>
            <div style="font-size:1.1rem; font-weight:500; margin-top:4px;">Distribution differs from training baseline</div>
            <div style="background:#452424; padding:12px; border-radius:6px; margin-top:12px;">
                <div style="color:#fbbf24; font-weight:600; font-size:0.9rem; margin-bottom:4px;">What this means for the patient</div>
                <div style="color:#d1d5db; font-size:0.85rem;">The model has never seen images like this. Predictions may be unreliable. Proceed with extreme caution.</div>
            </div>
        </div>
        '''
    return '''
    <div class="custom-card">
        <div style="display:flex; justify-content:space-between; align-items:center;">
            <div class="metric-title">INPUT IMAGE QUALITY CHECK</div>
            <div style="background:#064e3b; color:#34d399; padding:4px 10px; border-radius:12px; font-size:0.75rem;">βœ“ Acceptable</div>
        </div>
        <div style="font-size:1.1rem; font-weight:500; margin-top:4px; margin-bottom:12px;">Distribution matches training baseline</div>
        <div style="background:#453015; padding:12px; border-radius:6px; margin-top:8px; border-left: 4px solid #f59e0b;">
            <div style="color:#fbbf24; font-weight:600; font-size:0.9rem; margin-bottom:4px;">What this means for the patient</div>
            <div style="color:#d1d5db; font-size:0.85rem; line-height:1.4;">This <i>does not</i> 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.</div>
        </div>
    </div>
    '''

_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 = '<div class="custom-card">'
    html += '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">'
    html += '<div class="metric-title">TOP-3 PREDICTED PATHOLOGIES</div>'
    html += '<div style="font-size:0.8rem; color:#9ca3af;"><span style="color:#ef4444;">●</span> Critical &nbsp;&nbsp; <span style="color:#f59e0b;">●</span> Significant &nbsp;&nbsp; <span style="color:#3b82f6;">●</span> Standard</div>'
    html += '</div>'
    
    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 = '<span style="background:#1e3a8a; color:#93c5fd; padding:4px 10px; border-radius:12px; font-size:0.75rem; margin-left:8px;">Flagged finding</span>' if p["detected"] else ''
        
        html += f'''
        <div style="background:#333333; padding:16px; border-radius:8px; margin-bottom:12px;">
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
                <div style="font-size:1.1rem; font-weight:600;"><span style="color:{col}; font-size:1.2rem;">●</span> {lbl} {flagged}</div>
                <div style="font-weight:600; font-size:1.2rem;">{conf}%</div>
            </div>
            <div style="width:100%; background:#4b5563; height:8px; border-radius:4px; overflow:hidden; margin-bottom:12px;">
                <div style="width:{conf}%; background:{col}; height:100%;"></div>
            </div>
            <div style="font-size:0.85rem; color:#d1d5db;">{desc} <strong>{conf_text}</strong></div>
        </div>
        '''
        
    if inconclusive:
        html += '''
        <div style="background:#452424; border:1px solid #7f1d1d; padding:16px; border-radius:8px; margin-top:16px;">
            <div style="color:#fca5a5; font-weight:600; margin-bottom:4px;">All confidence scores are below 10% β€” interpret with extreme caution</div>
            <div style="color:#fecaca; font-size:0.9rem; line-height:1.4;">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 <i>not</i> drive any clinical decision without radiologist review.</div>
        </div>
        '''
    html += '</div>'
    return html

def render_14_classes(payload: dict) -> str:
    all_preds = payload.get("all_predictions", [])
    
    html = '<div class="custom-card">'
    html += '<div class="metric-title" style="margin-bottom:12px;">ALL 14 SCREENED CONDITIONS</div>'
    html += '<div style="display:flex; flex-wrap:wrap; gap:8px;">'
    
    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'<div style="background:{bg}; color:{color}; padding:6px 14px; border-radius:16px; font-size:0.85rem; font-weight:500; border:1px solid #4b5563;">{text}</div>'
        
    html += '</div>'
    html += '<div style="font-size:0.8rem; color:#6b7280; margin-top:16px;">Grey chips = below detection threshold. Not shown does not mean absent.</div>'
    html += '</div>'
    return html

def render_calibration_info() -> str:
    """Provides historical benchmark context as requested in the redesign proposal."""
    return '''
    <div class="custom-card">
        <div class="metric-title">MODEL CALIBRATION & BENCHMARKING</div>
        <div style="font-size:0.88rem; color:#d1d5db; line-height:1.6;">
            Model calibrated on NIH ChestX-ray14. Historical AUC benchmarks:
            <ul style="margin-top:8px; padding-left:18px; margin-bottom:8px;">
                <li><b style="color:#34d399;">Cardiomegaly:</b> ~0.81 AUC (Highly reliable)</li>
                <li><b style="color:#fbbf24;">Infiltration:</b> ~0.70 AUC (Historically lower reliability)</li>
                <li><b style="color:#fbbf24;">Pneumonia:</b> ~0.68 AUC</li>
            </ul>
            <i>Note for Clinicians:</i> Predictions for "Infiltration" and "Pneumonia" carry higher uncertainty due to lower historical AUC. Radiologist verification is mandatory.
        </div>
    </div>
    '''

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 = """<div class="custom-card" style="border-top: 4px solid #ef4444; background: #450a0a;">
    <div class="metric-title">ETHICAL & CLINICAL COMPLIANCE</div>
    <div style="font-size:0.9rem; color:#fca5a5;">
        <b>Data Privacy:</b> HIPAA-compliant processing. No PHI is stored.<br>
        <b>Model Bias:</b> Regular audits for demographic parity across age and gender.<br>
        <b>Clinical Intent:</b> Screening assistant only. Not for primary diagnosis.
    </div>
</div>"""
    summary_html = f'<div class="custom-card" style="border-left: 4px solid #3b82f6; background: #1e293b; padding: 15px; margin-bottom: 10px;"><div class="metric-title">CLINICAL SUMMARY</div><div style="font-size:1.1rem; font-weight:500; color:#f8fafc;">{payload.get("summary", "Screening complete.")}</div></div>'
    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('''
            <div class="custom-card">
                <div style="font-weight:600; font-size:0.95rem; margin-bottom:4px; display:flex; align-items:center; gap:8px;">
                    βš–οΈ Responsible AI notice
                </div>
                <div style="color:#9ca3af; font-size:0.9rem; line-height:1.5;">
                    This system <i>assists in prediction</i>, 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.
                </div>
            </div>
            ''')

    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)