""" NV-Generate Showcase — Gradio app entrypoint. Hero card landing → per-model workspace. Each model's Generate button is wrapped with @spaces.GPU on Hugging Face Spaces (ZeroGPU); locally the decorator is a no-op. """ from __future__ import annotations import os import subprocess import sys from pathlib import Path import gradio as gr ROOT = Path(__file__).resolve().parent sys.path.insert(0, str(ROOT)) def _ensure_upstream(): """Clone the NV-Generate-CTMR inference repo on first run. HF Spaces with `sdk: gradio` does not run shell scripts at build time, so we bootstrap the upstream checkout here at module load. Idempotent: skips if the directory already exists (local dev, subsequent Space restarts). """ upstream = ROOT / "repos" / "NV-Generate-CTMR" if upstream.exists(): return print("[nv-generate] cloning NV-Generate-CTMR upstream...", flush=True) upstream.parent.mkdir(parents=True, exist_ok=True) subprocess.run( [ "git", "clone", "--depth", "1", "https://github.com/NVIDIA-Medtech/NV-Generate-CTMR.git", str(upstream), ], check=True, ) _ensure_upstream() # ZeroGPU decorator — present on HF Spaces, optional locally. try: import spaces # type: ignore spaces_gpu_ct = spaces.GPU(duration=90) spaces_gpu_mr = spaces.GPU(duration=60) spaces_gpu_mr_brain = spaces.GPU(duration=60) except (ImportError, AttributeError): def _identity(d=None): # noqa: ARG001 def deco(fn): return fn return deco spaces_gpu_ct = _identity() spaces_gpu_mr = _identity() spaces_gpu_mr_brain = _identity() from ui.hero import render_hero from ui import workspace_ct, workspace_mr, workspace_mr_brain CSS = """ @import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap'); /* ═══════════════ theme tokens ═══════════════ Two coordinated palettes. Gradio toggles a `.dark` class on the document from the visitor's OS / HF Spaces preference and emits its own tokens under `:root` (light) + `:root.dark, :root .dark` (dark) — see gradio/themes/ base.py. We mirror that exact selector pattern so the whole UI flips with it. `:root` carries the light "clinical blue" theme; `:root.dark` carries the navy "MAISI Console" theme (values unchanged from the original dark build). Accent-tinted fills use color-mix() against an accent token, which resolves pixel-exact in dark (e.g. --green #76b900 == rgb(118,185,0)) and adapts automatically in light. */ :root { /* fonts + layout — theme-independent */ --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; --font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", "SF Mono", monospace; --num: "Geist Mono", ui-monospace, monospace; --container: 1240px; --gutter: 32px; /* ─── LIGHT — clean medical-blue clinical ─── */ --page-bg: #eceff4; --page-grad-a: #eff2f6; --page-grad-b: #eceff4; --page-grad-c: #e6eaf0; --bg-0: #f1f5f9; --bg-1: #ffffff; --bg-2: #f1f5f9; --panel: #ffffff; --panel-2: #f1f5f9; --viewer-bg: #0b1020; --line: #d8e0ea; --line-strong: #b8c4d4; --line-bright: #9aa9bd; --text: #0f172a; --text-2: #334155; --muted: #64748b; --muted-2: #94a3b8; --green: #5d9400; --green-glow: rgba(93, 148, 0, 0.35); --green-soft: rgba(93, 148, 0, 0.12); --warn: #9a6b00; --warn-soft: rgba(154, 107, 0, 0.10); --ct: #5d9400; --mr: #2563eb; --mrb: #7c3aed; --grid-line: rgba(15, 23, 42, 0.045); --glow-blue: rgba(37, 99, 235, 0.07); --glow-purple: rgba(124, 58, 237, 0.05); --card-hl: rgba(255, 255, 255, 0); --banner-shade: rgba(15, 23, 42, 0.05); --banner-grid: rgba(15, 23, 42, 0.05); --chip-bg: #f1f5f9; --chip-bg-faint: #f8fafc; --shadow: rgba(15, 23, 42, 0.14); --shadow-card: 0 1px 2px rgba(15, 23, 42, 0.05), 0 12px 32px rgba(15, 23, 42, 0.08); --shadow-card-hover: 0 2px 6px rgba(15, 23, 42, 0.07), 0 22px 48px -10px rgba(15, 23, 42, 0.16); --preset-label: #475569; --license-link: #8a5200; --radius: 0; --focus: #2563eb; --focus-ring: 0 0 0 3px rgba(37, 99, 235, 0.18); --corner-display: block; --pill-hover-bg: #f1f5f9; --slider-accent: #2563eb; --cta-off-a: #c4cdbb; --cta-off-b: #b4bdab; --cta-off-border: #a6b39a; --cta-off-text: rgba(24, 32, 12, 0.62); } :root.dark, :root .dark { /* ─── DARK — navy "MAISI Console" (unchanged from original build) ─── */ --page-bg: #010206; --page-grad-a: #050810; --page-grad-b: #02040c; --page-grad-c: #010206; --bg-0: #06102a; --bg-1: #0a1530; --bg-2: #0e1838; --panel: #0e1b3a; --panel-2: #142348; --viewer-bg: #0b1020; --line: rgba(140, 180, 240, 0.12); --line-strong: rgba(140, 180, 240, 0.22); --line-bright: rgba(140, 180, 240, 0.38); --text: #ecf0fa; --text-2: #b0bcd5; --muted: #7280a0; --muted-2: #4a5778; --green: #76b900; --green-glow: rgba(118, 185, 0, 0.55); --green-soft: rgba(118, 185, 0, 0.10); --warn: #f6c861; --warn-soft: rgba(246, 200, 97, 0.10); --ct: #76b900; --mr: #5fb4ff; --mrb: #b48aff; --grid-line: rgba(0, 0, 6, 0.42); --glow-blue: rgba(70, 140, 230, 0.16); --glow-purple: rgba(140, 100, 220, 0.06); --card-hl: rgba(255, 255, 255, 0.04); --banner-shade: rgba(0, 0, 0, 0.22); --banner-grid: rgba(150, 180, 240, 0.06); --chip-bg: rgba(140, 180, 240, 0.04); --chip-bg-faint: rgba(140, 180, 240, 0.03); --shadow: rgba(0, 0, 0, 0.70); --shadow-card: none; --shadow-card-hover: 0 1px 0 var(--card-hl) inset, 0 24px 60px -16px color-mix(in srgb, var(--accent) 35%, transparent); --preset-label: rgba(170, 200, 255, 0.85); --license-link: #ffe9a3; --radius: 0; --focus: var(--green); --focus-ring: 0 0 0 1px var(--green-soft); --corner-display: block; --pill-hover-bg: transparent; --slider-accent: var(--green); --cta-off-a: #4a5f00; --cta-off-b: #3a4c00; --cta-off-border: #2a3700; --cta-off-text: rgba(255, 255, 255, 0.70); } /* ─────────────── base + blueprint grid ─────────────── */ /* Override Gradio Base theme tokens that paint a default gray fill across the app */ .gradio-container, .gradio-container *, gradio-app { --body-background-fill: transparent !important; --body-background-fill-dark: transparent !important; --background-fill-primary: transparent !important; --background-fill-primary-dark: transparent !important; --background-fill-secondary: transparent !important; --background-fill-secondary-dark: transparent !important; --block-background-fill: transparent !important; --block-background-fill-dark: transparent !important; --panel-background-fill: transparent !important; --panel-background-fill-dark: transparent !important; --color-background-primary: transparent !important; } html { background: var(--page-bg) !important; } html, body { color: var(--text) !important; font-family: var(--font-sans); font-feature-settings: "ss01", "cv11", "tnum"; } body { background-image: /* Concentrated blue glow behind the hero only — not page-wide */ radial-gradient(ellipse 1100px 600px at 50% 220px, var(--glow-blue), transparent 65%), /* NVIDIA green hint warming the top-left of the page */ radial-gradient(ellipse 700px 380px at 18% -40px, color-mix(in srgb, var(--green) 6%, transparent), transparent 70%), /* Cool purple sweep at lower-right (very faint) */ radial-gradient(ellipse 900px 600px at 108% 108%, var(--glow-purple), transparent 60%), /* Architectural blueprint grid */ linear-gradient(var(--grid-line) 1px, transparent 1px), linear-gradient(90deg, var(--grid-line) 1px, transparent 1px), /* Page base — subtle vertical wash so panels lift off it */ linear-gradient(180deg, var(--page-grad-a) 0%, var(--page-grad-b) 50%, var(--page-grad-c) 100%) !important; background-size: auto, auto, auto, 32px 32px, 32px 32px, auto; background-attachment: fixed, fixed, fixed, fixed, fixed, fixed; background-color: var(--page-bg) !important; } /* ─────────────── film-grain noise overlay ─────────────── */ /* Breaks gradient banding, adds photographic warmth across every screenshot. */ body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; background-image: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.6 0 0 0 0 0.7 0 0 0 0 0.9 0 0 0 1 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); background-size: 240px 240px; opacity: 0.045; mix-blend-mode: overlay; } /* ─────────────── one-time scanline sweep on page load ─────────────── */ body::after { content: ""; position: fixed; left: 0; right: 0; top: -8px; height: 2px; z-index: 9999; pointer-events: none; background: linear-gradient(90deg, transparent 5%, color-mix(in srgb, var(--green) 35%, transparent) 30%, color-mix(in srgb, var(--green) 85%, transparent) 50%, color-mix(in srgb, var(--green) 35%, transparent) 70%, transparent 95%); filter: blur(3px); animation: page-scanline 1.8s cubic-bezier(.4,.0,.2,1) 0.4s 1 forwards; opacity: 0; } @keyframes page-scanline { 0% { transform: translateY(0); opacity: 0; } 8% { opacity: 0.9; } 50% { opacity: 1; } 90% { opacity: 0.4; } 100% { transform: translateY(100vh); opacity: 0; } } /* All Gradio root / wrapper elements stay transparent so body bg shows through */ gradio-app, gradio-app > div, gradio-app .main, gradio-app .wrap, gradio-app .contain, gradio-app #root, .app, .main, .wrap, .contain { background: transparent !important; background-color: transparent !important; } .gradio-container { max-width: var(--container) !important; padding: 0 var(--gutter) !important; background: transparent !important; background-color: transparent !important; font-family: var(--font-sans) !important; margin: 0 auto !important; } /* Override Gradio's default panel chrome — every wrapper class that can show bg */ .gradio-container .form, .gradio-container .gap, .gradio-container .panel, .gradio-container .block, .gradio-container .gr-box, .gradio-container .gr-group, .gradio-container .gr-padded, .gradio-container > .main, .gradio-container > .wrap, .gradio-container .styler, .gradio-container .container, .gradio-container .row, .gradio-container .column { background: transparent !important; background-color: transparent !important; border: none !important; box-shadow: none !important; } /* gr.HTML wraps its value in a .html-container div with horizontal padding. Zero the PADDING so HTML blocks (ws-intro, viewer-strip, legend, status) sit flush against the column edge; leave margin alone so children's own margin-bottom (e.g. .ws-intro { margin: 0 0 28px }) still creates space below. */ .gradio-container .html-container, .gradio-container [class*="html-container"] { padding: 0 !important; background: transparent !important; } /* The hero/workspace gr.Group wrappers must not add their own chrome */ .hero, .workspace { background: transparent !important; background-color: transparent !important; border: 0 !important; padding: 0 !important; } .hero > .gap, .workspace > .gap { padding: 0 !important; background: transparent !important; } .dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; background: var(--green); box-shadow: 0 0 6px var(--green-glow); } .dot-pulse { animation: dotpulse 2s ease-in-out infinite; } @keyframes dotpulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } } @keyframes fadein { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } } /* ─────────────── masthead ─────────────── */ .masthead { display: flex; align-items: center; justify-content: space-between; height: 56px; border-bottom: 1px solid var(--line); font-family: var(--font-sans); animation: fadein 0.4s ease both; } .masthead-brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--text); } .nv-mark { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 22px; background: var(--green); color: #000; font-family: var(--font-mono); font-weight: 700; font-size: 11px; letter-spacing: 0.04em; clip-path: polygon(0 0, 100% 0, 100% 70%, 86% 100%, 0 100%); } .masthead-name { font-size: 14px; font-weight: 500; letter-spacing: -0.005em; color: var(--text); } .masthead-nav { display: flex; align-items: center; gap: 18px; font-family: var(--font-sans); font-size: 13px; color: var(--text-2); } .masthead-nav a { color: var(--text-2); text-decoration: none; transition: color 160ms ease; } .masthead-nav a:hover { color: var(--green); } .masthead-sep { width: 3px; height: 3px; background: var(--muted-2); border-radius: 50%; } /* ─────────────── hero block ─────────────── */ .hero { padding: 0 0 32px; } .hero-mono { padding: 88px 0 48px; text-align: center; max-width: 720px; margin: 0 auto; position: relative; animation: fadein 0.55s ease 0.05s both; } /* Soft blue glow behind the headline gives atmospheric depth */ .hero-mono::before { content: ""; position: absolute; width: 720px; height: 380px; left: 50%; top: -40px; transform: translateX(-50%); background: radial-gradient(ellipse at center, color-mix(in srgb, var(--mr) 18%, transparent) 0%, transparent 55%), radial-gradient(ellipse at 30% 70%, color-mix(in srgb, var(--green) 12%, transparent) 0%, transparent 60%); filter: blur(16px); pointer-events: none; z-index: -1; opacity: 0.9; } .hero-eyebrow { display: inline-flex; align-items: center; gap: 12px; color: var(--green); font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; margin-bottom: 24px; } .line-tick { display: inline-block; width: 28px; height: 1px; background: var(--green); opacity: 0.5; } .hero-title { font-family: var(--font-sans); font-size: clamp(2.0rem, 4.0vw, 3.0rem); font-weight: 500; line-height: 1.05; letter-spacing: -0.032em; margin: 0; color: var(--text); } /* ─────────────── datasheet cards ─────────────── */ .hero-row { gap: 20px !important; align-items: stretch !important; } .hero-row > * { display: flex !important; flex-direction: column !important; } .hero-card-col { padding: 0 !important; display: flex !important; flex-direction: column !important; animation: fadein 0.5s ease both; } .hero-card-col:nth-child(1) { animation-delay: 0.10s; } .hero-card-col:nth-child(2) { animation-delay: 0.18s; } .hero-card-col:nth-child(3) { animation-delay: 0.26s; } .ds-card { position: relative; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-card); transition: border-color 220ms ease, background 220ms ease, transform 220ms ease, box-shadow 280ms ease; --accent: var(--green); flex: 1 1 auto; display: flex; flex-direction: column; } .ds-card.ds-ct { --accent: var(--ct); } .ds-card.ds-mr { --accent: var(--mr); } .ds-card.ds-mrb { --accent: var(--mrb); } .ds-card:hover { border-color: color-mix(in srgb, var(--accent) 60%, transparent); background: var(--panel-2); box-shadow: var(--shadow-card-hover); } .ds-card:hover .ds-corner { background: var(--accent); } .ds-frame { position: relative; padding: 18px 22px 22px; display: flex; flex-direction: column; flex: 1 1 auto; } /* Banner at top of card — modality scan icon over a faint accent gradient */ .ds-banner { position: relative; height: 110px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; border-bottom: 1px solid var(--line); background: linear-gradient(180deg, transparent 0%, var(--banner-shade) 100%), radial-gradient(ellipse at center, color-mix(in srgb, var(--accent) 18%, transparent) 0%, transparent 60%); overflow: hidden; color: var(--accent); } .ds-banner-grid { position: absolute; inset: 0; background-image: linear-gradient(var(--banner-grid) 1px, transparent 1px), linear-gradient(90deg, var(--banner-grid) 1px, transparent 1px); background-size: 16px 16px; pointer-events: none; mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%); } /* Single scanline sweep on first card render — telegraphs "this generates volumes" */ .ds-banner::after { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 2px; background: linear-gradient(90deg, transparent, var(--accent), transparent); filter: blur(1px); opacity: 0; animation: banner-scan 2.6s cubic-bezier(.4,0,.2,1) 1 forwards; animation-delay: 0.5s; } .hero-card-col:nth-child(1) .ds-banner::after { animation-delay: 0.5s; } .hero-card-col:nth-child(2) .ds-banner::after { animation-delay: 0.75s; } .hero-card-col:nth-child(3) .ds-banner::after { animation-delay: 1.0s; } @keyframes banner-scan { 0% { top: 0; opacity: 0; } 10% { opacity: 0.85; } 85% { opacity: 0.85; } 100% { top: 110px; opacity: 0; } } .ds-banner-icon { width: 64px; height: 64px; position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; transition: transform 280ms ease; } .ds-card:hover .ds-banner-icon { transform: scale(1.05); } .ds-banner-icon svg { width: 100%; height: 100%; display: block; } .ds-banner-caption { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent); opacity: 0.85; position: relative; z-index: 1; } .ds-corner { position: absolute; width: 7px; height: 7px; background: var(--line-bright); display: var(--corner-display); transition: background 220ms ease; } .ds-tl { top: -1px; left: -1px; clip-path: polygon(0 0, 100% 0, 0 100%); } .ds-tr { top: -1px; right: -1px; clip-path: polygon(0 0, 100% 0, 100% 100%); } .ds-bl { bottom: -1px; left: -1px; clip-path: polygon(0 0, 0 100%, 100% 100%); } .ds-br { bottom: -1px; right: -1px; clip-path: polygon(100% 0, 100% 100%, 0 100%); } .ds-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; height: 24px; } .ds-index { font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.18em; color: var(--accent); padding: 3px 9px; border: 1px solid var(--accent); border-radius: var(--radius); opacity: 0.9; } /* Availability status — single dot + label. Decoupled from license info. */ .ds-status { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); } .ds-status .dot { background: var(--accent); box-shadow: 0 0 6px var(--accent); } .ds-name { margin-bottom: 16px; } .ds-name-title { font-family: var(--font-sans); font-size: 1.20rem; font-weight: 500; letter-spacing: -0.018em; line-height: 1.15; color: var(--text); margin-bottom: 4px; } .ds-name-code { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em; text-transform: lowercase; color: var(--accent); opacity: 0.85; margin-bottom: 10px; } .ds-name-sub { font-family: var(--font-sans); font-size: 0.86rem; line-height: 1.5; color: var(--text-2); min-height: 2.6em; /* reserves ~2 lines so cards stay aligned */ margin-bottom: 14px; } /* Use-case section label */ .ds-uses-label { font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; } /* Use-case chips: small mono pills below the subtitle */ .ds-uses { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; min-height: 28px; } .ds-use { display: inline-flex; align-items: center; padding: 4px 9px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--chip-bg); font-family: var(--font-sans); font-size: 11px; font-weight: 500; letter-spacing: 0; color: var(--text-2); white-space: nowrap; } .ds-card:hover .ds-use { border-color: var(--line-strong); } /* License chip: separate from availability status, lives at card bottom */ .ds-license { margin-top: 14px; padding: 8px 12px; display: flex; align-items: center; gap: 10px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--chip-bg-faint); font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.04em; } .ds-license-k { color: var(--muted); text-transform: uppercase; letter-spacing: 0.16em; font-size: 9px; } .ds-license-v { color: var(--text); } .ds-license-warn { border-color: color-mix(in srgb, var(--warn) 30%, transparent); background: var(--warn-soft); } .ds-license-warn .ds-license-v { color: var(--warn); } .ds-license-ok { border-color: color-mix(in srgb, var(--green) 25%, transparent); } .ds-license-ok .ds-license-v { color: var(--text); } .ds-divider { height: 1px; background: var(--line); margin: 0 0 14px; position: relative; } .ds-divider::before, .ds-divider::after { content: ""; position: absolute; top: -2px; width: 4px; height: 4px; background: var(--accent); } .ds-divider::before { left: 0; } .ds-divider::after { right: 0; } .ds-spec { display: flex; flex-direction: column; gap: 7px; margin-top: auto; /* pushes spec to bottom of frame so cards always end at same baseline */ } .ds-row { display: flex; align-items: baseline; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.02em; color: var(--text-2); white-space: nowrap; overflow: hidden; } .ds-key { color: var(--muted); flex-shrink: 0; text-transform: uppercase; font-weight: 500; } .ds-leader { flex: 1 1 auto; border-bottom: 1px dotted var(--line-strong); margin: 0 8px; height: 0; align-self: end; margin-bottom: 4px; min-width: 12px; } .ds-val { color: var(--text); text-align: right; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; } /* CTA buttons under each card */ .ds-cta { padding: 0 !important; margin: 0 !important; } .ds-cta button, button.ds-cta { width: 100% !important; margin-top: 14px !important; padding: 14px 16px !important; background: transparent !important; border: 1px solid var(--line-strong) !important; border-radius: var(--radius) !important; color: var(--text) !important; font-family: var(--font-sans) !important; font-size: 13px !important; font-weight: 500 !important; letter-spacing: 0 !important; text-transform: none !important; text-align: center !important; cursor: pointer !important; transition: all 200ms ease !important; } /* Gradio puts elem_classes directly on the