# 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=`: **`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)