# 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)
**Success Probability:** 90%
---
## 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 Gradio's architecture.** Gradio is built with Svelte and has specific lifecycle expectations:
1. `gr.HTML` strips `
```
### 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}
```
---
## 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 | NiiVue handles fallbacks internally |
| Build system complexity | Use gradio cc tooling, don't customize |
| HF Spaces static file serving | Component bundles NiiVue, no external deps |
---
## 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%.
---
## Next Steps
1. [ ] Senior review of this spec
2. [ ] Create `gradio-niivue-viewer` repository (or subdirectory)
3. [ ] Scaffold component with `gradio cc create`
4. [ ] Implement Svelte frontend
5. [ ] Implement Python backend
6. [ ] Test locally
7. [ ] Test on HF Spaces
8. [ ] Integrate into stroke-deepisles-demo
9. [ ] (Optional) Publish to PyPI
---
## Appendix: Why WebGL + Gradio is Hard
From the ROOT_CAUSE_ANALYSIS.md and GRADIO_WEBGL_ANALYSIS.md research:
1. **Gradio closed NIfTI support** (Issue #4511) - "Not planned"
2. **Gradio closed WebGL canvas** (Issue #7649) - "Not planned"
3. **gr.HTML strips script tags** - Security feature
4. **js_on_load + import() blocks hydration** - Proven by A/B test
5. **HF Spaces CSP blocks external CDNs** - No workaround for cdn imports
6. **Gradio maintainer recommendation**: Custom Components
The pattern is clear: **Gradio doesn't natively support custom WebGL in gr.HTML.** The Custom Component is the only officially supported path.