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 ( "
DenseNet-based colitis classification with GradCAM and SmoothGrad explanations. Upload a directory or use a public Google Drive folder link, then run batch inference.