Issue #19: Replace Base64 Data URLs with File URLs for NiiVue Viewer
Status: RESOLVED β
Date: 2025-12-09 Resolved: 2025-12-09 Priority: P3 (Performance optimization) GitHub Issue: https://github.com/The-Obstacle-Is-The-Way/stroke-deepisles-demo/issues/19 Related: Bug #10, Bug #11 (both FIXED)
TL;DR
Replace base64-encoded data URLs (~65MB payloads) with Gradio's file serving for NiiVue volumes. The viewer works correctly now, but large payloads may cause slow loading or memory issues.
Problem
The NiiVue 3D viewer currently uses base64-encoded data URLs to pass NIfTI volumes to the browser:
# Current implementation in viewer.py
def nifti_to_data_url(nifti_path: Path) -> str:
"""Convert NIfTI file to base64 data URL."""
data = nifti_path.read_bytes()
b64 = base64.b64encode(data).decode("ascii")
return f"data:application/octet-stream;base64,{b64}"
Payload Size Analysis
| File | Raw Size | Base64 Size |
|---|---|---|
| DWI | 30.1 MB | ~40 MB |
| ADC | 17.7 MB | ~24 MB |
| Total | ~48 MB | ~65 MB |
Potential Issues
- Browser memory pressure - Large base64 strings in DOM
- Slow loading times - 65MB transferred per segmentation
- Gradio payload limits - May hit internal limits on large responses
- Mobile/low-bandwidth issues - Poor UX on slower connections
Proposed Solution
Use Gradio's built-in file serving instead of base64 data URLs.
Option A: Use gr.File component (Recommended)
Gradio automatically serves files and provides URLs:
from gradio import FileData
def nifti_to_file_url(nifti_path: Path) -> str:
"""Get Gradio file URL for NIfTI file."""
file_data = FileData(path=str(nifti_path))
return file_data.url # Returns /file=... URL served by Gradio
Option B: Use Gradio's file caching
import gradio as gr
# Gradio caches files and provides URLs
cached_path = gr.utils.get_upload_folder() / nifti_path.name
shutil.copy(nifti_path, cached_path)
file_url = f"/file={cached_path}"
Files to Modify
| File | Changes |
|---|---|
src/stroke_deepisles_demo/ui/viewer.py |
Replace nifti_to_data_url() with file URL function |
src/stroke_deepisles_demo/ui/app.py |
Update run_segmentation() to use file URLs |
Implementation Steps
Step 1: Research Gradio File Serving
Verify how Gradio serves files and what URL format NiiVue expects:
# Test script
import gradio as gr
from gradio import FileData
file_data = FileData(path="/path/to/test.nii.gz")
print(f"URL: {file_data.url}")
print(f"Type: {type(file_data.url)}")
Step 2: Update nifti_to_data_url() β nifti_to_file_url()
# viewer.py
def nifti_to_file_url(nifti_path: Path) -> str:
"""Get Gradio-served file URL for NIfTI file.
Args:
nifti_path: Path to NIfTI file
Returns:
URL string that Gradio will serve (e.g., /file=...)
"""
from gradio import FileData
file_data = FileData(path=str(nifti_path))
return file_data.url
Step 3: Update app.py to Use File URLs
# app.py - run_segmentation()
# Replace:
dwi_url = nifti_to_data_url(dwi_path)
mask_url = nifti_to_data_url(result.prediction_mask)
# With:
dwi_url = nifti_to_file_url(dwi_path)
mask_url = nifti_to_file_url(result.prediction_mask)
Step 4: Test NiiVue with File URLs
Verify NiiVue can load from Gradio's file URLs:
- Check CORS headers
- Verify Content-Type header
- Test with different browsers
Step 5: Cleanup
Remove or deprecate nifti_to_data_url() if no longer needed.
Testing Checklist
- NiiVue loads DWI volume from file URL
- NiiVue loads prediction mask overlay from file URL
- No CORS errors in browser console (same-origin requests)
- Loading time improved (no base64 encoding overhead)
- Memory usage reduced (streaming vs. DOM strings)
- Works on HF Spaces deployment (uses tempfile.gettempdir())
- All existing tests pass (134 tests)
Implementation Details
Final Implementation (2025-12-09)
The solution uses Gradio's built-in file serving at /gradio_api/file=<path>:
viewer.py - New function:
def nifti_to_gradio_url(nifti_path: Path) -> str:
"""Get Gradio file URL for a NIfTI file."""
abs_path = nifti_path.resolve()
return f"/gradio_api/file={abs_path}"
app.py - Updated usage:
dwi_url = nifti_to_gradio_url(dwi_path)
mask_url = nifti_to_gradio_url(result.prediction_mask)
Why This Works
Gradio allows temp files by default: Files in
tempfile.gettempdir()are automatically accessible via the/gradio_api/file=endpoint.Pipeline results are in temp dir:
run_pipeline_on_case()creates results intempfile.mkdtemp(), which is undertempfile.gettempdir().NiiVue supports HTTP URLs: The
loadVolumes()method can fetch from any HTTP/HTTPS URL, including relative URLs served by Gradio.Same-origin requests: Since NiiVue's JavaScript runs in the browser and requests files from the same Gradio server, there are no CORS issues.
Tests Added
class TestNiftiToGradioUrl:
def test_returns_gradio_api_format(self, synthetic_nifti_3d: Path) -> None:
url = nifti_to_gradio_url(synthetic_nifti_3d)
assert url.startswith("/gradio_api/file=")
def test_uses_absolute_path(self, synthetic_nifti_3d: Path) -> None:
url = nifti_to_gradio_url(synthetic_nifti_3d)
path_part = url.replace("/gradio_api/file=", "")
assert path_part.startswith("/")
def test_no_base64_encoding(self, synthetic_nifti_3d: Path) -> None:
url = nifti_to_gradio_url(synthetic_nifti_3d)
assert not url.startswith("data:")
assert ";base64," not in url
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| CORS issues | Gradio should handle CORS for its own file serving |
| NiiVue URL format | Test that NiiVue accepts relative URLs |
| File cleanup | Gradio handles temp file cleanup automatically |
| Security | Gradio's file serving is sandboxed to allowed paths |
Acceptance Criteria
- NiiVue viewer loads volumes from file URLs (not base64)
- No regression in viewer functionality
- Measurable improvement in loading time or memory usage
- All 130+ tests pass
- Works on HF Spaces