Spaces:
Runtime error
Runtime error
Download src/ui.py from XDL-Collitis/XDL-Colitis-Demo: direct link, hf CLI and curl.
- Browser
- Download file 27.1 kB
-
https://huggingface.co/spaces/XDL-Collitis/XDL-Colitis-Demo/resolve/main/src/ui.py
- Command line
-
hf download hf://spaces/XDL-Collitis/XDL-Colitis-Demo/src/ui.py
-
curl -L -o ui.py https://huggingface.co/spaces/XDL-Collitis/XDL-Colitis-Demo/resolve/main/src/ui.py
27.1 kB
| import json | |
| from pathlib import Path | |
| from typing import Any | |
| import gradio as gr | |
| from src.inference import ( | |
| CASE_OPTIONS, | |
| DEFAULT_CASE_NAME, | |
| DEFAULT_GRADCAM_TARGET_LAYER, | |
| DEVICE, | |
| GRADCAM_TARGET_LAYER_OPTIONS, | |
| batch_predict_with_xdl_stream, | |
| ) | |
| from src.utils import clean_drive_downloads, normalize_uploaded_paths | |
| UI_DEFAULTS_PATH = Path(__file__).resolve().parent.parent / "config" / "ui_defaults.json" | |
| UI_DEFAULTS_FALLBACK = { | |
| "selected_case": DEFAULT_CASE_NAME, | |
| "confidence_threshold": 0.60, | |
| "smoothgrad_samples": 50, | |
| "smoothgrad_noise": 0.05, | |
| "gradcam_target_layer": DEFAULT_GRADCAM_TARGET_LAYER, | |
| "save_xdl_results": False, | |
| "save_xdl_dir": "xdl_results", | |
| } | |
| GRADCAM_TARGET_LAYER_DROPDOWN_CHOICES = [ | |
| ("DenseBlock 3 (Default, balanced)", "denseblock3"), | |
| ("Transition 2 (Broad, stable)", "transition2"), | |
| ("Transition 1 (Earlier, detailed/noisier)", "transition1"), | |
| ("DenseBlock 4 (Late, center-heavy)", "denseblock4"), | |
| ("Transition 3 (Late, center-heavy)", "transition3"), | |
| ("Norm5 Last (Legacy behavior)", "norm5_last"), | |
| ] | |
| CUSTOM_CSS = """ | |
| .app-shell { | |
| --xdl-bg: #ffffff; | |
| --xdl-surface: #f8fafc; | |
| --xdl-text: #0f172a; | |
| --xdl-muted: #475569; | |
| --xdl-border: #d1d5db; | |
| --xdl-primary: #0f766e; | |
| --xdl-primary-strong: #0f4f4a; | |
| --xdl-accent: #0ea5e9; | |
| --xdl-success-bg: #dcfce7; | |
| --xdl-success-text: #166534; | |
| --xdl-warning-bg: #fef3c7; | |
| --xdl-warning-text: #92400e; | |
| max-width: 1120px; | |
| margin: 0 auto; | |
| color: var(--xdl-text); | |
| } | |
| .hero { | |
| border: 1px solid var(--xdl-border); | |
| background: linear-gradient(135deg, #f0fdfa 0%, #ecfeff 44%, #ffffff 100%); | |
| border-radius: 18px; | |
| padding: 20px 22px; | |
| margin-bottom: 12px; | |
| color: var(--xdl-text); | |
| box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08); | |
| } | |
| .hero-top { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 18px; | |
| align-items: flex-start; | |
| } | |
| .hero-kicker { | |
| color: var(--xdl-primary); | |
| font-size: 12px; | |
| font-weight: 900; | |
| letter-spacing: 0.12em; | |
| margin-bottom: 6px; | |
| text-transform: uppercase; | |
| } | |
| .hero h1 { | |
| margin: 0; | |
| font-size: 30px; | |
| line-height: 1.1; | |
| color: var(--xdl-text); | |
| } | |
| .hero p { | |
| margin: 8px 0 0 0; | |
| color: #334155; | |
| font-size: 15px; | |
| line-height: 1.5; | |
| max-width: 720px; | |
| } | |
| .hero-pills { | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: flex-end; | |
| gap: 8px; | |
| min-width: 220px; | |
| } | |
| .hero-pill { | |
| border: 1px solid #99f6e4; | |
| border-radius: 999px; | |
| background: #ffffff; | |
| color: var(--xdl-primary-strong); | |
| font-size: 12px; | |
| font-weight: 900; | |
| padding: 7px 10px; | |
| white-space: nowrap; | |
| } | |
| .panel { | |
| border: 1px solid #e2e8f0; | |
| border-radius: 12px; | |
| background: var(--xdl-bg); | |
| color: var(--xdl-text); | |
| padding: 12px; | |
| } | |
| .dashboard-shell { | |
| align-items: flex-start; | |
| flex-direction: row-reverse; | |
| gap: 16px; | |
| } | |
| .sidebar-panel { | |
| border-radius: 20px; | |
| padding: 16px; | |
| position: sticky; | |
| top: 12px; | |
| } | |
| .workspace-panel { | |
| border-radius: 20px; | |
| padding: 18px; | |
| min-height: 680px; | |
| } | |
| .control-section { | |
| border: 1px solid #e2e8f0; | |
| border-radius: 15px; | |
| background: #f8fafc; | |
| margin-bottom: 12px; | |
| padding: 12px; | |
| } | |
| .control-section-title { | |
| color: var(--xdl-primary-strong); | |
| font-size: 12px; | |
| font-weight: 900; | |
| letter-spacing: 0.08em; | |
| margin-bottom: 10px; | |
| text-transform: uppercase; | |
| } | |
| .workspace-title { | |
| display: flex; | |
| align-items: center !important; | |
| justify-content: space-between; | |
| gap: 14px; | |
| margin-bottom: 14px; | |
| } | |
| .workspace-title-copy { | |
| display: flex; | |
| align-items: center; | |
| flex: 1 1 auto; | |
| min-height: 36px; | |
| min-width: 0; | |
| } | |
| .workspace-title h2 { | |
| color: var(--xdl-text); | |
| font-size: 20px; | |
| line-height: 1.2; | |
| margin: 0; | |
| } | |
| .presentation-toggle { | |
| align-self: center !important; | |
| flex: 0 0 auto !important; | |
| min-width: fit-content; | |
| } | |
| .presentation-toggle button { | |
| align-items: center !important; | |
| border: 1px solid #99f6e4 !important; | |
| border-radius: 999px !important; | |
| background: linear-gradient(135deg, #ffffff, #f0fdfa) !important; | |
| color: var(--xdl-primary-strong) !important; | |
| display: inline-flex !important; | |
| font-size: 12px !important; | |
| font-weight: 900 !important; | |
| justify-content: center !important; | |
| line-height: 1 !important; | |
| margin: 0 !important; | |
| min-height: 36px !important; | |
| padding: 0 14px !important; | |
| white-space: nowrap; | |
| } | |
| .presentation-toggle button:hover { | |
| border-color: var(--xdl-primary) !important; | |
| box-shadow: 0 8px 20px rgba(15, 118, 110, 0.16) !important; | |
| } | |
| .empty-state { | |
| border: 1px dashed #cbd5e1; | |
| border-radius: 18px; | |
| background: linear-gradient(135deg, #ffffff, #f8fafc); | |
| color: var(--xdl-muted); | |
| padding: 28px; | |
| text-align: center; | |
| } | |
| .empty-state-title { | |
| color: var(--xdl-text); | |
| font-size: 18px; | |
| font-weight: 900; | |
| margin-bottom: 8px; | |
| } | |
| .empty-state-copy { | |
| font-size: 14px; | |
| line-height: 1.5; | |
| margin: 0 auto; | |
| max-width: 520px; | |
| } | |
| .summary-card { | |
| border: 1px solid #d1d5db; | |
| border-radius: 18px; | |
| background: #ffffff; | |
| color: var(--xdl-text); | |
| padding: 16px; | |
| box-shadow: 0 14px 35px rgba(15, 23, 42, 0.06); | |
| } | |
| .summary-title { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 10px; | |
| margin-bottom: 14px; | |
| } | |
| .summary-title h3 { | |
| color: var(--xdl-text); | |
| font-size: 18px; | |
| margin: 0; | |
| } | |
| .summary-title span { | |
| border-radius: 999px; | |
| background: #ecfeff; | |
| color: var(--xdl-primary-strong); | |
| font-size: 12px; | |
| font-weight: 900; | |
| padding: 6px 10px; | |
| } | |
| .metric-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-bottom: 12px; | |
| } | |
| .metric-card { | |
| border: 1px solid #e2e8f0; | |
| border-radius: 14px; | |
| background: #f8fafc; | |
| padding: 12px; | |
| } | |
| .metric-label { | |
| color: #64748b; | |
| font-size: 12px; | |
| font-weight: 800; | |
| margin-bottom: 6px; | |
| } | |
| .metric-value { | |
| color: var(--xdl-text); | |
| font-size: 20px; | |
| font-weight: 900; | |
| line-height: 1.15; | |
| word-break: break-word; | |
| } | |
| .metric-value.small { | |
| font-size: 16px; | |
| } | |
| .summary-details { | |
| border-top: 1px solid #e5e7eb; | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 8px 16px; | |
| padding-top: 12px; | |
| } | |
| .summary-detail { | |
| color: #334155; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| } | |
| .summary-detail b { | |
| color: var(--xdl-text); | |
| } | |
| .panel :is(h1, h2, h3, h4, h5, h6, p, label, span, th, td) { | |
| color: inherit; | |
| } | |
| .panel .secondary-wrap, | |
| .panel .prose, | |
| .panel .markdown { | |
| color: var(--xdl-text); | |
| } | |
| .xdl-report { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| color: var(--xdl-text); | |
| } | |
| .processing-status { | |
| border: 1px solid #dbeafe; | |
| background: #eff6ff; | |
| border-radius: 14px; | |
| color: var(--xdl-text); | |
| padding: 14px; | |
| margin: 12px 0; | |
| } | |
| .processing-status-title { | |
| font-size: 16px; | |
| font-weight: 800; | |
| margin-bottom: 10px; | |
| } | |
| .processing-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 12px; | |
| } | |
| .progress-card { | |
| border: 1px solid #bfdbfe; | |
| border-radius: 12px; | |
| background: #ffffff; | |
| color: var(--xdl-text); | |
| padding: 12px; | |
| } | |
| .progress-card-head { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 12px; | |
| align-items: center; | |
| font-size: 14px; | |
| margin-bottom: 8px; | |
| } | |
| .progress-card-head span { | |
| color: #075985; | |
| font-weight: 800; | |
| } | |
| .progress-message { | |
| color: #334155; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| margin-bottom: 10px; | |
| } | |
| .progress-track { | |
| height: 10px; | |
| border-radius: 999px; | |
| background: #dbeafe; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| border-radius: 999px; | |
| background: linear-gradient(90deg, var(--xdl-primary), var(--xdl-accent)); | |
| transition: width 180ms ease; | |
| } | |
| .xdl-guide { | |
| border: 1px solid #bae6fd; | |
| background: #f0f9ff; | |
| border-radius: 12px; | |
| padding: 14px; | |
| color: var(--xdl-text); | |
| } | |
| .xdl-guide-title { | |
| font-size: 16px; | |
| font-weight: 700; | |
| color: var(--xdl-text); | |
| margin-bottom: 10px; | |
| } | |
| .xdl-guide-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 10px; | |
| } | |
| .xdl-guide-grid div { | |
| border: 1px solid #dbeafe; | |
| background: var(--xdl-bg); | |
| color: var(--xdl-text); | |
| border-radius: 10px; | |
| padding: 10px; | |
| } | |
| .xdl-guide-grid b { | |
| display: block; | |
| color: #075985; | |
| font-size: 14px; | |
| margin-bottom: 4px; | |
| } | |
| .xdl-guide-grid span { | |
| display: block; | |
| color: #334155; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| } | |
| .xdl-report-heading { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| color: var(--xdl-text); | |
| font-size: 17px; | |
| font-weight: 700; | |
| } | |
| .xdl-report-heading span { | |
| border-radius: 999px; | |
| background: #e0f2fe; | |
| color: #075985; | |
| font-size: 12px; | |
| font-weight: 700; | |
| padding: 5px 10px; | |
| } | |
| .xdl-scroll { | |
| max-height: 820px; | |
| overflow-y: auto; | |
| padding: 2px 8px 2px 2px; | |
| } | |
| .xdl-card { | |
| border: 1px solid #cbd5e1; | |
| border-radius: 18px; | |
| background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%); | |
| color: var(--xdl-text); | |
| box-shadow: 0 16px 36px rgba(15, 23, 42, 0.09); | |
| margin-bottom: 18px; | |
| padding: 18px; | |
| } | |
| .xdl-card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 14px; | |
| align-items: flex-start; | |
| margin-bottom: 14px; | |
| } | |
| .xdl-filename { | |
| color: var(--xdl-text); | |
| font-size: 18px; | |
| font-weight: 800; | |
| word-break: break-word; | |
| } | |
| .xdl-card-subtitle { | |
| color: #64748b; | |
| font-size: 13px; | |
| margin-top: 3px; | |
| } | |
| .xdl-meta { | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: flex-end; | |
| gap: 8px; | |
| } | |
| .xdl-badge, | |
| .xdl-confidence { | |
| border-radius: 999px; | |
| font-size: 13px; | |
| font-weight: 800; | |
| padding: 7px 11px; | |
| white-space: nowrap; | |
| } | |
| .xdl-badge { | |
| background: var(--xdl-success-bg); | |
| color: var(--xdl-success-text); | |
| } | |
| .xdl-confidence { | |
| background: var(--xdl-warning-bg); | |
| color: var(--xdl-warning-text); | |
| } | |
| .xdl-index { | |
| color: var(--xdl-primary); | |
| font-size: 12px; | |
| font-weight: 900; | |
| letter-spacing: 0.08em; | |
| margin-bottom: 5px; | |
| text-transform: uppercase; | |
| } | |
| .xdl-acceptance-note { | |
| border-left: 3px solid var(--xdl-primary); | |
| background: #f0fdfa; | |
| border-radius: 10px; | |
| color: #334155; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| margin: 12px 0 14px 0; | |
| padding: 9px 11px; | |
| } | |
| .xdl-panel-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 12px; | |
| } | |
| .xdl-panel { | |
| border: 1px solid #e2e8f0; | |
| border-radius: 14px; | |
| background: var(--xdl-surface); | |
| color: var(--xdl-text); | |
| overflow: hidden; | |
| box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); | |
| } | |
| .xdl-panel-title { | |
| background: #0f172a; | |
| color: #ffffff; | |
| font-size: 13px; | |
| font-weight: 800; | |
| letter-spacing: 0.02em; | |
| padding: 8px 10px; | |
| text-align: center; | |
| } | |
| .xdl-panel img { | |
| display: block; | |
| width: 100%; | |
| height: auto; | |
| background: #ffffff; | |
| transition: transform 160ms ease, filter 160ms ease; | |
| } | |
| .xdl-image-link { | |
| color: inherit; | |
| display: block; | |
| position: relative; | |
| text-decoration: none; | |
| } | |
| .xdl-image-link:hover img { | |
| filter: saturate(1.05) contrast(1.03); | |
| transform: scale(1.01); | |
| } | |
| .xdl-image-hint { | |
| border: 1px solid rgba(255, 255, 255, 0.45); | |
| border-radius: 999px; | |
| background: rgba(15, 23, 42, 0.78); | |
| bottom: 9px; | |
| color: #ffffff !important; | |
| font-size: 11px; | |
| font-weight: 900; | |
| opacity: 0; | |
| padding: 5px 8px; | |
| pointer-events: none; | |
| position: absolute; | |
| right: 9px; | |
| transform: translateY(4px); | |
| transition: opacity 160ms ease, transform 160ms ease; | |
| } | |
| .xdl-image-link:hover .xdl-image-hint { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| .xdl-empty { | |
| border: 1px dashed #cbd5e1; | |
| border-radius: 14px; | |
| background: var(--xdl-surface); | |
| color: var(--xdl-muted); | |
| padding: 24px; | |
| text-align: center; | |
| } | |
| .xdl-empty-title { | |
| color: var(--xdl-text); | |
| font-size: 16px; | |
| font-weight: 800; | |
| margin-bottom: 6px; | |
| } | |
| .xdl-status { | |
| color: var(--xdl-muted); | |
| font-size: 13px; | |
| text-align: right; | |
| } | |
| @media (max-width: 900px) { | |
| .sidebar-panel { | |
| position: static; | |
| } | |
| .metric-grid, | |
| .summary-details { | |
| grid-template-columns: 1fr; | |
| } | |
| .hero-top { | |
| flex-direction: column; | |
| } | |
| .hero-pills { | |
| justify-content: flex-start; | |
| } | |
| .processing-grid, | |
| .xdl-guide-grid, | |
| .xdl-panel-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .xdl-card-header { | |
| flex-direction: column; | |
| } | |
| .xdl-meta { | |
| justify-content: flex-start; | |
| } | |
| } | |
| .action-stack { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .action-row { | |
| display: grid; | |
| grid-template-columns: minmax(0, 1fr) auto; | |
| gap: 8px; | |
| align-items: stretch; | |
| } | |
| .run-action button { | |
| font-weight: 900 !important; | |
| min-height: 44px; | |
| } | |
| .clean-action button { | |
| min-height: 44px; | |
| } | |
| .action-note { | |
| border: 1px solid #ccfbf1; | |
| border-radius: 10px; | |
| background: #f0fdfa; | |
| color: #0f4f4a; | |
| font-size: 12px; | |
| font-weight: 700; | |
| line-height: 1.35; | |
| padding: 9px 10px; | |
| } | |
| """ | |
| def _as_float(value: Any, fallback: float) -> float: | |
| try: | |
| return float(value) | |
| except (TypeError, ValueError): | |
| return float(fallback) | |
| def _as_int(value: Any, fallback: int) -> int: | |
| try: | |
| return int(value) | |
| except (TypeError, ValueError): | |
| return int(fallback) | |
| def _as_bool(value: Any, fallback: bool) -> bool: | |
| if isinstance(value, bool): | |
| return value | |
| if isinstance(value, str): | |
| return value.strip().lower() in {"1", "true", "yes", "y", "on"} | |
| if value is None: | |
| return fallback | |
| return bool(value) | |
| def _load_ui_defaults() -> dict[str, Any]: | |
| defaults = dict(UI_DEFAULTS_FALLBACK) | |
| try: | |
| raw_text = UI_DEFAULTS_PATH.read_text(encoding="utf-8") | |
| raw = json.loads(raw_text) | |
| if isinstance(raw, dict): | |
| for key in defaults: | |
| if key in raw: | |
| defaults[key] = raw[key] | |
| except Exception: | |
| pass | |
| selected_case = str(defaults.get("selected_case", DEFAULT_CASE_NAME)) | |
| defaults["selected_case"] = selected_case if selected_case in CASE_OPTIONS else DEFAULT_CASE_NAME | |
| defaults["confidence_threshold"] = min( | |
| 1.0, | |
| max(0.0, _as_float(defaults.get("confidence_threshold"), UI_DEFAULTS_FALLBACK["confidence_threshold"])), | |
| ) | |
| defaults["smoothgrad_samples"] = max( | |
| 1, | |
| _as_int(defaults.get("smoothgrad_samples"), UI_DEFAULTS_FALLBACK["smoothgrad_samples"]), | |
| ) | |
| defaults["smoothgrad_noise"] = min( | |
| 1.0, | |
| max(0.0, _as_float(defaults.get("smoothgrad_noise"), UI_DEFAULTS_FALLBACK["smoothgrad_noise"])), | |
| ) | |
| gradcam_target_layer = str(defaults.get("gradcam_target_layer", DEFAULT_GRADCAM_TARGET_LAYER)).strip().lower() | |
| defaults["gradcam_target_layer"] = ( | |
| gradcam_target_layer if gradcam_target_layer in GRADCAM_TARGET_LAYER_OPTIONS else DEFAULT_GRADCAM_TARGET_LAYER | |
| ) | |
| defaults["save_xdl_results"] = _as_bool(defaults.get("save_xdl_results"), UI_DEFAULTS_FALLBACK["save_xdl_results"]) | |
| defaults["save_xdl_dir"] = str(defaults.get("save_xdl_dir") or UI_DEFAULTS_FALLBACK["save_xdl_dir"]) | |
| return defaults | |
| def _toggle_save_dir(enabled: bool): | |
| is_enabled = bool(enabled) | |
| return gr.update(visible=is_enabled, interactive=is_enabled) | |
| def _toggle_presentation_view(enabled: bool): | |
| next_enabled = not bool(enabled) | |
| label = "Exit Presentation View" if next_enabled else "Enter Presentation View" | |
| variant = "primary" if next_enabled else "secondary" | |
| return next_enabled, gr.update(visible=not next_enabled), gr.update(value=label, variant=variant) | |
| def _loaded_images_from_upload(uploaded_files): | |
| image_paths = normalize_uploaded_paths(uploaded_files) | |
| names = [path.name for path in image_paths] | |
| stored_paths = [str(path) for path in image_paths] | |
| return gr.update(choices=names, value=names[0] if names else None, interactive=bool(names)), stored_paths | |
| def _empty_summary_html() -> str: | |
| return ( | |
| "<div class='empty-state'>" | |
| "<div class='empty-state-title'>No Results Yet</div>" | |
| "<div class='empty-state-copy'>Upload an image folder or enter a public Google Drive folder link, " | |
| "then run inference to see the batch diagnosis summary.</div>" | |
| "</div>" | |
| ) | |
| def _empty_processing_html() -> str: | |
| return ( | |
| "<div class='empty-state'>" | |
| "<div class='empty-state-title'>Processing Status Will Appear Here</div>" | |
| "<div class='empty-state-copy'>Input preparation, Drive download progress, and XDL generation progress " | |
| "will stream here while the model runs.</div>" | |
| "</div>" | |
| ) | |
| def _empty_xdl_html() -> str: | |
| return ( | |
| "<div class='empty-state'>" | |
| "<div class='empty-state-title'>XDL Cards Not Generated Yet</div>" | |
| "<div class='empty-state-copy'>Classified images will appear as presentation-ready Original, GradCAM, " | |
| "and SmoothGrad cards after inference completes.</div>" | |
| "</div>" | |
| ) | |
| def _reset_demo(): | |
| clean_drive_downloads() | |
| ui_defaults = _load_ui_defaults() | |
| save_enabled = bool(ui_defaults["save_xdl_results"]) | |
| return ( | |
| ui_defaults["selected_case"], | |
| None, | |
| [], | |
| "", | |
| gr.update(choices=[], value=None, interactive=False), | |
| ui_defaults["confidence_threshold"], | |
| ui_defaults["smoothgrad_samples"], | |
| ui_defaults["smoothgrad_noise"], | |
| ui_defaults["gradcam_target_layer"], | |
| ui_defaults["save_xdl_results"], | |
| gr.update( | |
| value=ui_defaults["save_xdl_dir"], | |
| visible=save_enabled, | |
| interactive=save_enabled, | |
| ), | |
| _empty_summary_html(), | |
| _empty_processing_html(), | |
| _empty_xdl_html(), | |
| [], | |
| ) | |
| def build_demo() -> gr.Blocks: | |
| ui_defaults = _load_ui_defaults() | |
| with gr.Blocks(title="XDL Colitis Demo") as demo: | |
| gr.HTML( | |
| f""" | |
| <style>{CUSTOM_CSS}</style> | |
| <div class="app-shell"> | |
| <div class="hero"> | |
| <div class="hero-top"> | |
| <div class="hero-copy"> | |
| <div class="hero-kicker">Medical AI Explanation Demo</div> | |
| <h1>XDL Colitis Workbench</h1> | |
| <p>DenseNet-based colitis classification with GradCAM and SmoothGrad explanations. Upload a directory or use a public Google Drive folder link, then run batch inference.</p> | |
| </div> | |
| <div class="hero-pills"> | |
| <span class="hero-pill">Device: {DEVICE.type}</span> | |
| <span class="hero-pill">3 Case Modes</span> | |
| <span class="hero-pill">Folder / Drive Input</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(elem_classes=["app-shell", "dashboard-shell"]): | |
| with gr.Column(scale=1, min_width=330, elem_classes=["panel", "sidebar-panel"]) as sidebar_panel: | |
| gr.HTML("<div class='control-section-title'>Data Source</div>") | |
| with gr.Group(elem_classes=["control-section"]): | |
| upload_input = gr.UploadButton( | |
| label="Select Image Folder", | |
| variant="secondary", | |
| file_count="directory", | |
| type="filepath", | |
| ) | |
| google_drive_link = gr.Textbox( | |
| label="Google Drive Link (Optional)", | |
| placeholder="Public Google Drive folder link", | |
| ) | |
| loaded_images = gr.Dropdown( | |
| choices=[], | |
| label="Loaded Images", | |
| info="Filenames detected from upload or Google Drive after processing starts.", | |
| interactive=False, | |
| ) | |
| uploaded_files_state = gr.State([]) | |
| folder_path = gr.State("") | |
| gr.HTML("<div class='control-section-title'>Model Case</div>") | |
| with gr.Group(elem_classes=["control-section"]): | |
| selected_case = gr.Dropdown( | |
| choices=CASE_OPTIONS, | |
| value=ui_defaults["selected_case"], | |
| label="Problem Case", | |
| info="Choose the model group that matches your diagnosis scenario.", | |
| ) | |
| gr.HTML("<div class='control-section-title'>Inference Settings</div>") | |
| with gr.Group(elem_classes=["control-section"]): | |
| threshold = gr.Number( | |
| value=ui_defaults["confidence_threshold"], | |
| minimum=0.0, | |
| maximum=1.0, | |
| step=0.01, | |
| precision=2, | |
| label="Confidence Threshold", | |
| info="Range: 0.00 to 1.00", | |
| ) | |
| gradcam_target_layer = gr.Dropdown( | |
| choices=GRADCAM_TARGET_LAYER_DROPDOWN_CHOICES, | |
| value=ui_defaults["gradcam_target_layer"], | |
| label="GradCAM Target Layer", | |
| info="Try `transition2` or `denseblock3` if CAM looks too centered.", | |
| ) | |
| smoothgrad_samples = gr.Number( | |
| value=ui_defaults["smoothgrad_samples"], | |
| minimum=1, | |
| maximum=1000, | |
| step=1, | |
| precision=0, | |
| label="SmoothGrad Samples", | |
| info="Higher values improve stability but increase runtime.", | |
| ) | |
| smoothgrad_noise = gr.Number( | |
| value=ui_defaults["smoothgrad_noise"], | |
| minimum=0.0, | |
| maximum=1.0, | |
| step=0.01, | |
| precision=2, | |
| label="SmoothGrad Noise Level", | |
| info="Typical range: 0.01 to 0.20", | |
| ) | |
| gr.HTML("<div class='control-section-title'>Export</div>") | |
| with gr.Group(elem_classes=["control-section"]): | |
| save_xdl_results = gr.Checkbox( | |
| label="Save XDL Results Locally", | |
| value=ui_defaults["save_xdl_results"], | |
| ) | |
| save_xdl_dir = gr.Textbox( | |
| label="Save Folder", | |
| value=ui_defaults["save_xdl_dir"], | |
| placeholder="xdl_results", | |
| visible=bool(ui_defaults["save_xdl_results"]), | |
| interactive=bool(ui_defaults["save_xdl_results"]), | |
| ) | |
| with gr.Column(elem_classes=["action-stack"]): | |
| with gr.Row(elem_classes=["action-row"]): | |
| run_btn = gr.Button( | |
| "Run / Rerun Inference", | |
| variant="primary", | |
| elem_classes=["run-action"], | |
| ) | |
| clean_btn = gr.Button("Clean", variant="secondary", elem_classes=["clean-action"]) | |
| gr.HTML( | |
| "<div class='action-note'>Runs classification first, then streams XDL overlays as each image finishes.</div>" | |
| ) | |
| with gr.Column(scale=3, elem_classes=["panel", "workspace-panel"]): | |
| with gr.Row(elem_classes=["workspace-title"]): | |
| gr.HTML( | |
| "<div class='workspace-title-copy'><h2>Results Workspace</h2></div>" | |
| ) | |
| presentation_state = gr.State(False) | |
| presentation_btn = gr.Button( | |
| "Enter Presentation View", | |
| variant="secondary", | |
| elem_classes=["presentation-toggle"], | |
| scale=0, | |
| min_width=178, | |
| ) | |
| summary_out = gr.HTML(value=_empty_summary_html(), label="Summary") | |
| processing_status_out = gr.HTML(value=_empty_processing_html(), label="Processing Status") | |
| xdl_report_out = gr.HTML(value=_empty_xdl_html(), label="Classified XDL Results") | |
| with gr.Accordion("Detailed Per-Image Table", open=False): | |
| table_out = gr.Dataframe( | |
| headers=["filename", "status", "predicted_label", "confidence_or_error"], | |
| datatype=["str", "str", "str", "str"], | |
| interactive=False, | |
| label="Per-image Results", | |
| ) | |
| save_xdl_results.change( | |
| fn=_toggle_save_dir, | |
| inputs=[save_xdl_results], | |
| outputs=[save_xdl_dir], | |
| ) | |
| upload_input.upload( | |
| fn=_loaded_images_from_upload, | |
| inputs=[upload_input], | |
| outputs=[loaded_images, uploaded_files_state], | |
| ) | |
| presentation_btn.click( | |
| fn=_toggle_presentation_view, | |
| inputs=[presentation_state], | |
| outputs=[presentation_state, sidebar_panel, presentation_btn], | |
| ) | |
| run_btn.click( | |
| fn=batch_predict_with_xdl_stream, | |
| inputs=[ | |
| uploaded_files_state, | |
| selected_case, | |
| folder_path, | |
| threshold, | |
| smoothgrad_samples, | |
| smoothgrad_noise, | |
| save_xdl_results, | |
| save_xdl_dir, | |
| gradcam_target_layer, | |
| google_drive_link, | |
| ], | |
| outputs=[summary_out, processing_status_out, xdl_report_out, loaded_images, table_out], | |
| ) | |
| clean_btn.click( | |
| fn=_reset_demo, | |
| inputs=[], | |
| outputs=[ | |
| selected_case, | |
| upload_input, | |
| uploaded_files_state, | |
| google_drive_link, | |
| loaded_images, | |
| threshold, | |
| smoothgrad_samples, | |
| smoothgrad_noise, | |
| gradcam_target_layer, | |
| save_xdl_results, | |
| save_xdl_dir, | |
| summary_out, | |
| processing_status_out, | |
| xdl_report_out, | |
| table_out, | |
| ], | |
| ) | |
| return demo.queue() | |