stroke-deepisles-demo / docs /specs /29-codebase-status-audit.md
VibecoderMcSwaggins's picture
feat: Gradio Custom Component for NiiVue (#29)
227ab66 unverified
|
raw
history blame
9.64 kB

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:

// 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

# 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