from __future__ import annotations import html import traceback import gradio as gr from PIL import Image from src.config import CLASS_DISPLAY_NAMES, CLASS_NAMES from src.modeling import predict, weighted_ensemble_cam CUSTOM_CSS = """ /* ── Design tokens ─────────────────────────────────────────────────── */ :root { --bg: #080c14; --surface: #0f1520; --surface-2: #151d2c; --border: rgba(255,255,255,.08); --border-2: rgba(255,255,255,.13); --text: #f1f5f9; --text-2: #8a96aa; --text-3: #5a6478; --accent: #7c6fff; --accent-2: #40c4ff; --radius: 14px; --radius-sm: 8px; --font: "DM Sans", ui-sans-serif, system-ui, sans-serif; --font-mono: "DM Mono", ui-monospace, monospace; } /* ── Base ──────────────────────────────────────────────────────────── */ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap'); html, body, .gradio-container { background: var(--bg) !important; color: var(--text) !important; font-family: var(--font) !important; } .gradio-container { max-width: 1180px !important; margin: 0 auto !important; padding: 0 20px !important; } /* Kill Gradio's default chrome */ .gradio-container .block, .gradio-container .form, .gradio-container .panel, .gradio-container .wrap, .gradio-container .gr-box { background: transparent !important; border: none !important; box-shadow: none !important; } .gradio-container label, .gradio-container label span, .gradio-container .info, .gradio-container p, .gradio-container span { color: var(--text-2) !important; font-family: var(--font) !important; } /* ── Hero ──────────────────────────────────────────────────────────── */ .hero { padding: 36px 0 28px; border-bottom: 1px solid var(--border); margin-bottom: 28px; } .hero h1 { margin: 0 !important; font-size: 2rem !important; font-weight: 700 !important; letter-spacing: -0.03em !important; color: var(--text) !important; } .hero p { margin: 4px 0 0 !important; font-size: .95rem !important; color: var(--text-2) !important; } .chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; } .chip { display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border-radius: 99px; font-size: .8rem; font-weight: 500; background: var(--surface-2); border: 1px solid var(--border-2); color: var(--text-2); } .chip:first-child { background: rgba(124,111,255,.15); border-color: rgba(124,111,255,.4); color: #b3acff; } /* ── Card ──────────────────────────────────────────────────────────── */ .card { background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; padding: 20px !important; } .card + .card { margin-top: 12px; } .card-label { font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-3); margin-bottom: 14px; } /* ── Upload / heatmap images ───────────────────────────────────────── */ #mri_upload, #heatmap_preview { background: transparent !important; border: 0 !important; box-shadow: none !important; width: 100% !important; } #mri_upload > div, #mri_upload .wrap, #mri_upload .block, #mri_upload .form, #heatmap_preview > div, #heatmap_preview .wrap, #heatmap_preview .block, #heatmap_preview .form { background: transparent !important; border: 0 !important; box-shadow: none !important; padding: 0 !important; width: 100% !important; overflow: visible !important; } #mri_upload .image-container, #mri_upload .upload-container, #mri_upload [data-testid="image"], #mri_upload .empty, #heatmap_preview .image-container, #heatmap_preview .upload-container, #heatmap_preview [data-testid="image"], #heatmap_preview .empty { width: 100% !important; border-radius: var(--radius) !important; background: var(--surface-2) !important; border: 1px dashed var(--border-2) !important; box-shadow: none !important; } #mri_upload .image-container, #mri_upload .upload-container, #mri_upload [data-testid="image"], #mri_upload .empty { min-height: 420px !important; height: 420px !important; } #heatmap_preview .image-container, #heatmap_preview .upload-container, #heatmap_preview [data-testid="image"], #heatmap_preview .empty { min-height: 240px !important; height: 240px !important; } #mri_upload img, #heatmap_preview img { width: 100% !important; height: 100% !important; object-fit: contain !important; border-radius: var(--radius) !important; background: var(--surface-2) !important; } /* Extra bottom padding on upload card so source buttons are never clipped */ #upload_card { padding-bottom: 32px !important; } /* ── Upload / paste source buttons ─────────────────────────────────── */ /* Tab strip container */ #mri_upload .tabs, #mri_upload [role="tablist"], #mri_upload .tab-nav { display: flex !important; flex-direction: row !important; gap: 8px !important; padding: 0 0 12px 0 !important; background: transparent !important; border: none !important; opacity: 1 !important; visibility: visible !important; position: relative !important; z-index: 30 !important; } /* Each tab / icon button */ #mri_upload [role="tab"], #mri_upload .tab-nav button, #mri_upload .tabs button, #mri_upload button.tab-button { display: inline-flex !important; align-items: center !important; justify-content: center !important; min-width: 44px !important; min-height: 44px !important; padding: 10px 12px !important; border-radius: var(--radius-sm) !important; background: #1e2d45 !important; border: 1px solid rgba(255,255,255,.18) !important; color: #e2e8f0 !important; font-size: .85rem !important; font-weight: 500 !important; opacity: 1 !important; visibility: visible !important; z-index: 30 !important; cursor: pointer !important; box-shadow: 0 2px 8px rgba(0,0,0,.35) !important; transition: background .15s !important; } #mri_upload [role="tab"]:hover, #mri_upload .tabs button:hover { background: #283d57 !important; } /* Force SVG icons inside the buttons to be white/light */ #mri_upload [role="tab"] svg, #mri_upload .tabs button svg, #mri_upload button svg { color: #e2e8f0 !important; fill: #e2e8f0 !important; stroke: #e2e8f0 !important; width: 20px !important; height: 20px !important; opacity: 1 !important; } /* Active / selected tab */ #mri_upload [role="tab"][aria-selected="true"], #mri_upload [role="tab"].selected { background: var(--accent) !important; border-color: var(--accent) !important; color: #fff !important; } #mri_upload [role="tab"][aria-selected="true"] svg, #mri_upload [role="tab"].selected svg { color: #fff !important; fill: #fff !important; stroke: #fff !important; } /* ── Checkbox ──────────────────────────────────────────────────────── */ #heatmap_checkbox { margin-top: 12px !important; } #heatmap_checkbox label { display: flex !important; align-items: center !important; gap: 10px !important; padding: 12px 14px !important; border-radius: var(--radius-sm) !important; background: var(--surface-2) !important; border: 1px solid var(--border) !important; min-height: auto !important; } #heatmap_checkbox input[type="checkbox"] { appearance: none !important; -webkit-appearance: none !important; width: 20px !important; height: 20px !important; min-width: 20px !important; border-radius: 6px !important; background: var(--bg) !important; border: 1.5px solid var(--border-2) !important; cursor: pointer !important; } #heatmap_checkbox input[type="checkbox"]:checked { background-color: var(--accent) !important; border-color: var(--accent) !important; background-image: url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.2 10.4L8.1 14.2L15.9 5.8' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important; background-repeat: no-repeat !important; background-position: center !important; } #heatmap_checkbox span { color: var(--text) !important; font-size: .88rem !important; font-weight: 500 !important; } /* ── Run button ────────────────────────────────────────────────────── */ #run_button button { width: 100% !important; margin-top: 10px !important; height: 44px !important; border-radius: var(--radius-sm) !important; background: var(--accent) !important; color: #fff !important; font-weight: 600 !important; font-size: .92rem !important; border: none !important; box-shadow: 0 0 0 1px rgba(124,111,255,.3), 0 4px 16px rgba(124,111,255,.2) !important; transition: opacity .15s !important; letter-spacing: -.01em !important; } #run_button button:hover { opacity: .88 !important; } /* ── Result card ───────────────────────────────────────────────────── */ .result-card { background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; padding: 22px 24px !important; margin-bottom: 12px; } .pred-eyebrow { font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .1em; color: var(--text-3); margin-bottom: 8px; } .pred-label { font-size: 2.2rem; font-weight: 700; letter-spacing: -0.045em; color: var(--text); line-height: 1; margin-bottom: 6px; } .pred-sub { font-size: .85rem; color: var(--text-2); margin-bottom: 20px; } .pred-sub b { color: #b3acff; font-weight: 600; } /* Metric row — always 3 columns */ .metric-grid { display: grid !important; grid-template-columns: repeat(3, 1fr) !important; gap: 10px !important; } .metric { padding: 12px 14px; border-radius: var(--radius-sm); background: var(--surface-2); border: 1px solid var(--border); } .metric .k { font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-3); margin-bottom: 5px; } .metric .v { font-size: 1.05rem; font-weight: 700; color: var(--text); font-family: var(--font-mono); } .metric .v.confidence { color: #b3acff; } /* ── Probability card ──────────────────────────────────────────────── */ .prob-card { background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; padding: 22px 24px !important; margin-bottom: 12px; } .prob-title { font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .1em; color: var(--text-3); margin-bottom: 16px; } .prob-item { margin-bottom: 14px; } .prob-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; } .prob-label { font-size: .88rem; font-weight: 500; color: var(--text-2); } .prob-item.top .prob-label { color: var(--text); } .prob-percent { font-size: .82rem; font-weight: 600; color: var(--text-3); font-family: var(--font-mono); } .prob-item.top .prob-percent { color: #b3acff; } .prob-track { height: 4px; border-radius: 99px; background: var(--surface-2); overflow: hidden; } .prob-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); } /* ── Details card ──────────────────────────────────────────────────── */ .details-card { background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; padding: 22px 24px !important; margin-bottom: 12px; } .details-title { font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .1em; color: var(--text-3); margin-bottom: 6px; } .details-subtitle { font-size: .82rem; color: var(--text-3); margin-bottom: 18px; line-height: 1.5; } /* Member row — 4-column grid */ .member-row { display: grid !important; grid-template-columns: 1.8fr 1fr 1fr 1fr !important; gap: 10px !important; align-items: start !important; padding: 14px !important; border-radius: var(--radius-sm) !important; background: var(--surface-2) !important; border: 1px solid var(--border) !important; margin-bottom: 10px !important; } .member-name { font-size: .9rem; font-weight: 600; color: var(--text); line-height: 1.3; } .member-meta { margin-top: 4px; font-size: .78rem; color: var(--text-3); } .detail-pill { padding: 10px 12px; border-radius: var(--radius-sm); background: var(--bg); border: 1px solid var(--border); } .detail-key { font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-3); margin-bottom: 4px; } .detail-value { font-size: .9rem; font-weight: 600; color: var(--text); font-family: var(--font-mono); } .detail-value.accent { color: #b3acff; } .vote-track { margin-top: 6px; height: 3px; border-radius: 99px; background: rgba(255,255,255,.08); overflow: hidden; } .vote-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); } /* ── Responsive ────────────────────────────────────────────────────── */ @media (max-width: 860px) { .metric-grid { grid-template-columns: repeat(3, 1fr) !important; } .member-row { grid-template-columns: 1fr 1fr !important; } } @media (max-width: 560px) { .metric-grid { grid-template-columns: 1fr 1fr !important; } .member-row { grid-template-columns: 1fr !important; } .pred-label { font-size: 1.7rem; } } """ CLASS_CHIPS_HTML = "".join( f"{html.escape(CLASS_DISPLAY_NAMES[name])}" for name in CLASS_NAMES ) HERO_HTML = f"""
Brain MRI Ensemble Classifier · EfficientNet-B0 + MobileNetV3-Small