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 = """
System Status
Model
READY
Mode
CPU
Classes
4
Input Size
128×128
Detection Classes
COVID-19 CLASS 1
Normal CLASS 2
Viral Pneumonia CLASS 3
Lung Opacity CLASS 4
Upload a chest radiograph on the left, then press Run Analysis to generate a structured diagnostic report.
""" 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""" {conf:.0f}% """ # 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"""
{cls}
{pct:.1f}%
""" html = f"""
Scan ID {scan_id}
Date/Time {timestamp}
Dimensions {dims} px
Modality CXR
{tag}
Primary Finding
{ring_svg}
{label}
Model confidence: {conf:.2f}%
Clinical Impression
{desc}
Differential Diagnosis
{bars}
⚠️ 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.
""" 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("""
Lung Disease Detection System
Chest Radiograph Analysis  ·  CNN Classifier  ·  v1.0
Model Ready
CPU
""") # ── 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('
Input — Chest Radiograph
') image_input = gr.Image( type="pil", label="Upload", show_label=False, elem_classes=["upload-zone"], height=240, ) gr.HTML("""
Detects COVID-19, Normal, Pneumonia, Lung Opacity
Modality PA or AP Chest Radiograph
Model CNN  ·  128×128 input  ·  4-class softmax
""") 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('
Analysis Report
') 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, )