File size: 6,788 Bytes
0edafbf b0a934c 0edafbf b0a934c 0edafbf b0a934c 0edafbf |
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 |
# 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)
|