Spaces:
Sleeping
Sleeping
| import os | |
| import io | |
| import base64 | |
| import hashlib | |
| import numpy as np | |
| import gradio as gr | |
| from PIL import Image | |
| from datetime import datetime | |
| import tensorflow as tf | |
| import model_setup | |
| os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" | |
| CLASS_LABELS = ["COVID-19", "Normal", "Viral Pneumonia", "Lung Opacity"] | |
| CLASS_COLORS = { | |
| "COVID-19": "#ff5f5f", | |
| "Normal": "#3dd68c", | |
| "Viral Pneumonia": "#ffaa44", | |
| "Lung Opacity": "#4fa3f7", | |
| } | |
| CLASS_TAGS = { | |
| "COVID-19": "CRITICAL", | |
| "Normal": "CLEAR", | |
| "Viral Pneumonia": "ABNORMAL", | |
| "Lung Opacity": "ABNORMAL", | |
| } | |
| CLASS_DESCRIPTIONS = { | |
| "COVID-19": | |
| "Findings consistent with COVID-19 pneumonia. Bilateral, peripheral, " | |
| "and basal-predominant ground-glass opacities are characteristic. " | |
| "Clinical correlation and RT-PCR confirmation recommended.", | |
| "Normal": | |
| "No significant pathological findings detected. Lung fields are clear " | |
| "with no evidence of consolidation, effusion, or interstitial changes. " | |
| "Standard follow-up as clinically indicated.", | |
| "Viral Pneumonia": | |
| "Findings suggestive of viral pneumonia. Diffuse bilateral interstitial " | |
| "infiltrates with ground-glass opacity pattern. Further evaluation with " | |
| "HRCT and laboratory correlation is advised.", | |
| "Lung Opacity": | |
| "Area(s) of increased opacity detected. Differential includes consolidation, " | |
| "atelectasis, pleural effusion, or mass lesion. CT evaluation recommended " | |
| "for further characterisation.", | |
| } | |
| IMG_SIZE = (128, 128) | |
| print("Loading modelβ¦") | |
| model = tf.keras.models.load_model(model_setup.paths["cnn_model_lung_detection.keras"]) | |
| print("Model ready.") | |
| _bg_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), | |
| "attached_assets", "image_1779746772720.png") | |
| _buf = io.BytesIO() | |
| _pil = Image.open(_bg_path) | |
| _pil.resize((1400, int(_pil.height * 1400 / _pil.width)), Image.LANCZOS).convert("RGB").save( | |
| _buf, format="JPEG", quality=84) | |
| BG_URI = f"data:image/jpeg;base64,{base64.b64encode(_buf.getvalue()).decode()}" | |
| print("Background loaded.") | |
| def preprocess(image: Image.Image) -> np.ndarray: | |
| arr = np.array(image.convert("RGB").resize(IMG_SIZE), dtype=np.float32) / 255.0 | |
| return np.expand_dims(arr, axis=0) | |
| PLACEHOLDER = """ | |
| <div style="font-family:'Inter',sans-serif;"> | |
| <!-- System status grid --> | |
| <div style="font-size:9px;font-weight:600;letter-spacing:0.22em;color:#6a9ec0; | |
| text-transform:uppercase;padding-bottom:8px;margin-bottom:12px; | |
| border-bottom:1px solid rgba(60,130,210,0.30);">System Status</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:20px;"> | |
| <div style="background:rgba(4,10,26,0.92);border:1px solid rgba(60,130,210,0.32); | |
| border-radius:5px;padding:11px 14px;"> | |
| <div style="font-size:9px;color:#5a8aaa;font-weight:600;letter-spacing:0.16em; | |
| text-transform:uppercase;margin-bottom:6px;">Model</div> | |
| <div style="font-size:14px;color:#3dd68c;font-weight:300;letter-spacing:0.06em;">READY</div> | |
| </div> | |
| <div style="background:rgba(4,10,26,0.92);border:1px solid rgba(60,130,210,0.32); | |
| border-radius:5px;padding:11px 14px;"> | |
| <div style="font-size:9px;color:#5a8aaa;font-weight:600;letter-spacing:0.16em; | |
| text-transform:uppercase;margin-bottom:6px;">Mode</div> | |
| <div style="font-size:14px;color:#4fa3f7;font-weight:300;letter-spacing:0.06em;">CPU</div> | |
| </div> | |
| <div style="background:rgba(4,10,26,0.92);border:1px solid rgba(60,130,210,0.32); | |
| border-radius:5px;padding:11px 14px;"> | |
| <div style="font-size:9px;color:#5a8aaa;font-weight:600;letter-spacing:0.16em; | |
| text-transform:uppercase;margin-bottom:6px;">Classes</div> | |
| <div style="font-size:14px;color:#98c0d8;font-weight:300; | |
| font-family:'JetBrains Mono','Courier New',monospace;">4</div> | |
| </div> | |
| <div style="background:rgba(4,10,26,0.92);border:1px solid rgba(60,130,210,0.32); | |
| border-radius:5px;padding:11px 14px;"> | |
| <div style="font-size:9px;color:#5a8aaa;font-weight:600;letter-spacing:0.16em; | |
| text-transform:uppercase;margin-bottom:6px;">Input Size</div> | |
| <div style="font-size:14px;color:#98c0d8;font-weight:300; | |
| font-family:'JetBrains Mono','Courier New',monospace;">128×128</div> | |
| </div> | |
| </div> | |
| <!-- Detection classes --> | |
| <div style="font-size:9px;font-weight:600;letter-spacing:0.22em;color:#6a9ec0; | |
| text-transform:uppercase;padding-bottom:8px;margin-bottom:12px; | |
| border-bottom:1px solid rgba(60,130,210,0.30);">Detection Classes</div> | |
| <div style="display:flex;flex-direction:column;gap:8px;margin-bottom:20px;"> | |
| <div style="display:flex;align-items:center;gap:12px;padding:9px 12px; | |
| background:rgba(4,10,26,0.88);border-radius:4px; | |
| border:1px solid rgba(255,95,95,0.25);"> | |
| <div style="width:6px;height:6px;border-radius:50%;background:#ff5f5f; | |
| box-shadow:0 0 6px #ff5f5faa;flex-shrink:0;"></div> | |
| <span style="font-size:11px;color:#c0d8f0;font-weight:300;letter-spacing:0.04em;">COVID-19</span> | |
| <span style="margin-left:auto;font-size:9px;color:#5a7a96;letter-spacing:0.10em;font-weight:500;">CLASS 1</span> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:12px;padding:9px 12px; | |
| background:rgba(4,10,26,0.88);border-radius:4px; | |
| border:1px solid rgba(61,214,140,0.25);"> | |
| <div style="width:6px;height:6px;border-radius:50%;background:#3dd68c; | |
| box-shadow:0 0 6px #3dd68caa;flex-shrink:0;"></div> | |
| <span style="font-size:11px;color:#c0d8f0;font-weight:300;letter-spacing:0.04em;">Normal</span> | |
| <span style="margin-left:auto;font-size:9px;color:#5a7a96;letter-spacing:0.10em;font-weight:500;">CLASS 2</span> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:12px;padding:9px 12px; | |
| background:rgba(4,10,26,0.88);border-radius:4px; | |
| border:1px solid rgba(255,170,68,0.25);"> | |
| <div style="width:6px;height:6px;border-radius:50%;background:#ffaa44; | |
| box-shadow:0 0 6px #ffaa44aa;flex-shrink:0;"></div> | |
| <span style="font-size:11px;color:#c0d8f0;font-weight:300;letter-spacing:0.04em;">Viral Pneumonia</span> | |
| <span style="margin-left:auto;font-size:9px;color:#5a7a96;letter-spacing:0.10em;font-weight:500;">CLASS 3</span> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:12px;padding:9px 12px; | |
| background:rgba(4,10,26,0.88);border-radius:4px; | |
| border:1px solid rgba(79,163,247,0.25);"> | |
| <div style="width:6px;height:6px;border-radius:50%;background:#4fa3f7; | |
| box-shadow:0 0 6px #4fa3f7aa;flex-shrink:0;"></div> | |
| <span style="font-size:11px;color:#c0d8f0;font-weight:300;letter-spacing:0.04em;">Lung Opacity</span> | |
| <span style="margin-left:auto;font-size:9px;color:#5a7a96;letter-spacing:0.10em;font-weight:500;">CLASS 4</span> | |
| </div> | |
| </div> | |
| <!-- Instructions --> | |
| <div style="padding:12px 14px;background:rgba(4,10,26,0.85); | |
| border:1px dashed rgba(60,130,210,0.35);border-radius:5px; | |
| font-size:11px;color:#7aaccc;font-weight:300; | |
| line-height:1.8;text-align:center;letter-spacing:0.03em;"> | |
| Upload a chest radiograph on the left, then press | |
| <span style="color:#98c0d8;font-weight:500;">Run Analysis</span> | |
| to generate a structured diagnostic report. | |
| </div> | |
| </div> | |
| """ | |
| def predict(image: Image.Image): | |
| if image is None: | |
| return gr.update(value=PLACEHOLDER), gr.update(visible=False) | |
| probs = model.predict(preprocess(image), verbose=0)[0] | |
| idx = int(np.argmax(probs)) | |
| label = CLASS_LABELS[idx] | |
| conf = float(probs[idx]) * 100 | |
| color = CLASS_COLORS[label] | |
| desc = CLASS_DESCRIPTIONS[label] | |
| tag = CLASS_TAGS[label] | |
| scan_id = hashlib.md5(image.tobytes()[:256]).hexdigest()[:8].upper() | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M UTC") | |
| dims = f"{image.width} Γ {image.height}" | |
| # SVG confidence ring (circumference of r=28: 2ΟΓ28 β 175.9) | |
| arc = conf / 100 * 175.9 | |
| ring_bg = "#162030" | |
| ring_svg = f""" | |
| <svg width="76" height="76" viewBox="0 0 76 76"> | |
| <circle cx="38" cy="38" r="28" fill="none" stroke="{ring_bg}" stroke-width="5"/> | |
| <circle cx="38" cy="38" r="28" fill="none" stroke="{color}" stroke-width="5" | |
| stroke-dasharray="{arc:.1f} 175.9" stroke-linecap="round" | |
| transform="rotate(-90 38 38)"/> | |
| <text x="38" y="43" text-anchor="middle" | |
| font-family="'JetBrains Mono','Courier New',monospace" | |
| font-size="13" font-weight="600" fill="{color}">{conf:.0f}%</text> | |
| </svg>""" | |
| # Probability bars | |
| bars = "" | |
| for i, (cls, prob) in enumerate(zip(CLASS_LABELS, probs)): | |
| pct = float(prob) * 100 | |
| clr = CLASS_COLORS[cls] | |
| w = f"{pct:.1f}" | |
| is_primary = (i == idx) | |
| glow = f"box-shadow:0 0 8px {clr}55;" if is_primary else "" | |
| alpha = "ff" if is_primary else "66" | |
| bars += f""" | |
| <div style="display:grid;grid-template-columns:136px 1fr 52px;align-items:center;gap:12px;"> | |
| <span style="font-size:10.5px;font-weight:{'500' if is_primary else '300'};color:{'#98c0d8' if is_primary else '#5a7a96'};letter-spacing:0.04em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{cls}</span> | |
| <div style="height:4px;background:rgba(18,38,68,0.90);border-radius:2px;overflow:hidden;"> | |
| <div style="width:{w}%;height:100%;border-radius:2px;background:{clr};{glow}transition:width 0.7s cubic-bezier(0.4,0,0.2,1);"></div> | |
| </div> | |
| <span style="font-family:'JetBrains Mono',monospace;font-size:9.5px;font-weight:500;color:{clr if is_primary else '#5a7a96'};text-align:right;white-space:nowrap;">{pct:.1f}<span style="font-size:7.5px;opacity:0.65;">%</span></span> | |
| </div>""" | |
| html = f""" | |
| <div style="font-family:'Inter',sans-serif;"> | |
| <!-- Report header --> | |
| <div style="display:flex;justify-content:space-between;align-items:flex-start; | |
| margin-bottom:16px;padding-bottom:12px; | |
| border-bottom:1px solid rgba(60,120,200,0.28);"> | |
| <div style="display:flex;flex-direction:column;gap:5px;"> | |
| <div style="display:flex;gap:10px;align-items:baseline;"> | |
| <span style="font-size:9px;font-weight:600;letter-spacing:0.18em;color:#5a8aaa; | |
| text-transform:uppercase;width:72px;flex-shrink:0;">Scan ID</span> | |
| <span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:#8ec0e0; | |
| letter-spacing:0.05em;">{scan_id}</span> | |
| </div> | |
| <div style="display:flex;gap:10px;align-items:baseline;"> | |
| <span style="font-size:9px;font-weight:600;letter-spacing:0.18em;color:#5a8aaa; | |
| text-transform:uppercase;width:72px;flex-shrink:0;">Date/Time</span> | |
| <span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:#8ec0e0; | |
| letter-spacing:0.05em;">{timestamp}</span> | |
| </div> | |
| <div style="display:flex;gap:10px;align-items:baseline;"> | |
| <span style="font-size:9px;font-weight:600;letter-spacing:0.18em;color:#5a8aaa; | |
| text-transform:uppercase;width:72px;flex-shrink:0;">Dimensions</span> | |
| <span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:#8ec0e0; | |
| letter-spacing:0.05em;">{dims} px</span> | |
| </div> | |
| <div style="display:flex;gap:10px;align-items:baseline;"> | |
| <span style="font-size:9px;font-weight:600;letter-spacing:0.18em;color:#5a8aaa; | |
| text-transform:uppercase;width:72px;flex-shrink:0;">Modality</span> | |
| <span style="font-size:10px;color:#8ec0e0;letter-spacing:0.04em;">CXR</span> | |
| </div> | |
| </div> | |
| <div style="font-size:9px;font-weight:700;letter-spacing:0.20em;text-transform:uppercase; | |
| padding:5px 11px;border-radius:3px;border:1px solid; | |
| background:rgba({_badge_rgb(color)},0.15); | |
| border-color:rgba({_badge_rgb(color)},0.40); | |
| color:{color};flex-shrink:0;"> | |
| {tag} | |
| </div> | |
| </div> | |
| <!-- Primary finding --> | |
| <div style="font-size:9px;font-weight:600;letter-spacing:0.22em;color:#5a8aaa; | |
| text-transform:uppercase;margin-bottom:10px; | |
| padding-bottom:6px;border-bottom:1px solid rgba(60,120,200,0.18);">Primary Finding</div> | |
| <div style="display:flex;align-items:center;gap:18px;padding:14px 16px; | |
| background:rgba(4,10,26,0.80);border:1px solid rgba(60,130,220,0.22); | |
| border-radius:6px;margin-bottom:16px;"> | |
| <div style="flex-shrink:0;"> | |
| {ring_svg} | |
| </div> | |
| <div style="flex:1;min-width:0;"> | |
| <div style="font-size:20px;font-weight:300;letter-spacing:0.06em; | |
| line-height:1.15;margin-bottom:6px;color:{color};">{label}</div> | |
| <div style="font-size:11px;font-weight:300;color:#6a90ac;letter-spacing:0.04em;"> | |
| Model confidence: <span style="font-family:'JetBrains Mono',monospace;font-size:10.5px;font-weight:500;color:{color};">{conf:.2f}%</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Clinical notes --> | |
| <div style="font-size:9px;font-weight:600;letter-spacing:0.22em;color:#5a8aaa; | |
| text-transform:uppercase;margin-bottom:10px;margin-top:16px; | |
| padding-bottom:6px;border-bottom:1px solid rgba(60,120,200,0.18);">Clinical Impression</div> | |
| <div style="font-size:11.5px;font-weight:300;color:#b4d0e4; | |
| line-height:1.80;letter-spacing:0.025em; | |
| padding:12px 14px;background:rgba(4,10,26,0.55); | |
| border-radius:5px;border:1px solid rgba(60,130,220,0.14);"> | |
| {desc} | |
| </div> | |
| <!-- Differential --> | |
| <div style="font-size:9px;font-weight:600;letter-spacing:0.22em;color:#5a8aaa; | |
| text-transform:uppercase;margin-bottom:10px;margin-top:16px; | |
| padding-bottom:6px;border-bottom:1px solid rgba(60,120,200,0.18);">Differential Diagnosis</div> | |
| <div style="display:flex;flex-direction:column;gap:9px;"> | |
| {bars} | |
| </div> | |
| <!-- Disclaimer --> | |
| <div style="margin-top:18px;padding:10px 14px; | |
| background:rgba(35,22,6,0.45);border:1px solid rgba(130,90,25,0.28); | |
| border-radius:4px;font-size:9.5px;font-weight:300; | |
| color:#a08050;line-height:1.65;letter-spacing:0.02em;"> | |
| ⚠️ FOR RESEARCH AND EDUCATIONAL USE ONLY. This AI output is not a clinical diagnosis and must not be used as a substitute for professional radiological assessment. | |
| </div> | |
| </div>""" | |
| return gr.update(value=html), gr.update(visible=True) | |
| def _badge_rgb(hex_color: str) -> str: | |
| h = hex_color.lstrip("#") | |
| r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) | |
| return f"{r},{g},{b}" | |
| css = f""" | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap'); | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| RESET | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PAGE BACKGROUND β medical lab photo, zoomed to screen area | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| html, body {{ height: 100%; }} | |
| body {{ | |
| font-family: 'Inter', system-ui, sans-serif; | |
| background-image: url('{BG_URI}'); | |
| background-size: 190% auto; | |
| background-position: 6% 28%; | |
| background-attachment: fixed; | |
| background-repeat: no-repeat; | |
| background-color: #ffffff; | |
| }} | |
| /* No overlay β clean white background with dark cards */ | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GRADIO CHROME STRIP | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .gradio-container {{ | |
| position: relative !important; | |
| z-index: 1 !important; | |
| max-width: 1000px !important; | |
| width: 100% !important; | |
| margin: 0 auto !important; | |
| padding: 24px 16px 40px !important; | |
| background: #ffffff !important; | |
| }} | |
| /* Remove all default Gradio block chrome */ | |
| .gradio-container .block, | |
| .gradio-container .form, | |
| .gradio-container .gap, | |
| .gradio-container .padded, | |
| .gradio-container .contain {{ | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| padding: 0 !important; | |
| gap: 0 !important; | |
| }} | |
| footer, .built-with, #footer {{ display: none !important; }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SYSTEM HEADER | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .sys-header {{ | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: rgba(2, 6, 16, 0.98); | |
| border: 1px solid rgba(50, 120, 210, 0.40); | |
| border-bottom: 1px solid rgba(30, 90, 160, 0.25); | |
| border-radius: 8px 8px 0 0; | |
| padding: 0 24px; | |
| height: 64px; | |
| box-shadow: | |
| inset 0 2px 0 rgba(60, 140, 240, 0.60), | |
| 0 -1px 20px rgba(30, 80, 180, 0.10); | |
| }} | |
| .sys-title {{ | |
| font-size: 12px; | |
| font-weight: 600; | |
| letter-spacing: 0.24em; | |
| color: #98c8e8; | |
| text-transform: uppercase; | |
| line-height: 1; | |
| margin-bottom: 7px; | |
| }} | |
| .sys-subtitle {{ | |
| font-size: 9px; | |
| font-weight: 400; | |
| letter-spacing: 0.16em; | |
| color: #567a92; | |
| text-transform: uppercase; | |
| }} | |
| .sys-indicators {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| }} | |
| .sys-dot {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| font-size: 9px; | |
| font-weight: 500; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: #6898b0; | |
| }} | |
| .sys-dot::before {{ | |
| content: ''; | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| }} | |
| .sys-dot.ready::before {{ background: #3dd68c; box-shadow: 0 0 8px #3dd68caa; }} | |
| .sys-dot.cpu::before {{ background: #4fa3f7; box-shadow: 0 0 8px #4fa3f788; }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MAIN PANEL β two columns | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .main-row {{ | |
| display: flex !important; | |
| gap: 0 !important; | |
| background: #0a1220; | |
| border: 1px solid rgba(50, 120, 210, 0.40); | |
| border-top: none; | |
| border-radius: 0 0 8px 8px; | |
| overflow: hidden; | |
| box-shadow: | |
| 0 20px 60px rgba(0, 0, 0, 0.55), | |
| 0 0 100px rgba(20, 60, 160, 0.06); | |
| }} | |
| /* Ensure Gradio's row is a flex container */ | |
| .gradio-container .main-row > .row, | |
| .gradio-container .row {{ | |
| display: flex !important; | |
| flex-wrap: nowrap !important; | |
| gap: 0 !important; | |
| background: transparent !important; | |
| }} | |
| /* Columns */ | |
| .left-col, .right-col {{ | |
| flex: 1 !important; | |
| min-width: 0 !important; | |
| padding: 24px 26px !important; | |
| display: flex !important; | |
| flex-direction: column !important; | |
| gap: 0 !important; | |
| }} | |
| .left-col {{ | |
| border-right: 1px solid rgba(40, 100, 190, 0.18) !important; | |
| }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| COLUMN HEADING | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .col-head {{ | |
| font-size: 9px; | |
| font-weight: 600; | |
| letter-spacing: 0.24em; | |
| color: #6a9ec0; | |
| text-transform: uppercase; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid rgba(50, 110, 190, 0.22); | |
| margin-bottom: 18px; | |
| }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| IMAGE UPLOAD ZONE | |
| Target broadly since Gradio 6.x class names can vary | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .upload-zone {{ | |
| margin-bottom: 14px !important; | |
| flex-shrink: 0 !important; | |
| }} | |
| /* The inner image container */ | |
| .upload-zone .image-container, | |
| .upload-zone .upload-area, | |
| .upload-zone [data-testid="image"], | |
| .upload-zone .wrap {{ | |
| border: 1px dashed rgba(50, 110, 200, 0.50) !important; | |
| border-radius: 6px !important; | |
| background: rgba(2, 6, 16, 0.88) !important; | |
| min-height: 220px !important; | |
| display: flex !important; | |
| align-items: center !important; | |
| justify-content: center !important; | |
| transition: border-color 0.2s ease, background 0.2s ease !important; | |
| }} | |
| .upload-zone .wrap:hover, | |
| .upload-zone .image-container:hover {{ | |
| border-color: rgba(70, 150, 240, 0.60) !important; | |
| background: rgba(2, 6, 18, 0.92) !important; | |
| }} | |
| /* Upload text / icon tones */ | |
| .upload-zone .wrap > p, | |
| .upload-zone .upload-text, | |
| .upload-zone .or {{ | |
| color: #7aaacc !important; | |
| font-family: 'Inter', sans-serif !important; | |
| font-size: 12px !important; | |
| font-weight: 300 !important; | |
| letter-spacing: 0.06em !important; | |
| }} | |
| .upload-zone svg {{ color: #3c6088 !important; opacity: 0.90 !important; }} | |
| /* Toolbar below image */ | |
| .upload-zone .toolbar {{ | |
| background: rgba(2, 5, 14, 0.88) !important; | |
| border: none !important; | |
| border-top: 1px solid rgba(40, 90, 160, 0.18) !important; | |
| }} | |
| .upload-zone .toolbar button {{ | |
| color: #4a7090 !important; | |
| background: transparent !important; | |
| border: none !important; | |
| }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| UPLOAD METADATA STRIP | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .upload-meta {{ | |
| padding: 12px 16px; | |
| background: rgba(2, 6, 16, 0.88); | |
| border: 1px solid rgba(40, 90, 160, 0.30); | |
| border-radius: 6px; | |
| margin-bottom: 14px; | |
| }} | |
| .meta-strip-row {{ | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| font-size: 10px; | |
| font-weight: 300; | |
| letter-spacing: 0.05em; | |
| color: #8ab8d0; | |
| line-height: 1.5; | |
| }} | |
| .meta-strip-row:last-child {{ margin-bottom: 0; }} | |
| .meta-strip-dot {{ | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| margin-top: 4px; | |
| }} | |
| .meta-strip-key {{ | |
| font-size: 9px; | |
| font-weight: 600; | |
| letter-spacing: 0.16em; | |
| color: #4e7292; | |
| text-transform: uppercase; | |
| flex-shrink: 0; | |
| padding-top: 1px; | |
| min-width: 68px; | |
| }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ANALYZE BUTTON β secondary variant so custom CSS always wins | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .gradio-container .analyze-btn button, | |
| .gradio-container .analyze-btn > button, | |
| .gradio-container .analyze-btn * button, | |
| .gradio-container .analyze-btn button:focus {{ | |
| width: 100% !important; | |
| min-height: 46px !important; | |
| background: #0d2a50 !important; | |
| border: 1px solid #3a80cc !important; | |
| color: #e0f0ff !important; | |
| font-family: 'Inter', sans-serif !important; | |
| font-size: 10px !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0.22em !important; | |
| text-transform: uppercase !important; | |
| border-radius: 5px !important; | |
| padding: 13px 0 !important; | |
| transition: all 0.20s ease !important; | |
| cursor: pointer !important; | |
| box-shadow: none !important; | |
| opacity: 1 !important; | |
| text-shadow: none !important; | |
| filter: none !important; | |
| }} | |
| .gradio-container .analyze-btn button:hover, | |
| .gradio-container .analyze-btn button:active {{ | |
| background: #113260 !important; | |
| border-color: #4a9cee !important; | |
| color: #f0f8ff !important; | |
| box-shadow: 0 0 24px rgba(50, 140, 230, 0.22) !important; | |
| opacity: 1 !important; | |
| }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CLEAR BUTTON β catches ALL Gradio secondary buttons | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .clear-btn button, | |
| .clear-btn button:focus, | |
| .gradio-container button.secondary, | |
| .gradio-container button[class*="secondary"], | |
| .gradio-container button[style*="background"] {{ | |
| width: 100% !important; | |
| background: transparent !important; | |
| border: 1px solid rgba(60, 100, 150, 0.50) !important; | |
| color: #7aaccc !important; | |
| font-family: 'Inter', sans-serif !important; | |
| font-size: 9.5px !important; | |
| font-weight: 400 !important; | |
| letter-spacing: 0.16em !important; | |
| text-transform: uppercase !important; | |
| border-radius: 5px !important; | |
| padding: 9px 0 !important; | |
| margin-top: 10px !important; | |
| transition: all 0.15s ease !important; | |
| cursor: pointer !important; | |
| }} | |
| .clear-btn button:hover, | |
| .gradio-container button.secondary:hover {{ | |
| border-color: rgba(80, 130, 190, 0.60) !important; | |
| color: #80a8c8 !important; | |
| background: rgba(20, 40, 70, 0.30) !important; | |
| }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PLACEHOLDER STATE | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .placeholder-wrap {{ | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 320px; | |
| text-align: center; | |
| padding: 24px; | |
| border: 1px dashed rgba(40, 90, 160, 0.22); | |
| border-radius: 6px; | |
| background: rgba(2, 5, 14, 0.70); | |
| }} | |
| .placeholder-icon {{ | |
| font-size: 28px; | |
| color: #203a54; | |
| margin-bottom: 16px; | |
| line-height: 1; | |
| }} | |
| .placeholder-title {{ | |
| font-size: 11px; | |
| font-weight: 600; | |
| letter-spacing: 0.22em; | |
| color: #5a8aaa; | |
| text-transform: uppercase; | |
| margin-bottom: 12px; | |
| }} | |
| .placeholder-sub {{ | |
| font-size: 11.5px; | |
| font-weight: 300; | |
| color: #5a7a96; | |
| line-height: 1.85; | |
| max-width: 260px; | |
| }} | |
| .placeholder-sub em {{ | |
| color: #7aaccc; | |
| font-style: normal; | |
| font-weight: 500; | |
| }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ANALYSIS REPORT | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .report {{ font-family: 'Inter', sans-serif; }} | |
| /* Report header: scan metadata + badge */ | |
| .report-head {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| margin-bottom: 18px; | |
| padding-bottom: 14px; | |
| border-bottom: 1px solid rgba(40, 90, 160, 0.22); | |
| }} | |
| .report-meta {{ display: flex; flex-direction: column; gap: 5px; }} | |
| .meta-row {{ | |
| display: flex; | |
| gap: 10px; | |
| align-items: baseline; | |
| }} | |
| .meta-key {{ | |
| font-size: 8px; | |
| font-weight: 600; | |
| letter-spacing: 0.18em; | |
| color: #5a8aaa; | |
| text-transform: uppercase; | |
| width: 70px; | |
| flex-shrink: 0; | |
| }} | |
| .meta-val {{ | |
| font-size: 10px; | |
| font-weight: 400; | |
| color: #8ec0e0; | |
| letter-spacing: 0.04em; | |
| }} | |
| .meta-val.mono {{ | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 9.5px; | |
| color: #90c8e0; | |
| letter-spacing: 0.05em; | |
| }} | |
| .report-badge {{ | |
| font-size: 8.5px; | |
| font-weight: 700; | |
| letter-spacing: 0.22em; | |
| text-transform: uppercase; | |
| padding: 5px 12px; | |
| border-radius: 3px; | |
| border: 1px solid; | |
| flex-shrink: 0; | |
| }} | |
| /* Section heading */ | |
| .section-label {{ | |
| font-size: 8px; | |
| font-weight: 600; | |
| letter-spacing: 0.22em; | |
| color: #5a8aaa; | |
| text-transform: uppercase; | |
| margin-bottom: 10px; | |
| margin-top: 20px; | |
| padding-bottom: 6px; | |
| border-bottom: 1px solid rgba(50, 100, 180, 0.22); | |
| }} | |
| .section-label:first-of-type {{ margin-top: 0; }} | |
| /* Primary finding card */ | |
| .finding-card {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| padding: 16px 18px; | |
| background: rgba(2, 6, 18, 0.88); | |
| border: 1px solid rgba(50, 110, 200, 0.28); | |
| border-radius: 6px; | |
| }} | |
| .finding-left {{ flex-shrink: 0; }} | |
| .finding-right {{ flex: 1; min-width: 0; }} | |
| .finding-name {{ | |
| font-size: 22px; | |
| font-weight: 300; | |
| letter-spacing: 0.06em; | |
| line-height: 1.1; | |
| margin-bottom: 7px; | |
| }} | |
| .finding-conf {{ | |
| font-size: 11px; | |
| font-weight: 300; | |
| color: #6a90ac; | |
| letter-spacing: 0.04em; | |
| }} | |
| .finding-conf .mono {{ | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 10.5px; | |
| font-weight: 500; | |
| }} | |
| /* Clinical impression */ | |
| .clinical-text {{ | |
| font-size: 11.5px; | |
| font-weight: 300; | |
| color: #a0c8e0; | |
| line-height: 1.82; | |
| letter-spacing: 0.025em; | |
| }} | |
| /* Differential table */ | |
| .diff-table {{ display: flex; flex-direction: column; gap: 10px; }} | |
| .diff-row {{ | |
| display: grid; | |
| grid-template-columns: 136px 1fr 52px; | |
| align-items: center; | |
| gap: 12px; | |
| }} | |
| .diff-name {{ | |
| font-size: 10.5px; | |
| font-weight: 300; | |
| color: #6a8aaa; | |
| letter-spacing: 0.04em; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| }} | |
| .diff-primary .diff-name {{ | |
| color: #98c0d8 !important; | |
| font-weight: 500 !important; | |
| }} | |
| .diff-bar-wrap {{ | |
| height: 4px; | |
| background: rgba(12, 28, 55, 0.95); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| }} | |
| .diff-bar {{ | |
| height: 100%; | |
| border-radius: 2px; | |
| transition: width 0.7s cubic-bezier(0.4, 0, 0.2, 1); | |
| }} | |
| .diff-pct {{ | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 9.5px; | |
| font-weight: 500; | |
| text-align: right; | |
| white-space: nowrap; | |
| }} | |
| .pct-unit {{ font-size: 7.5px; opacity: 0.65; }} | |
| /* Disclaimer */ | |
| .report-footer {{ | |
| margin-top: 20px; | |
| padding: 10px 14px; | |
| background: rgba(25, 16, 4, 0.55); | |
| border: 1px solid rgba(120, 80, 20, 0.32); | |
| border-radius: 4px; | |
| font-size: 9.5px; | |
| font-weight: 300; | |
| color: #907040; | |
| line-height: 1.65; | |
| letter-spacing: 0.025em; | |
| }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GRADIO LABELS β hide default labels (we handle our own) | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .gradio-container label > span, | |
| .gradio-container .label-wrap span {{ display: none !important; }} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SCROLLBAR | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| ::-webkit-scrollbar {{ width: 3px; }} | |
| ::-webkit-scrollbar-track {{ background: transparent; }} | |
| ::-webkit-scrollbar-thumb {{ background: rgba(50, 100, 160, 0.28); border-radius: 2px; }} | |
| """ | |
| with gr.Blocks() as demo: | |
| # ββ System header βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.HTML(""" | |
| <div class="sys-header"> | |
| <div> | |
| <div class="sys-title">Lung Disease Detection System</div> | |
| <div class="sys-subtitle"> | |
| Chest Radiograph Analysis Β· CNN Classifier Β· v1.0 | |
| </div> | |
| </div> | |
| <div class="sys-indicators"> | |
| <div class="sys-dot ready">Model Ready</div> | |
| <div class="sys-dot cpu">CPU</div> | |
| </div> | |
| </div> | |
| """) | |
| # ββ Main row: two columns βββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Row(elem_classes=["main-row"], equal_height=True): | |
| # Left column: upload | |
| with gr.Column(elem_classes=["left-col"], scale=1): | |
| gr.HTML('<div class="col-head">Input β Chest Radiograph</div>') | |
| image_input = gr.Image( | |
| type="pil", | |
| label="Upload", | |
| show_label=False, | |
| elem_classes=["upload-zone"], | |
| height=240, | |
| ) | |
| gr.HTML(""" | |
| <div class="upload-meta"> | |
| <div class="meta-strip-row"> | |
| <div class="meta-strip-dot" style="background:#3dd68c;box-shadow:0 0 5px #3dd68c88;"></div> | |
| <span class="meta-strip-key">Detects</span> | |
| COVID-19, Normal, Pneumonia, Lung Opacity | |
| </div> | |
| <div class="meta-strip-row"> | |
| <div class="meta-strip-dot" style="background:#4fa3f7;box-shadow:0 0 5px #4fa3f788;"></div> | |
| <span class="meta-strip-key">Modality</span> | |
| PA or AP Chest Radiograph | |
| </div> | |
| <div class="meta-strip-row"> | |
| <div class="meta-strip-dot" style="background:#8898a8;"></div> | |
| <span class="meta-strip-key">Model</span> | |
| CNN Β· 128Γ128 input Β· 4-class softmax | |
| </div> | |
| </div> | |
| """) | |
| analyze_btn = gr.Button( | |
| "Run Analysis", | |
| variant="secondary", | |
| elem_classes=["analyze-btn"], | |
| ) | |
| # Right column: results | |
| with gr.Column(elem_classes=["right-col"], scale=1): | |
| gr.HTML('<div class="col-head">Analysis Report</div>') | |
| result_display = gr.HTML(value=PLACEHOLDER) | |
| clear_btn = gr.Button( | |
| "Clear Study", | |
| variant="secondary", | |
| elem_classes=["clear-btn"], | |
| visible=False, | |
| ) | |
| # ββ Event handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| analyze_btn.click( | |
| fn=predict, | |
| inputs=[image_input], | |
| outputs=[result_display, clear_btn], | |
| ) | |
| def clear(): | |
| return None, gr.update(value=PLACEHOLDER), gr.update(visible=False) | |
| clear_btn.click(fn=clear, inputs=[], outputs=[image_input, result_display, clear_btn]) | |
| _port = int(os.environ.get("PORT", 7860)) | |
| _dev_domain = os.environ.get("REPLIT_DEV_DOMAIN", "") | |
| _root_path = f"https://{_dev_domain}:{_port}" if _dev_domain else "" | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=_port, | |
| root_path=_root_path, | |
| css=css, | |
| theme=gr.themes.Base(), | |
| strict_cors=False, | |
| ) | |