# Spec #28: Gradio Custom Component for NiiVue **Date:** 2025-12-10 **Status:** REQUIRED - All gr.HTML hacks have failed (confirmed Dec 10) **Blocks:** Issue #24 (HF Spaces "Loading..." forever) **Effort:** Medium (2-3 days + 0.5-1 day buffer for HF Spaces quirks) **Success Probability:** 90% **Audited:** AUDIT_REPORT_2025_12_10.md - GO recommendation --- ## Executive Summary **All `gr.HTML` + JavaScript approaches have FAILED. This is the only path forward.** Gradio maintainers have explicitly closed both: - [Issue #4511](https://github.com/gradio-app/gradio/issues/4511) - NIfTI/medical imaging support → "Not planned" - [Issue #7649](https://github.com/gradio-app/gradio/issues/7649) - WebGL canvas component → "Not planned" Their official answer: **"Create a Gradio Custom Component."** This spec documents what we need to build to properly integrate NiiVue (WebGL2 medical imaging viewer) into our Gradio app. --- ## Why Current Approach Fails ### What We've Tried | Attempt | Why It Failed | |---------|---------------| | CDN import in js_on_load | HF Spaces CSP blocks external imports | | Vendored NiiVue + dynamic import() | import() in js_on_load blocks Svelte hydration | | head= parameter | Still uses ES module import, same problem | | head_paths= parameter | Same as above | | gr.set_static_paths() | File serving works, but JS loading mechanism broken | ### Root Cause **We're fighting `gr.HTML`'s limitations, not Gradio itself.** Gradio CAN do WebGL (proven by `gradio-litmodel3d`), but NOT via `gr.HTML`: 1. `gr.HTML` strips `
{#if error}
{error}
{:else if loading}
Loading viewer...
{/if}
``` **Key improvements from audit feedback:** - WebGL2 capability check before initialization - WebGL context loss/restore handlers - Proper error UI states - Loading state management - Reactive update when value changes ### Phase 3: Implement Python Backend (2-3 hours) ```python # backend/gradio_niivue_viewer/__init__.py from __future__ import annotations from typing import Any from gradio.components.base import Component from gradio.data_classes import FileData, GradioModel class NiiVueViewerData(GradioModel): background_url: str | None = None overlay_url: str | None = None class NiiVueViewer(Component): """WebGL NIfTI viewer using NiiVue.""" data_model = NiiVueViewerData def __init__( self, value: NiiVueViewerData | None = None, *, label: str | None = None, height: int = 500, **kwargs, ): self.height = height super().__init__(value=value, label=label, **kwargs) def preprocess(self, payload: NiiVueViewerData | None) -> dict[str, Any] | None: if payload is None: return None return { "background_url": payload.background_url, "overlay_url": payload.overlay_url, } def postprocess(self, value: dict[str, Any] | None) -> NiiVueViewerData | None: if value is None: return None return NiiVueViewerData( background_url=value.get("background_url"), overlay_url=value.get("overlay_url"), ) ``` ### Phase 4: Build and Test (2-3 hours) ```bash # Build the component cd gradio-niivue-viewer gradio cc build # Install locally pip install -e . # Test in demo app python demo/app.py ``` ### Phase 5: Integrate into stroke-deepisles-demo (1-2 hours) Replace `gr.HTML` with the custom component: ```python # Before (broken) from stroke_deepisles_demo.ui.viewer import create_niivue_html viewer = gr.HTML(value="", elem_id="niivue-viewer") # ... then set viewer.value = create_niivue_html(...) # After (working) from gradio_niivue_viewer import NiiVueViewer viewer = NiiVueViewer(label="Interactive 3D Viewer") # ... then set viewer.value = {"background_url": dwi_url, "overlay_url": mask_url} ``` ### Phase 6: HF Spaces Deployment (CRITICAL) **This phase is essential.** HF Spaces runs `pip install` only - it does NOT run `npm` or `gradio cc build`. #### 6a. Commit build artifacts to git ```bash cd packages/gradio-niivue-viewer # Build the component (generates frontend/dist/ or templates/) gradio cc build # Force-add build artifacts (they may be gitignored by default) git add -f gradio_niivue_viewer/templates/ # Or wherever the build output lands - check with: # find . -name "*.js" -path "*/dist/*" -o -name "*.css" -path "*/dist/*" git commit -m "chore: add compiled frontend assets for HF Spaces deployment" ``` **Why:** HF Spaces won't run npm/node build steps. The compiled JS/CSS must be in the repo. #### 6b. Update requirements.txt Add the local component to the main `requirements.txt`: ```text # requirements.txt gradio>=5.0 # ... other deps ... # Local custom component (editable install) -e ./packages/gradio-niivue-viewer ``` **Alternative:** If the component is at repo root: ```text -e . ``` #### 6c. Verify .gitignore doesn't exclude build artifacts Check that `packages/gradio-niivue-viewer/.gitignore` doesn't exclude: - `gradio_niivue_viewer/templates/` - `frontend/dist/` - Any compiled `.js` or `.css` files needed at runtime If they're excluded, either: 1. Remove those lines from `.gitignore`, OR 2. Use `git add -f` to force-add them #### 6d. Test deployment flow ```bash # Simulate what HF Spaces does pip install -r requirements.txt python -m stroke_deepisles_demo.ui.app # Should work WITHOUT running gradio cc build ``` --- ## Existing References ### Working WebGL Custom Components 1. **[gradio-litmodel3d](https://pypi.org/project/gradio-litmodel3d/)** - WebGL Model3D viewer with HDR lighting - Source: https://github.com/gradio-app/gradio/tree/main/demo/model3d_component - Proof that WebGL works in Custom Components 2. **[gradio-molecule3d](https://pypi.org/project/gradio-molecule3d/)** - 3D molecule viewer - Uses Three.js (WebGL) ### Gradio Documentation - [Custom Components in 5 Minutes](https://www.gradio.app/guides/custom-components-in-five-minutes) - [Gradio Components Documentation](https://www.gradio.app/docs/gradio/components) - [Custom Component Gallery](https://www.gradio.app/custom-components/gallery) ### NiiVue Resources - [NiiVue GitHub](https://github.com/niivue/niivue) - [NiiVue npm](https://www.npmjs.com/package/@niivue/niivue) - [NiiVue Examples](https://niivue.com/docs/) --- ## Acceptance Criteria ### Must Have (MVP) - [ ] Component loads NIfTI volumes from Gradio file URLs - [ ] Component displays background image (DWI) - [ ] Component displays overlay mask (segmentation) with colormap - [ ] Component works on HuggingFace Spaces - [ ] No "Loading..." hang - failures are graceful - [ ] All existing tests pass ### Nice to Have (Future) - [ ] Crosshair controls - [ ] Slice orientation toggle (axial/coronal/sagittal) - [ ] Opacity slider for overlay - [ ] Pan/zoom/rotate controls - [ ] Screenshot/export functionality - [ ] Publish to PyPI for community use --- ## Risk Assessment | Risk | Mitigation | |------|------------| | Svelte/TypeScript learning curve | Follow gradio-litmodel3d example closely | | NiiVue WebGL2 browser support | Explicit WebGL2 check in Svelte + graceful error UI | | Build system complexity | Use gradio cc tooling, don't customize | | HF Spaces static file serving | Component bundles NiiVue, no external deps | | **Build artifacts not in git** | Phase 6a: Force-add compiled assets with `git add -f` | | **requirements.txt missing component** | Phase 6b: Add `-e ./packages/gradio-niivue-viewer` | --- ## Alternatives Considered ### Alternative 1: Keep Hacking gr.HTML - **Effort:** Low - **Success probability:** 0% (CONFIRMED FAILED) - **Why rejected:** We tried 6 approaches over 2 days. ALL failed. This is not a viable path. ### Alternative 2: Static HTML Space (No Gradio) - **Effort:** High (rebuild entire UI) - **Success probability:** 99% - **Why rejected:** Lose Gradio's file upload, dropdowns, layout features. Too much work. ### Alternative 3: Remove 3D Viewer (2D Only) - **Effort:** Low - **Success probability:** 100% - **Why rejected:** Loses key feature. Static Report tab already works, but 3D is valuable. --- ## Decision **Proceed with Gradio Custom Component approach.** This is the official Gradio-recommended solution. It's more work than hacking `gr.HTML`, but it's the architecturally correct approach with 90% success probability vs 30%. --- ## Testing Matrix ### Level 1: Local Build Verification ```bash cd packages/gradio-niivue-viewer # Build component gradio cc build # Install locally pip install -e . # Run demo app python demo/app.py # → Verify: App loads, no console errors, viewer renders ``` **Pass criteria:** - [ ] `gradio cc build` completes without errors - [ ] Demo app launches at localhost:7860 - [ ] No JavaScript console errors - [ ] Canvas renders (black background visible) ### Level 2: Volume Loading Test ```python # demo/app.py import gradio as gr from gradio_niivue_viewer import NiiVueViewer def load_sample(): # Use a known good NIfTI file return { "background_url": "/gradio_api/file=/path/to/sample.nii.gz", "overlay_url": None } with gr.Blocks() as demo: viewer = NiiVueViewer() btn = gr.Button("Load Sample") btn.click(load_sample, outputs=viewer) demo.launch() ``` **Pass criteria:** - [ ] NIfTI file loads without errors - [ ] Multiplanar view displays correctly - [ ] Overlay mask renders with red colormap (when provided) ### Level 3: HF Spaces Dry Run Deploy to a **private/throwaway Space** before production: ```bash # Create test space huggingface-cli repo create test-niivue-viewer --type space --private # Push and test git push hf-test main ``` **Pass criteria:** - [ ] Space shows "Running" (not stuck on "Loading...") - [ ] Viewer initializes (no hydration deadlock) - [ ] Volume loading works via Gradio file serving - [ ] WebGL2 error shown gracefully if unsupported ### Level 4: Integration Test Replace `gr.HTML` in stroke-deepisles-demo: ```python # src/stroke_deepisles_demo/ui/components.py from gradio_niivue_viewer import NiiVueViewer def create_results_display(): # ... niivue_viewer = NiiVueViewer(label="Interactive 3D Viewer") # ... ``` **Pass criteria:** - [ ] Existing 136 tests still pass - [ ] Segmentation pipeline works end-to-end - [ ] Viewer displays DWI + mask overlay - [ ] No "Loading..." hang on HF Spaces --- ## Next Steps 1. [x] Senior review of this spec (AUDIT_REPORT_2025_12_10.md) 2. [x] Red team review - all gaps addressed (build artifacts, npm install, null handling) 3. [ ] Create `packages/gradio-niivue-viewer/` subdirectory 4. [ ] Scaffold component with `gradio cc create` 5. [ ] Install NiiVue: `cd frontend && npm install @niivue/niivue@0.65.0` 6. [ ] Implement Svelte frontend (with WebGL2 checks + null value handling) 7. [ ] Implement Python backend 8. [ ] Level 1 test: Local build verification 9. [ ] Level 2 test: Volume loading 10. [ ] Level 3 test: HF Spaces dry run 11. [ ] Level 4 test: Integration 12. [ ] **CRITICAL**: Commit build artifacts to git 13. [ ] **CRITICAL**: Update requirements.txt with `-e ./packages/gradio-niivue-viewer` 14. [ ] (Optional) Publish to PyPI --- ## Appendix: Why WebGL + `gr.HTML` Doesn't Work From the ROOT_CAUSE_ANALYSIS.md and GRADIO_WEBGL_ANALYSIS.md research: 1. **Gradio CAN do WebGL** - proven by `gradio-litmodel3d` custom component 2. **But NOT via `gr.HTML`** - the `js_on_load` + `import()` pattern blocks Svelte hydration 3. **Our A/B test proved it** - disabling `js_on_load` makes the app load perfectly 4. **Gradio closed NIfTI support** (Issue #4511) - "Not planned for core" 5. **Gradio closed WebGL canvas** (Issue #7649) - "Not planned for core" 6. **gr.HTML strips script tags** - Security feature, can't bypass 7. **HF Spaces CSP blocks external CDNs** - Must vendor or bundle dependencies 8. **Gradio maintainer recommendation**: Custom Components The pattern is clear: **The `gr.HTML` + `js_on_load` + async `import()` pattern is fundamentally broken.** The Custom Component is the officially supported path for WebGL content.