Spaces:
Runtime error
Runtime error
Commit History
-
Name mask classes from the request, not the canonical label table
The legend showed "label 2" where hepatic tumor should be. The cause is worse than a missing name: the saved CT mask does not use canonical 132-class IDs at all. Upstream's filter_mask_with_organs (scripts/sample_mask.py:154) rewrites each requested organ to its 1-based position in the request: for i in range(len(anatomy_list)): combine_label[combine_label == anatomy_list[i]] = -(i + 1) So [liver, hepatic tumor, spleen, gallbladder] yields mask values 1..4 in that order. Naming those through label_dict.json happened to be right for liver and spleen and wrong for everything else -- "hepatic tumor" landed on dummy1 and rendered as a placeholder, while gallbladder was confidently displayed as "pancreas". Silent mislabelling is the real defect here; the placeholder is just the visible symptom. Every preset was affected: Full chest 5/5 wrong (all five lung lobes shown as liver/spleen/pancreas/kidney), Whole abdomen 9/10, Pelvis organs 3/3, Liver + lesion 2/4. GenerationResul…ca3f97e -
Split viewer fetch timing into TTFB, transfer, and throughput
The console reported "download 26.0s, decode+upload 0.4s" -- the opposite shape to what I predicted from the server side, where a curl of the same route ran at 14.3 MB/s. A single "download" number cannot distinguish a server that is slow to start responding from a link that is slow to carry the bytes, and those need opposite fixes, so it now reports time-to-first-byte and body transfer separately, plus the achieved MB/s and the content-encoding the browser saw. content-encoding is included deliberately: a proxy applying gzip to an already gzipped .nii.gz would burn container CPU re-compressing incompressible data, which would show up as TTFB rather than transfer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>973c650 -
Centre the viewer status text so it clears the pane labels
#status sat at top:8px/left:12px, directly under the "Coronal" pane label at top:10px/left:10px, so "Downloading volume..." overlapped it. The four pane labels own the corners, so the status moves to the centre of the canvas as a pill with a backdrop, which is also where a loading indicator reads best. Two follow-ons: #status:empty is transparent so no empty pill floats over the image when idle, and the download/decode timing shows for 3.5s then fades rather than sitting permanently on top of the volume. setStatus() clears the fade class so a re-render is visible again. Also moved the viewer_copy import inside the DOWNCAST_VIEWER branch. It ran unconditionally, so NV_GENERATE_DOWNCAST_VIEWER=0 still required nibabel to be importable -- which defeats the point of an escape hatch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>3855d49 -
Serve int16 to the viewer; time the browser side
Measured why the image appears ~20s after the server is done. Pulling a real generated volume off the Space through the file route: download is 1.5s at 14.3 MB/s, so the network was never the problem. The file is 384^3 float32, which means the browser gunzips 21.7 MB into a 226 MB array in JavaScript and uploads that as a 3D WebGL texture. That is the wait. The viewer now streams an int16 companion. On a real 384^3 CT: 226 MB -> 113 MB in memory, 21.7 -> 18.0 MB on the wire, 0.90s to produce, max absolute error 0.0153 HU (0.0008% of range) with shape, affine and value range preserved. CT is Hounsfield units and natively integral; nibabel writes scl_slope/scl_inter for anything needing rescale, which niivue honours. The float32 original is untouched and is still what the Download button serves -- downcasting the artefact users take away would be wrong. Masks are label maps, already integral, so they pass through. NV_GENERATE_DOWNCAST_VIEWER=0 opts out, and any failure falls back to the…3452775
-
Restore CT duration to 90s
f265752 cut it to 60 on the assumption that ~25s of warm inference left ample headroom. Measuring MR-Brain through the Gradio API shows that assumption was wrong about cold starts: 45.3s on the first call after a container start, then 10.4s and 8.0s once warm. The gap is CUDA/kernel warmup rather than weight loading, since the 10.4s run was also a checkpoint cache miss. CT loads five checkpoints against MR-Brain's two and runs the paired pipeline, so its cold call needs at least as much headroom. A duration tuned to the warm case would kill the cold one, which is a worse failure than losing queue priority. Reverting until CT's own spread is measured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>9446f99 -
Add tools/space.py for driving the deployed Space from a terminal
Replaces copy-pasting container logs out of the web UI. Wraps the Space API: status (runtime stage/hardware), logs (run or build, filtered to [nv-generate] by default), timings, wait (block until the READY banner appears or the stage goes terminal), and generate (fire one generation through the Gradio API). The log endpoint replays its backlog and then holds the connection open forever, so reads are bounded by wall time rather than EOF -- curl --max-time does this predictably where urllib's socket timeouts did not surface partial stream data. generate consumes real ZeroGPU quota, so it prompts unless given -y. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>3e2ec7a -
Stream volumes by URL, cache checkpoints, reclaim archive space, quiet logs
Stream the viewer payload instead of inlining it. render_viewer() now hands niivue a /gradio_api/file= link resolved against document.baseURI, rather than base64-encoding the volume and mask into the page. A 46 MB CT was becoming ~61 MB of HTML built and shipped synchronously; measured on a test volume the payload drops 58x. app.launch() gains allowed_paths for the output dir. NV_GENERATE_VIEWER_MODE=base64 restores the old path if a deployment blocks file serving. Cache checkpoints across runs. scripts/inference.py issues five torch.load() calls per CT generation -- ~3.35 GB re-read and re-deserialised every time. _cached_torch_load() memoises them by (path, size, mtime) with LRU eviction under a byte budget (NV_GENERATE_CKPT_CACHE_GIB, default 8). This is host RAM, not VRAM: CT's weights are ~3.35 GB against a 96 GB xlarge slice, so VRAM was never the limit -- the 48-80 GB figure is activations, not parameters. Reclaim the mask archive. Once extracted, the 11.6 GB .zip is never rea…f265752 -
Move CPU work off the GPU, and log phase timings
The logs showed a CT run writing its NIfTI output ~10-15s before the image appeared. That gap was the mask label scan (nibabel + np.unique over the whole volume) and the viewer's base64 encode of both the image and mask — neither of which touches CUDA, but both of which ran inside the @spaces.GPU function and so were billed against the ZeroGPU allocation. Split for all three models. The decorated function now does inference only and hands the result to a gr.State; a following .then() does the label scan and the viewer encode on CPU. CT additionally gains generate(scan_labels=False) so the label pass can be deferred, and resolve_anatomy_labels() to run it afterwards. Added utils/timing.py: a nesting phase() context manager and a Stopwatch, all output prefixed [nv-generate] so it greps cleanly out of Gradio/MONAI/tqdm noise. Instrumented startup (clone, per-model prefetch, mask extraction, UI build) and generation (inference, label scan, viewer encode with payload size). A run now read…261940d -
Fix weight links resolving to nothing
775850b linked weights from the HF cache but produced dangling symlinks, so every model file was unreadable: "[Errno 2] No such file or directory: 'models/autoencoder_v1.pt'". os.link() on a symlink source hard-links the symlink itself, target string included, rather than the file it points at. hf_hub_download returns the snapshot entry, which is a *relative* symlink into the cache (../../../blobs/<sha>). Linking that into <upstream>/models/ carried the relative target along, where it resolved to <upstream>/../../blobs/<sha> -- nothing. The result passed through upstream's `saved to:` unverified. The second-order failure: a dangling symlink is False to exists() but True to lexists(), so link() and symlink() both fail EEXIST and shutil.copy2 then writes *through* the broken link, which is where the Errno 2 surfaced. Three fixes: resolve the source with realpath() before linking so the blob is linked; unlink any stale dst first, dangling ones included; and verify the result resolves, …d5d94ae -
Prefetch weights at startup and stop duplicating them on disk
Three problems, all on the CT path, all visible in the Space logs: 1. The whole weight fetch ran inside the @spaces.GPU function. CT pulls ~15 GB (11.6 GB of it the anatomy-mask archive) and then unzips it, which alone is ~130s against a 90s GPU budget. prefetch_weights() now runs at import time in app.py, outside the decorator, so the GPU allocation only pays for inference. It also pre-extracts the mask archive, which find_masks.py then skips. 2. run_paired_ct() called ensure_weights() even though upstream scripts/inference.py calls download_model_data() itself — with a different root_dir. That wrote the 11.6 GB archive to two separate locations. Removed; ensure_weights now defaults to the same DATA_DIR upstream uses. 3. Upstream copies each file out of the HF cache with shutil.copy2, storing everything twice. _LinkingShutil hard-links instead: one inode, two names, zero extra bytes. Moving the blob would corrupt hf_hub_download's blobs/snapshots layou…775850b -
Open each workspace on a default quick preset
CT defaults to "Liver + lesion", MR to "T1 brain", MR-Brain to "T1 whole brain" (which already matched, now made explicit). Controls read their initial values from the preset dicts via a new by_label() helper rather than duplicating literals, so the on-load state and the preset button cannot drift apart. by_label raises on an unknown label, so renaming a preset fails at import instead of silently leaving a stale default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>52a231e -
Run CT on the ZeroGPU xlarge slice
ZeroGPU moved to RTX Pro 6000 Blackwell, where the default `large` size is half a card (48 GB) rather than the old ~70 GB H200 MIG slice. The CT presets were budgeted against ~70 GB (see ui/presets.py) and OOM on `large`, so CT now requests `xlarge` (96 GB). This costs 2x quota and queues longer, so MR and MR-Brain stay on `large` — they cap at 256^3 and fit comfortably. Pin spaces>=0.47.0, the first release with the size= argument. The import guard in app.py only catches ImportError/AttributeError, so an older spaces would raise an uncaught TypeError at startup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>24f896b
-
Move Quick Presets to the top of the controls sidebar
Quick Presets sat just above the Generate button — the last thing users saw, after they'd already configured everything by hand. Move it to the top of the controls column on all three workspaces so it's the first thing visible, encouraging a preset-first flow. New order per workspace: CT : Quick presets → Conditioning → Geometry → Diffusion → Generate MR : Quick presets → Conditioning → Geometry → Diffusion → Generate MR Brain : Quick presets → Contrast → Geometry → Diffusion → Generate The click-handler bindings at the end of build() still reference `sample_btns` correctly — it's just defined earlier in the column now. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>7fb7cc3 -
Tune ZeroGPU durations: CT 90s, MR/MR-Brain 60s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>57d05e1 -
Reduce ZeroGPU request duration 180s -> 120s
Lowers the @spaces.GPU duration for all three models from 180s to 120s. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>ad5ba7a -
Default MR CFG guidance to 10
Changes the MR workspace CFG slider default from 15.0 to 10.0, matching the MR-Brain workspace (already 10.0). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>407e4a9 -
Make the Seed input compact like the other number boxes
Seed is a standalone gr.Number, so it rendered a full-width, tall input (~173x38) while every other number box in the panel is a compact slider number box (~74-88 x 22). Gave the Seed gr.Number an elem_classes ("seed-field") in all three workspaces and constrained its input to max-width 88px / height 22px / 12px font — now 88x22, matching the steps and spacing boxes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>3188a6e -
Flat-matte Generate button, fix hover, FOV-based MR presets
1. Generate button — was a green gradient (#7fc003 -> var(--green)) with glossy inset highlights + a green glow shadow; in light mode var(--green) is the deeper #5d9400 so the gradient was pronounced. Now a flat solid #76b900 with no gradient and no shadow — matte, and pixel-identical in light and dark (fixed hex, not the theme token). 2. Hover was unreadable in dark. Root cause: every .primary-cta state rule used the descendant selector `.primary-cta button`, but Gradio puts the class on the <button> itself — so :hover/:active/:disabled never matched. On hover, `.controls button:hover` took over and set the background to var(--pill-hover-bg) = transparent in dark, revealing the navy panel behind the black text. Rewrote every state to `button.primary-cta`, and excluded .primary-cta from the `.controls button:hover` chip rule. Hover is now a flat brighter green (#84cc00) with readable dark text. 3. MR quick presets — replaced with the three highest-vol…e7c2ab6 -
Stop the radio fieldset clipping the first pill's left border
Gradio's radio/checkbox <fieldset> computes overflow:auto, which clips the first pill's 1px left border where it sits flush at the fieldset edge. Visible in light mode (real border contrast — most obvious on the selected W/L preset's bold green outline); invisible in dark where the border is near-transparent. Set the .preset-row and .controls pill fieldsets to overflow:visible so the full outline renders. Verified the Soft Tissue pill shows a complete border in light, and overflow is visible in both themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>2f5e4d9 -
Trim intro-card bottom margin 35px -> 28px
Slightly less spacing below the model info card per feedback — still clearly separated from the controls/viewer, just tighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>bc46cdb -
Fix intro-card bottom margin on the right element
The previous bump targeted .ws-intro — an inner div three levels inside the gr.HTML wrapper — so it had no visible effect. The element that actually carries the spacing is Gradio's .block wrapper, which sets an inline margin-bottom: 20px. Override it via `.block:has(.ws-intro)` with !important (inline styles need it) -> margin-bottom: 35px. Reverted .ws-intro back to its original 28px. Verified the computed margin is 35px in both light and dark. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>7bd86dc -
Add breathing room below the model info card
Bumped the .ws-intro panel's bottom margin 28px -> 36px so there's slightly more separation between the top model-information card and the controls sidebar / multiplanar viewer below it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>e221ce7 -
Center Window/Level preset pill text
The W/L preset pills (.preset-row label) had the same Gradio ml-2 span-margin offset as the body-region / voxel pills, and no rule was zeroing it. Added .preset-row label > span { margin: 0 } — text is now symmetric (13px/13px) in all four presets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>e33c60d
-
UI polish: spec banner color, control alignment, presets, card hover
Five fixes from the post-review feedback; all surface-level, dark and light stay consistent. 1. Spec banner — the model-page spec keys (.ws-fact-k) were flat gray; now tinted with the page accent (CT green / MR blue / MR-Brain purple) and bumped to 600 weight so they read as structured data. 2. Controls left edge — text inputs, field labels and the checkbox sat ~12px right of the pill rows and section headers, because the Gradio per-field .block wrapper carries 12px horizontal padding. Zeroed .controls .block left/right padding so every control shares the panel's content-left edge. 3. Pill text centering — Gradio's ml-2 utility put an 8px margin-left on the pill label span, shoving body-region / voxel pill text off-centre (23px vs 15px). Added margin:0 to the pill span rule; now 15/15. 4. Quick presets — replaced the thin 1-2 organ CT presets with four meaningful ones: "Whole abdomen" (10 abdominal organs), "Full chest" (all 5 lung lobes), "Liver + les…95b6812 -
Fix panel depth and unify corners across themes
Two issues found via computed-style inspection of the live Space: 1. Panels had no surface. .controls, .preset-row and .viewer rendered fully transparent — no background, border, or shadow. A broad wrapper-chrome reset (.gradio-container .block/.row/...) outranked the panel rules by specificity and stripped them. Light mode only looked "white" because the page itself was near-white. Fixed by prefixing the panel rules with .gradio-container so they outrank the reset. Panels now render their real surface in both themes — white + shadow in light, navy in dark — giving genuine depth behind the control groups. 2. Corner radius was inconsistent — rounded in light, square in dark. Unified to square in both themes (--radius: 0); the corner ticks now also show in light. Also dropped the per-modality card tint (a color-mix gradient that silently invalidated .ds-card's background) and darkened the light page to #eceff4 so the white panels read clearly. Co-Authored-B…5753f3d -
Refine light theme: depth, hierarchy, clinical polish
Applies the light-theme fix plan + reference mockup. All changes are light-only (token-driven); the dark theme resolves every new token to a neutral value and is unchanged. - Palette: page #f8fafc, panels #ffffff, borders #d8e0ea/#b8c4d4, text #0f172a/#334155/#64748b/#94a3b8, green #5d9400, blue #2563eb, purple #7c3aed — stronger contrast and a clearer text hierarchy. - Background grid contrast cut to rgba(15,23,42,0.045) so it no longer competes with content. - Depth: cards and panels (.ds-card, .controls, .ws-intro, .viewer, .preset-row, .legend-host, .nv-download) get a two-layer card shadow and an 8px corner radius via new --shadow-card / --radius tokens. - Cards carry a faint per-modality background tint; the dark-mode corner ticks are hidden in light (they clash with rounded corners). - Inputs get a blue focus ring (--focus / --focus-ring); sliders and the checkbox use a blue accent; pills gain a muted hover fill. - Fixes a circular --cta-off-text reference in the …64160eb -
Add adaptive clinical dual-theme (light + dark)
Replaces the force-dark hack with a real adaptive dual-theme that follows the visitor's OS / HF Spaces preference. - Tokenize every color in the CSS block: ~25 existing vars kept, ~20 new semantic tokens added for previously-hardcoded literals. Accent-tinted fills now use color-mix() against an accent token, which resolves pixel-exact in dark (e.g. --green #76b900 == rgb(118,185,0)). - Define two token blocks mirroring Gradio's own selectors: :root carries the new light "clinical blue" palette; :root.dark, :root .dark carries the dark "MAISI Console" palette (values copied verbatim, so dark mode is unchanged). Gradio toggles the .dark class from the user preference. - The niivue viewer surface and its empty-state placeholder stay dark in both themes (radiology/PACS convention); their colors are not tokenized. - Modality dots (.ws-dot) in the three workspaces now use var(--ct/mr/mrb) so they pick up the deepened light-mode modality colors. - Remove _all_dark_theme(); us…2e66cee -
Fix light mode by collapsing the theme, not hacking the DOM
The previous fix forced Gradio's .dark class via a <head> script + MutationObserver. That fought Gradio's SSR hydration and triggered a remount loop: the hero cards (gr.HTML) never rendered and the page was left with three dead buttons. Root cause of the original bug: Gradio's _get_theme_css() emits every token under both :root (light) and :root.dark (dark). Our UI is dark-native, so light mode looked washed out. Real fix: build the theme with gr.themes.Base(), then copy every *_dark token value onto its light counterpart. Gradio's :root block then carries dark colors and the app renders dark for every visitor -- pure server-rendered CSS, no client-side script, nothing to break hydration. Verified against gradio 6.14.0: 84 tokens collapsed, server serves theme.css with dark values in :root, build_app() and launch() clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>68d0499 -
Force dark mode by pinning Gradio's .dark class
The ?__theme=dark redirect didn't stick — HF Spaces controls the embedded iframe's theme and overrides the URL param. Instead, pin Gradio's `.dark` class on <html>, <body>, and <gradio-app> via a <head> script, and re-add it with a MutationObserver whenever HF's theme toggle strips it. This makes the app render identically to dark mode regardless of OS or HF theme. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>89c8f24 -
Move js/head to launch() — Gradio 6 ignores them on Blocks
Gradio 6.0 relocated the js and head parameters from the gr.Blocks constructor to launch(). They were silently dropped (UserWarning only), so the force-dark-mode redirect never ran and light-mode visitors still got washed-out theme tokens. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>d2fed28 -
Force dark mode site-wide and cap CT GPU duration
- Force ?__theme=dark via gr.Blocks js= so light-mode visitors get the intended dark "MAISI Console" aesthetic instead of washed-out light tokens. - Add <meta name="color-scheme" content="dark"> via head= so native browser chrome paints dark before hydration. - Reduce CT @spaces.GPU duration 300s -> 180s; 300s exceeded the ZeroGPU maximum allowed (180s) and rejected CT generations upfront. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>5f11b27
-
Add HF Spaces .gitattributes (LFS config)
e4d3e7e -
Initial commit: NV-Generate Gradio showcase
A Hugging Face Spaces app that surfaces NVIDIA Medtech's three open-weight 3D medical image generators (CT, MR, MR Brain) under one console-style UI, all running the latest MAISI-v2 rectified-flow variants. Architecture ============ - app.py — Gradio Blocks entrypoint. Hero card landing → per-model workspace switched via gr.Group visibility. Auto-clones the NV-Generate-CTMR upstream repo on first run so the same flow works locally and on HF Spaces. - pipelines/ base.py — GenerationRequest / GenerationResult dataclasses. _runner.py — Shared driver: chdir into upstream, write modified configs into configs/_temp/, invoke scripts.diff_model_infer or scripts.inference, harvest the resulting NIfTI(s). ct.py — Paired image + 132-class mask (rflow-ct). mr.py — Multi-contrast MR image-only (rflow-mr). mr_brain.py — Brain MR multi-sequence (rflow-mr-brain). - viewer/ niivue_embed.py — Render…ab1db83