VibecoderMcSwaggins commited on
Commit
5ac4ed0
Β·
1 Parent(s): 1a927f3

feat(docs): add comprehensive specifications for NiiVue integration and JavaScript loading issues

Browse files

- Introduced multiple new documentation files detailing the proposed Gradio Custom Component for NiiVue, addressing the "Loading..." issue on HuggingFace Spaces, and providing a root cause analysis of JavaScript loading failures.
- Included a detailed audit of JavaScript loading issues, diagnostic steps for the "Loading..." bug, and an analysis of Gradio's compatibility with WebGL.
- Documented the decision-making process and next steps for implementing the custom component approach, emphasizing the need for a structured solution to integrate NiiVue effectively.

These additions aim to enhance understanding and provide a clear path forward for developers working with Gradio and NiiVue.

docs/specs/28-gradio-custom-component-niivue.md ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spec #28: Gradio Custom Component for NiiVue
2
+
3
+ **Date:** 2025-12-10
4
+ **Status:** PROPOSED
5
+ **Blocks:** Issue #24 (HF Spaces "Loading..." forever)
6
+ **Effort:** Medium (2-3 days)
7
+ **Success Probability:** 90%
8
+
9
+ ---
10
+
11
+ ## Executive Summary
12
+
13
+ **The current `gr.HTML` + JavaScript approach will not work reliably.**
14
+
15
+ Gradio maintainers have explicitly closed both:
16
+ - [Issue #4511](https://github.com/gradio-app/gradio/issues/4511) - NIfTI/medical imaging support β†’ "Not planned"
17
+ - [Issue #7649](https://github.com/gradio-app/gradio/issues/7649) - WebGL canvas component β†’ "Not planned"
18
+
19
+ Their official answer: **"Create a Gradio Custom Component."**
20
+
21
+ This spec documents what we need to build to properly integrate NiiVue (WebGL2 medical imaging viewer) into our Gradio app.
22
+
23
+ ---
24
+
25
+ ## Why Current Approach Fails
26
+
27
+ ### What We've Tried
28
+
29
+ | Attempt | Why It Failed |
30
+ |---------|---------------|
31
+ | CDN import in js_on_load | HF Spaces CSP blocks external imports |
32
+ | Vendored NiiVue + dynamic import() | import() in js_on_load blocks Svelte hydration |
33
+ | head= parameter | Still uses ES module import, same problem |
34
+ | head_paths= parameter | Same as above |
35
+ | gr.set_static_paths() | File serving works, but JS loading mechanism broken |
36
+
37
+ ### Root Cause
38
+
39
+ **We're fighting Gradio's architecture.** Gradio is built with Svelte and has specific lifecycle expectations:
40
+
41
+ 1. `gr.HTML` strips `<script>` tags (security)
42
+ 2. `js_on_load` runs during component mount - async operations can block hydration
43
+ 3. ES module `import()` in any lifecycle hook can hang the entire app
44
+
45
+ **Gradio was not designed for custom WebGL content in `gr.HTML`.**
46
+
47
+ ---
48
+
49
+ ## The Solution: Gradio Custom Component
50
+
51
+ ### What Is a Gradio Custom Component?
52
+
53
+ A Custom Component is a proper Svelte + Python component that integrates with Gradio's architecture:
54
+
55
+ ```
56
+ gradio-niivue-viewer/
57
+ β”œβ”€β”€ frontend/
58
+ β”‚ β”œβ”€β”€ Index.svelte # Svelte component (renders NiiVue)
59
+ β”‚ β”œβ”€β”€ package.json # Frontend deps (including niivue)
60
+ β”‚ └── ...
61
+ β”œβ”€β”€ backend/
62
+ β”‚ └── gradio_niivue_viewer/
63
+ β”‚ └── __init__.py # Python component class
64
+ β”œβ”€β”€ pyproject.toml # Package definition
65
+ └── demo/
66
+ └── app.py # Example usage
67
+ ```
68
+
69
+ ### Why This Works
70
+
71
+ 1. **Svelte-native**: Component integrates with Gradio's lifecycle properly
72
+ 2. **Official pattern**: Gradio maintainers recommend this for WebGL
73
+ 3. **Isolated loading**: NiiVue loads within the component, not globally
74
+ 4. **Proper error handling**: Failures don't block app initialization
75
+ 5. **Reusable**: Can publish to PyPI for others to use
76
+
77
+ ---
78
+
79
+ ## Technical Approach
80
+
81
+ ### Phase 1: Scaffold Component (1 hour)
82
+
83
+ Use Gradio's CLI to create the component:
84
+
85
+ ```bash
86
+ gradio cc create NiiVueViewer \
87
+ --template Image \
88
+ --overwrite
89
+ ```
90
+
91
+ This creates the basic structure with Svelte frontend and Python backend.
92
+
93
+ ### Phase 2: Implement Svelte Frontend (4-6 hours)
94
+
95
+ Modify `frontend/Index.svelte`:
96
+
97
+ ```svelte
98
+ <script lang="ts">
99
+ import { onMount, onDestroy } from 'svelte';
100
+ import { Niivue } from '@niivue/niivue';
101
+ import type { FileData } from '@gradio/client';
102
+
103
+ export let value: {
104
+ background_url: string | null;
105
+ overlay_url: string | null;
106
+ } | null = null;
107
+
108
+ let container: HTMLDivElement;
109
+ let nv: Niivue | null = null;
110
+
111
+ onMount(async () => {
112
+ nv = new Niivue({
113
+ backColor: [0, 0, 0, 1],
114
+ show3Dcrosshair: true,
115
+ });
116
+ await nv.attachToCanvas(container.querySelector('canvas'));
117
+ await loadVolumes();
118
+ });
119
+
120
+ onDestroy(() => {
121
+ if (nv) nv.dispose();
122
+ });
123
+
124
+ async function loadVolumes() {
125
+ if (!nv || !value) return;
126
+ const volumes = [];
127
+ if (value.background_url) {
128
+ volumes.push({ url: value.background_url });
129
+ }
130
+ if (value.overlay_url) {
131
+ volumes.push({
132
+ url: value.overlay_url,
133
+ colormap: 'red',
134
+ opacity: 0.5,
135
+ });
136
+ }
137
+ if (volumes.length > 0) {
138
+ await nv.loadVolumes(volumes);
139
+ }
140
+ }
141
+
142
+ $: if (value && nv) loadVolumes();
143
+ </script>
144
+
145
+ <div bind:this={container} class="niivue-container">
146
+ <canvas></canvas>
147
+ </div>
148
+
149
+ <style>
150
+ .niivue-container {
151
+ width: 100%;
152
+ height: 500px;
153
+ background: #000;
154
+ }
155
+ canvas {
156
+ width: 100%;
157
+ height: 100%;
158
+ }
159
+ </style>
160
+ ```
161
+
162
+ ### Phase 3: Implement Python Backend (2-3 hours)
163
+
164
+ ```python
165
+ # backend/gradio_niivue_viewer/__init__.py
166
+ from __future__ import annotations
167
+ from typing import Any
168
+ from gradio.components.base import Component
169
+ from gradio.data_classes import FileData, GradioModel
170
+
171
+ class NiiVueViewerData(GradioModel):
172
+ background_url: str | None = None
173
+ overlay_url: str | None = None
174
+
175
+ class NiiVueViewer(Component):
176
+ """WebGL NIfTI viewer using NiiVue."""
177
+
178
+ data_model = NiiVueViewerData
179
+
180
+ def __init__(
181
+ self,
182
+ value: NiiVueViewerData | None = None,
183
+ *,
184
+ label: str | None = None,
185
+ height: int = 500,
186
+ **kwargs,
187
+ ):
188
+ self.height = height
189
+ super().__init__(value=value, label=label, **kwargs)
190
+
191
+ def preprocess(self, payload: NiiVueViewerData | None) -> dict[str, Any] | None:
192
+ if payload is None:
193
+ return None
194
+ return {
195
+ "background_url": payload.background_url,
196
+ "overlay_url": payload.overlay_url,
197
+ }
198
+
199
+ def postprocess(self, value: dict[str, Any] | None) -> NiiVueViewerData | None:
200
+ if value is None:
201
+ return None
202
+ return NiiVueViewerData(
203
+ background_url=value.get("background_url"),
204
+ overlay_url=value.get("overlay_url"),
205
+ )
206
+ ```
207
+
208
+ ### Phase 4: Build and Test (2-3 hours)
209
+
210
+ ```bash
211
+ # Build the component
212
+ cd gradio-niivue-viewer
213
+ gradio cc build
214
+
215
+ # Install locally
216
+ pip install -e .
217
+
218
+ # Test in demo app
219
+ python demo/app.py
220
+ ```
221
+
222
+ ### Phase 5: Integrate into stroke-deepisles-demo (1-2 hours)
223
+
224
+ Replace `gr.HTML` with the custom component:
225
+
226
+ ```python
227
+ # Before (broken)
228
+ from stroke_deepisles_demo.ui.viewer import create_niivue_html
229
+ viewer = gr.HTML(value="", elem_id="niivue-viewer")
230
+ # ... then set viewer.value = create_niivue_html(...)
231
+
232
+ # After (working)
233
+ from gradio_niivue_viewer import NiiVueViewer
234
+ viewer = NiiVueViewer(label="Interactive 3D Viewer")
235
+ # ... then set viewer.value = {"background_url": dwi_url, "overlay_url": mask_url}
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Existing References
241
+
242
+ ### Working WebGL Custom Components
243
+
244
+ 1. **[gradio-litmodel3d](https://pypi.org/project/gradio-litmodel3d/)**
245
+ - WebGL Model3D viewer with HDR lighting
246
+ - Source: https://github.com/gradio-app/gradio/tree/main/demo/model3d_component
247
+ - Proof that WebGL works in Custom Components
248
+
249
+ 2. **[gradio-molecule3d](https://pypi.org/project/gradio-molecule3d/)**
250
+ - 3D molecule viewer
251
+ - Uses Three.js (WebGL)
252
+
253
+ ### Gradio Documentation
254
+
255
+ - [Custom Components in 5 Minutes](https://www.gradio.app/guides/custom-components-in-five-minutes)
256
+ - [Gradio Components Documentation](https://www.gradio.app/docs/gradio/components)
257
+ - [Custom Component Gallery](https://www.gradio.app/custom-components/gallery)
258
+
259
+ ### NiiVue Resources
260
+
261
+ - [NiiVue GitHub](https://github.com/niivue/niivue)
262
+ - [NiiVue npm](https://www.npmjs.com/package/@niivue/niivue)
263
+ - [NiiVue Examples](https://niivue.com/docs/)
264
+
265
+ ---
266
+
267
+ ## Acceptance Criteria
268
+
269
+ ### Must Have (MVP)
270
+
271
+ - [ ] Component loads NIfTI volumes from Gradio file URLs
272
+ - [ ] Component displays background image (DWI)
273
+ - [ ] Component displays overlay mask (segmentation) with colormap
274
+ - [ ] Component works on HuggingFace Spaces
275
+ - [ ] No "Loading..." hang - failures are graceful
276
+ - [ ] All existing tests pass
277
+
278
+ ### Nice to Have (Future)
279
+
280
+ - [ ] Crosshair controls
281
+ - [ ] Slice orientation toggle (axial/coronal/sagittal)
282
+ - [ ] Opacity slider for overlay
283
+ - [ ] Pan/zoom/rotate controls
284
+ - [ ] Screenshot/export functionality
285
+ - [ ] Publish to PyPI for community use
286
+
287
+ ---
288
+
289
+ ## Risk Assessment
290
+
291
+ | Risk | Mitigation |
292
+ |------|------------|
293
+ | Svelte/TypeScript learning curve | Follow gradio-litmodel3d example closely |
294
+ | NiiVue WebGL2 browser support | NiiVue handles fallbacks internally |
295
+ | Build system complexity | Use gradio cc tooling, don't customize |
296
+ | HF Spaces static file serving | Component bundles NiiVue, no external deps |
297
+
298
+ ---
299
+
300
+ ## Alternatives Considered
301
+
302
+ ### Alternative 1: Keep Hacking gr.HTML
303
+ - **Effort:** Low
304
+ - **Success probability:** 30%
305
+ - **Why rejected:** We've tried 5+ approaches, all failed. Gradio architecture doesn't support this.
306
+
307
+ ### Alternative 2: Static HTML Space (No Gradio)
308
+ - **Effort:** High (rebuild entire UI)
309
+ - **Success probability:** 99%
310
+ - **Why rejected:** Lose Gradio's file upload, dropdowns, layout features. Too much work.
311
+
312
+ ### Alternative 3: Remove 3D Viewer (2D Only)
313
+ - **Effort:** Low
314
+ - **Success probability:** 100%
315
+ - **Why rejected:** Loses key feature. Static Report tab already works, but 3D is valuable.
316
+
317
+ ---
318
+
319
+ ## Decision
320
+
321
+ **Proceed with Gradio Custom Component approach.**
322
+
323
+ 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%.
324
+
325
+ ---
326
+
327
+ ## Next Steps
328
+
329
+ 1. [ ] Senior review of this spec
330
+ 2. [ ] Create `gradio-niivue-viewer` repository (or subdirectory)
331
+ 3. [ ] Scaffold component with `gradio cc create`
332
+ 4. [ ] Implement Svelte frontend
333
+ 5. [ ] Implement Python backend
334
+ 6. [ ] Test locally
335
+ 7. [ ] Test on HF Spaces
336
+ 8. [ ] Integrate into stroke-deepisles-demo
337
+ 9. [ ] (Optional) Publish to PyPI
338
+
339
+ ---
340
+
341
+ ## Appendix: Why WebGL + Gradio is Hard
342
+
343
+ From the ROOT_CAUSE_ANALYSIS.md and GRADIO_WEBGL_ANALYSIS.md research:
344
+
345
+ 1. **Gradio closed NIfTI support** (Issue #4511) - "Not planned"
346
+ 2. **Gradio closed WebGL canvas** (Issue #7649) - "Not planned"
347
+ 3. **gr.HTML strips script tags** - Security feature
348
+ 4. **js_on_load + import() blocks hydration** - Proven by A/B test
349
+ 5. **HF Spaces CSP blocks external CDNs** - No workaround for cdn imports
350
+ 6. **Gradio maintainer recommendation**: Custom Components
351
+
352
+ The pattern is clear: **Gradio doesn't natively support custom WebGL in gr.HTML.** The Custom Component is the only officially supported path.
AUDIT_JS_LOADING_ISSUES.md β†’ docs/specs/AUDIT_JS_LOADING_ISSUES.md RENAMED
File without changes
DIAGNOSTIC_HF_LOADING.md β†’ docs/specs/DIAGNOSTIC_HF_LOADING.md RENAMED
File without changes
GRADIO_WEBGL_ANALYSIS.md β†’ docs/specs/GRADIO_WEBGL_ANALYSIS.md RENAMED
File without changes
ROOT_CAUSE_ANALYSIS.md β†’ docs/specs/ROOT_CAUSE_ANALYSIS.md RENAMED
File without changes
docs/specs/{07-hf-spaces-deployment.md β†’ archive/07-hf-spaces-deployment.md} RENAMED
File without changes
docs/specs/{10-bug-niivue-viewer-black-screen.md β†’ archive/10-bug-niivue-viewer-black-screen.md} RENAMED
File without changes
docs/specs/{11-bug-niivue-js-on-load-not-rerunning.md β†’ archive/11-bug-niivue-js-on-load-not-rerunning.md} RENAMED
File without changes
docs/specs/{19-perf-base64-to-file-urls.md β†’ archive/19-perf-base64-to-file-urls.md} RENAMED
File without changes
docs/specs/{23-slice-comparison-overlay-bug.md β†’ archive/23-slice-comparison-overlay-bug.md} RENAMED
File without changes
docs/specs/{24-bug-hf-spaces-loading-forever.md β†’ archive/24-bug-hf-spaces-loading-forever.md} RENAMED
File without changes