File size: 9,638 Bytes
227ab66 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# Spec #29: Codebase Status Audit (Issue #24 NiiVue/WebGL)
**Date:** 2025-12-10
**Status:** ALL `gr.HTML` HACKS CONFIRMED FAILED (Dec 10, 2025)
**Purpose:** Top-down analysis of current frontend/NiiVue implementation state after multiple hotfix attempts
---
## Executive Summary: The `gr.HTML` + `js_on_load` + `import()` Pattern is Broken
After 6 iterations of attempted hotfixes for Issue #24 (HF Spaces "Loading..." forever), **every `gr.HTML`-based approach has failed**:
| Attempt | Result |
|---------|--------|
| CDN import | FAILED - CSP blocked |
| Vendored + js_on_load import() | FAILED - Blocks Svelte hydration |
| head_paths | FAILED - Same hydration issue |
| head= with import() | **FAILED** - Confirmed Dec 10 |
**Root Cause (PROVEN):** Async `import()` inside `js_on_load` blocks Gradio's Svelte hydration. Our A/B test confirmed: disabling `js_on_load` makes the app load.
**Clarification:** Gradio CAN do WebGL via Custom Components (`gradio-litmodel3d` proves this). The issue is the `gr.HTML` approach, not Gradio itself.
**The correct solution is Gradio Custom Component (spec #28).**
---
## Current Frontend Architecture
### File Inventory
| File | Purpose | Lines | Status |
|------|---------|-------|--------|
| `ui/viewer.py` | NiiVue HTML/JS generation | 643 | **BLOATED** - contains 5 approaches |
| `ui/app.py` | Main Gradio app | 313 | Clean |
| `ui/components.py` | UI components | 94 | Clean |
| `app.py` (root) | Local dev entry | 61 | Clean |
| `ui/assets/niivue.js` | Vendored NiiVue v0.65.0 | 2.9MB | **NECESSARY** |
### What's in `viewer.py` Right Now
| Component | Lines | Status | Notes |
|-----------|-------|--------|-------|
| `NIIVUE_VERSION` | 30 | OK | Version tracking |
| `_ASSET_DIR`, `_NIIVUE_JS_PATH` | 31-32 | OK | Path constants |
| `NIIVUE_JS_URL` | 36 | **UNUSED** | Computed but not actually used |
| Module-level logging | 39-42 | **SLOP** | 4 log statements at import time |
| `get_niivue_head_html()` | 45-77 | **PROBLEMATIC** | Still uses `await import()` |
| `get_niivue_loader_path()` | 80-109 | **DEPRECATED** | Marked deprecated but still exists |
| `nifti_to_gradio_url()` | 112-142 | OK | Issue #19 fix, working |
| `get_slice_at_max_lesion()` | 145-187 | OK | Matplotlib helper |
| `render_3panel_view()` | 190-281 | OK | Matplotlib 3-panel |
| `render_slice_comparison()` | 284-380 | OK | Matplotlib comparison |
| `create_niivue_html()` | 383-434 | OK | HTML generation |
| `NIIVUE_ON_LOAD_JS` | 449-538 | **MOSTLY OK** | No import(), uses window.Niivue |
| `NIIVUE_UPDATE_JS` | 546-642 | **MOSTLY OK** | No import(), uses window.Niivue |
---
## The Core Problem: `get_niivue_head_html()` Still Uses `import()`
The current "fix" in `get_niivue_head_html()` does this:
```javascript
// viewer.py:63-76
<script type="module">
try {
const niivueUrl = '{NIIVUE_JS_URL}';
console.log('[NiiVue Loader] Attempting to load from:', niivueUrl);
const { Niivue } = await import(niivueUrl); // <-- SAME BROKEN PATTERN!
window.Niivue = Niivue;
console.log('[NiiVue Loader] Successfully loaded');
} catch (error) {
console.error('[NiiVue Loader] FAILED to load:', error);
window.NIIVUE_LOAD_ERROR = error.message;
}
</script>
```
**This is the EXACT same `await import()` pattern that breaks on HF Spaces.**
The only difference from our previous attempts:
- Before: `await import()` in `js_on_load`
- Now: `await import()` in `head=` script
**Why this might not matter:** The A/B test proved that `js_on_load` with async code breaks Gradio. Moving the `import()` to `head=` might help, but it's still executing async code that could fail silently and leave `window.Niivue` undefined.
---
## What's Necessary vs What's Slop
### NECESSARY (Keep)
| Item | Why |
|------|-----|
| `ui/assets/niivue.js` | HF Spaces CSP blocks CDN imports |
| `gr.set_static_paths()` | Required for Gradio 6.x file serving |
| `nifti_to_gradio_url()` | Issue #19 fix, working |
| `create_niivue_html()` | Generates viewer HTML |
| `NIIVUE_ON_LOAD_JS` | Initializes viewer (doesn't import) |
| `NIIVUE_UPDATE_JS` | Re-initializes after updates |
| Matplotlib functions | Working 2D fallback |
| `allowed_paths` in launch() | Runtime file access |
### SLOP (Should Remove/Refactor)
| Item | Why It's Slop |
|------|---------------|
| `NIIVUE_JS_URL` module-level computation | Computed but unused in production |
| Module-level logging (lines 39-42) | Noisy startup logs, not useful |
| `get_niivue_loader_path()` | Deprecated, generates file we don't need |
| `get_niivue_head_html()` with import() | Still uses broken pattern |
| Multiple diagnostic docs | Overlapping, contradictory, stale |
### UNCERTAIN (Depends on head= fix working)
| Item | Status |
|------|--------|
| `head=get_niivue_head_html()` in launch() | **30% chance this works** |
---
## Documentation Status
### docs/specs/ Files
| File | Status | Issue |
|------|--------|-------|
| `00-context.md` | **ACCURATE** | None |
| `28-gradio-custom-component-niivue.md` | **ACCURATE** | Just written |
| `AUDIT_JS_LOADING_ISSUES.md` | **OUTDATED** | Says `set_static_paths` is blocker, but we've moved past that |
| `DIAGNOSTIC_HF_LOADING.md` | **OUTDATED** | Lists hypotheses we've since disproven |
| `ROOT_CAUSE_ANALYSIS.md` | **PARTIALLY OUTDATED** | Says "IN PROGRESS", discusses head= as solution |
| `GRADIO_WEBGL_ANALYSIS.md` | **ACCURATE** | Core analysis, identifies real problem |
### docs/TECHNICAL_DEBT.md
| Status | Issue |
|--------|-------|
| **OUTDATED** | Claims "Ironclad/Production-Ready" but doesn't mention P0 NiiVue/WebGL blocker |
---
## Recommended Cleanup Actions
### Immediate (If head= fix fails)
1. **Delete deprecated code:**
- Remove `get_niivue_loader_path()`
- Remove module-level logging
- Clean up `NIIVUE_JS_URL` if unused
2. **Archive old diagnostic docs:**
- Move `AUDIT_JS_LOADING_ISSUES.md` to `archive/`
- Move `DIAGNOSTIC_HF_LOADING.md` to `archive/`
- Update `ROOT_CAUSE_ANALYSIS.md` status
3. **Update TECHNICAL_DEBT.md:**
- Add P0 section for NiiVue/WebGL blocker
- Link to spec #28 (Custom Component)
### Long-term (After decision on path forward)
1. **If Custom Component route:**
- Remove all `head=` NiiVue loading code
- Remove `get_niivue_head_html()`
- Simplify `viewer.py` to just Matplotlib functions
- NiiVue loading becomes the component's responsibility
2. **If 2D fallback route:**
- Remove entire NiiVue integration
- Remove `ui/assets/niivue.js` (2.9MB)
- Remove `NIIVUE_ON_LOAD_JS`, `NIIVUE_UPDATE_JS`
- Keep only Matplotlib rendering
---
## Honest Assessment
### What We've Tried (6+ iterations)
1. **CDN import** β Blocked by CSP
2. **Vendored + dynamic import in js_on_load** β Blocks Svelte hydration
3. **head_paths with loader HTML** β Complex, didn't work
4. **head= with inline import()** β Current state, **probably won't work**
5. **Various set_static_paths/allowed_paths combos** β File serving works, JS loading doesn't
### The Pattern
Every attempt has been a variation of:
> "Load NiiVue via some JavaScript mechanism within Gradio"
Every attempt has failed because:
> **Gradio was not designed for custom WebGL content**
### The Correct Solution
**Stop fighting Gradio's architecture. Use a Gradio Custom Component.**
This is:
- What Gradio maintainers recommend (Issues #4511, #7649)
- How existing WebGL components work (gradio-litmodel3d)
- 90% success probability vs 30% for more hacks
See spec #28 for implementation details.
---
## Current Entry Point Flow
```
HF Spaces Docker
β
CMD ["python", "-m", "stroke_deepisles_demo.ui.app"]
β
ui/app.py __main__ block
β
gr.set_static_paths([_ASSETS_DIR]) # Enable file serving
β
get_demo() # Creates Blocks with js_on_load components
β
demo.launch(
head=get_niivue_head_html(), # <-- Injects <script type="module"> with import()
allowed_paths=[_ASSETS_DIR],
)
β
Browser loads page
β
<head> script runs: await import('/gradio_api/file=.../niivue.js')
β
[UNCERTAIN] Does import() succeed? Does it block Svelte?
β
If yes: window.Niivue is set, js_on_load works
If no: window.Niivue undefined, viewer shows error
```
---
## Files Modified During Issue #24 Debug
| File | Changes | Commits |
|------|---------|---------|
| `viewer.py` | ~6 rewrites of JS loading approach | Multiple |
| `ui/app.py` | Added head=, set_static_paths | Multiple |
| `app.py` | Same as ui/app.py | Multiple |
| `ui/assets/niivue.js` | Added vendored library | 1 |
| `.gitignore` | Added niivue-loader.html | 1 |
| `.pre-commit-config.yaml` | Exclude assets/ from large file check | 1 |
---
## Conclusion
**The codebase is messy but not unfixable.** The mess comes from iterating through multiple failed approaches without cleaning up between attempts.
**The real issue is architectural:** Gradio + custom WebGL = unsupported pattern.
**Next steps:**
1. Test if current `head=` approach works on HF Spaces (low confidence)
2. If it fails, implement Gradio Custom Component (spec #28)
3. Clean up cruft regardless of which path we take
---
## Appendix: How to Verify Current State
```bash
# Check if NiiVue file serving works
curl -I "https://[space-url]/gradio_api/file=/home/user/demo/src/stroke_deepisles_demo/ui/assets/niivue.js"
# Should return 200 OK with application/javascript
# Check browser console for:
# - "[NiiVue Loader] Attempting to load from: ..."
# - "[NiiVue Loader] Successfully loaded" OR "[NiiVue Loader] FAILED"
# - Any errors during Gradio initialization
```
|