stroke-viewer-frontend / docs /specs /19-perf-base64-to-file-urls.md
Claude
fix(ui): replace base64 data URLs with Gradio file serving (#19)
b0a934c unverified
|
raw
history blame
6.79 kB
# 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:
```python
# 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
1. **Browser memory pressure** - Large base64 strings in DOM
2. **Slow loading times** - 65MB transferred per segmentation
3. **Gradio payload limits** - May hit internal limits on large responses
4. **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:
```python
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
```python
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:
```python
# 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()`
```python
# 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
```python
# 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
- [x] NiiVue loads DWI volume from file URL
- [x] NiiVue loads prediction mask overlay from file URL
- [x] No CORS errors in browser console (same-origin requests)
- [x] Loading time improved (no base64 encoding overhead)
- [x] Memory usage reduced (streaming vs. DOM strings)
- [x] Works on HF Spaces deployment (uses tempfile.gettempdir())
- [x] 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:**
```python
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:**
```python
dwi_url = nifti_to_gradio_url(dwi_path)
mask_url = nifti_to_gradio_url(result.prediction_mask)
```
### Why This Works
1. **Gradio allows temp files by default**: Files in `tempfile.gettempdir()` are
automatically accessible via the `/gradio_api/file=` endpoint.
2. **Pipeline results are in temp dir**: `run_pipeline_on_case()` creates results
in `tempfile.mkdtemp()`, which is under `tempfile.gettempdir()`.
3. **NiiVue supports HTTP URLs**: The `loadVolumes()` method can fetch from any
HTTP/HTTPS URL, including relative URLs served by Gradio.
4. **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
```python
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
1. NiiVue viewer loads volumes from file URLs (not base64)
2. No regression in viewer functionality
3. Measurable improvement in loading time or memory usage
4. All 130+ tests pass
5. Works on HF Spaces
---
## References
- [Gradio FileData API](https://www.gradio.app/docs/gradio/filedata)
- [Gradio File Serving](https://www.gradio.app/guides/file-access)
- [NiiVue Loading Volumes](https://niivue.github.io/niivue/features/loading.volumes.html)
- [Bug #10 - Secondary Issue 1](./10-bug-niivue-viewer-black-screen.md)