fix(ui): simplify NiiVue loading, add diagnostic logging (#24)
Browse filesRoot cause analysis from external agent suggests the "Loading..." hang
is caused by JavaScript failing to load due to path resolution issues
or silent errors.
Changes based on external agent feedback:
1. **Use `head` param instead of `head_paths`**
- Eliminates file I/O that could fail on HF Spaces
- Inline script injection is simpler and more reliable
2. **Add diagnostic logging at startup**
- Log asset directory paths at module load time
- Log whether assets exist for debugging HF Spaces issues
3. **Improve error handling in NiiVue loader**
- Add try/catch around ES module import
- Store errors in window.NIIVUE_LOAD_ERROR for debugging
- Update js_on_load to check for loader errors
4. **Clean up root app.py**
- Clarify it's NOT used by HF Spaces Docker (CMD uses ui/app.py)
- Update to use same `head` approach as ui/app.py
5. **Add DIAGNOSTIC_HF_LOADING.md**
- Documents all research and hypotheses
- Lists external agent's analysis and recommendations
All 136 tests pass. Lint and type checks clean.
- DIAGNOSTIC_HF_LOADING.md +228 -0
- app.py +17 -18
- src/stroke_deepisles_demo/ui/app.py +12 -4
- src/stroke_deepisles_demo/ui/viewer.py +58 -28
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Diagnostic: HuggingFace Spaces "Loading..." Forever Bug
|
| 2 |
+
|
| 3 |
+
**Date**: 2025-12-10
|
| 4 |
+
**Status**: UNRESOLVED - App stuck on "Loading..." despite backend running
|
| 5 |
+
**Symptom**: HF Spaces shows "Running on T4", logs show successful startup, but UI never renders
|
| 6 |
+
|
| 7 |
+
## Observed Behavior
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
===== Application Startup at 2025-12-10 02:10:47 =====
|
| 11 |
+
* Running on local URL: http://0.0.0.0:7860
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
- Backend Python starts successfully
|
| 15 |
+
- Gradio server binds to `0.0.0.0:7860` (correct for Docker)
|
| 16 |
+
- Frontend shows Gradio "Loading..." spinner indefinitely
|
| 17 |
+
- No error messages in container logs
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## Research Findings
|
| 22 |
+
|
| 23 |
+
### 1. Known Gradio Issues with Custom JavaScript
|
| 24 |
+
|
| 25 |
+
#### Issue #11649: Custom JS via `head` fails with 404
|
| 26 |
+
**Source**: [GitHub Issue #11649](https://github.com/gradio-app/gradio/issues/11649)
|
| 27 |
+
|
| 28 |
+
Users report custom JavaScript files failing to load even with `allowed_paths` configured.
|
| 29 |
+
|
| 30 |
+
**Solution found**: Use `head_paths` parameter instead of `head`:
|
| 31 |
+
```python
|
| 32 |
+
with gr.Blocks(head_paths=["custom.html"]) as demo:
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
Alternative URL format that works: `/gradio_api/file=static/custom.js` instead of `/file/static/custom.js`
|
| 36 |
+
|
| 37 |
+
#### Issue #10250: JavaScript in `head` param not executing
|
| 38 |
+
**Source**: [GitHub Issue #10250](https://github.com/gradio-app/gradio/issues/10250)
|
| 39 |
+
|
| 40 |
+
JavaScript occasionally executes after extended delays (5-10+ minutes) or not at all.
|
| 41 |
+
|
| 42 |
+
**Key insight**: `gr.HTML()` components intentionally do NOT execute JavaScript for security. Only `head` parameter supports JS execution.
|
| 43 |
+
|
| 44 |
+
#### Issue #6426: gr.Blocks head argument not working
|
| 45 |
+
**Source**: [GitHub Issue #6426](https://github.com/gradio-app/gradio/issues/6426)
|
| 46 |
+
|
| 47 |
+
Two critical bugs:
|
| 48 |
+
1. Only the FIRST script tag from `head` is applied
|
| 49 |
+
2. Script tags are injected AFTER page loads, preventing execution
|
| 50 |
+
|
| 51 |
+
**Fixed in PR #6639** - but may require specific Gradio version.
|
| 52 |
+
|
| 53 |
+
### 2. ES Module Script Behavior
|
| 54 |
+
|
| 55 |
+
**Source**: [ES Modules Explainer](https://gist.github.com/jakub-g/385ee6b41085303a53ad92c7c8afd7a6)
|
| 56 |
+
|
| 57 |
+
Key facts about `<script type="module">`:
|
| 58 |
+
- **Always deferred by default** - does NOT block HTML parsing
|
| 59 |
+
- **No way to make it blocking** - even without async/defer
|
| 60 |
+
- If import fails, script silently fails but shouldn't block page
|
| 61 |
+
|
| 62 |
+
**Implication**: Our NiiVue loader (`<script type="module">`) should NOT be blocking Gradio's rendering. The issue is elsewhere.
|
| 63 |
+
|
| 64 |
+
### 3. HuggingFace Spaces Known Issues
|
| 65 |
+
|
| 66 |
+
#### Origin Mismatch Bug (Issue #10893)
|
| 67 |
+
**Source**: [HF Forum Thread](https://discuss.huggingface.co/t/gradio-space-javascript-not-executing-fields-not-populating-persistent-syntaxerror-in-browser-console/163689)
|
| 68 |
+
|
| 69 |
+
Gradio's `postMessage` calls fail due to origin mismatch between `https://huggingface.co` and the actual Space URL. This can prevent JavaScript execution entirely.
|
| 70 |
+
|
| 71 |
+
#### Docker "Running" But "Loading..." Forever
|
| 72 |
+
**Source**: [HF Forum Thread](https://discuss.huggingface.co/t/space-stuck-in-building-despite-gradio-welcome-port/36890)
|
| 73 |
+
|
| 74 |
+
Common causes:
|
| 75 |
+
- Binding to `127.0.0.1` instead of `0.0.0.0` (**we already fixed this**)
|
| 76 |
+
- Incorrect port configuration (**we use 7860**)
|
| 77 |
+
- Private Space authentication issues (**our Space is public**)
|
| 78 |
+
|
| 79 |
+
#### Interface Not Showing Despite Running
|
| 80 |
+
**Source**: [HF Forum Thread](https://discuss.huggingface.co/t/bug-free-gradio-interface-not-loading-on-hfs/25102)
|
| 81 |
+
|
| 82 |
+
Assigning Interface to a variable before launching can cause this:
|
| 83 |
+
```python
|
| 84 |
+
# WRONG
|
| 85 |
+
iface = gr.Interface(...).launch()
|
| 86 |
+
|
| 87 |
+
# RIGHT
|
| 88 |
+
gr.Interface(...).launch()
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
**Our code**: We use `get_demo().launch()` which SHOULD be correct.
|
| 92 |
+
|
| 93 |
+
### 4. Gradio set_static_paths Requirements
|
| 94 |
+
|
| 95 |
+
**Source**: [Gradio Docs](https://www.gradio.app/docs/gradio/set_static_paths)
|
| 96 |
+
|
| 97 |
+
- Must be called BEFORE creating Blocks
|
| 98 |
+
- Affects ALL Gradio apps in the same interpreter session
|
| 99 |
+
- Exposes entire directories to network (security consideration)
|
| 100 |
+
|
| 101 |
+
**Our code**: We call `gr.set_static_paths()` at module level before imports. ✅
|
| 102 |
+
|
| 103 |
+
### 5. Browser Cache Issues
|
| 104 |
+
|
| 105 |
+
**Source**: [HF Forum Thread](https://discuss.huggingface.co/t/issue-with-perpetual-loading-on-the-space/35684)
|
| 106 |
+
|
| 107 |
+
Some "Loading..." issues resolved by clearing browser cache. Works in one browser but not another.
|
| 108 |
+
|
| 109 |
+
**Unlikely cause**: This is a fresh deployment, not a cache issue.
|
| 110 |
+
|
| 111 |
+
---
|
| 112 |
+
|
| 113 |
+
## Our Current Implementation
|
| 114 |
+
|
| 115 |
+
### JavaScript Loading Flow
|
| 116 |
+
|
| 117 |
+
1. `app.py` (root entry point for HF Spaces Docker):
|
| 118 |
+
- Calls `gr.set_static_paths(paths=[str(_ASSETS_DIR)])` before imports
|
| 119 |
+
- Imports `get_demo()` and `get_niivue_loader_path()`
|
| 120 |
+
- Launches with `head_paths=[str(niivue_loader)]`
|
| 121 |
+
|
| 122 |
+
2. `ui/app.py` (also calls set_static_paths):
|
| 123 |
+
- Module-level `gr.set_static_paths()` before imports
|
| 124 |
+
- Creates demo with `js_on_load=NIIVUE_ON_LOAD_JS` on gr.HTML component
|
| 125 |
+
|
| 126 |
+
3. `viewer.py`:
|
| 127 |
+
- Generates `niivue-loader.html` at runtime with absolute path
|
| 128 |
+
- Content: `<script type="module">import { Niivue } from '/gradio_api/file=...'</script>`
|
| 129 |
+
|
| 130 |
+
### Files Involved
|
| 131 |
+
|
| 132 |
+
| File | Purpose | Status |
|
| 133 |
+
|------|---------|--------|
|
| 134 |
+
| `app.py` (root) | HF Spaces entry point | Uses head_paths ✅ |
|
| 135 |
+
| `src/.../ui/app.py` | Main UI module | Uses js_on_load ✅ |
|
| 136 |
+
| `src/.../ui/viewer.py` | NiiVue loader generation | Generates at runtime ✅ |
|
| 137 |
+
| `src/.../ui/components.py` | UI components | Uses NIIVUE_ON_LOAD_JS ✅ |
|
| 138 |
+
| `src/.../ui/assets/niivue.js` | Vendored NiiVue library | 2.9MB, tracked ✅ |
|
| 139 |
+
| `src/.../ui/assets/niivue-loader.html` | Generated loader | gitignored ✅ |
|
| 140 |
+
|
| 141 |
+
### Dockerfile CMD
|
| 142 |
+
|
| 143 |
+
```dockerfile
|
| 144 |
+
CMD ["python", "-m", "stroke_deepisles_demo.ui.app"]
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
This runs `ui/app.py` as `__main__`, which should execute our launch() with head_paths.
|
| 148 |
+
|
| 149 |
+
---
|
| 150 |
+
|
| 151 |
+
## Hypotheses
|
| 152 |
+
|
| 153 |
+
### H1: `js_on_load` Breaking Gradio Initialization
|
| 154 |
+
|
| 155 |
+
**Theory**: The `js_on_load` parameter on `gr.HTML` might be executing before Gradio fully initializes, causing a crash.
|
| 156 |
+
|
| 157 |
+
**Evidence**: Our code has `js_on_load=NIIVUE_ON_LOAD_JS` which is a complex async IIFE.
|
| 158 |
+
|
| 159 |
+
**Test**: Remove `js_on_load` parameter and see if app loads.
|
| 160 |
+
|
| 161 |
+
### H2: `head_paths` Not Being Applied on HF Spaces
|
| 162 |
+
|
| 163 |
+
**Theory**: The `head_paths` parameter might not be reaching the frontend on HF Spaces due to Docker networking or Gradio configuration.
|
| 164 |
+
|
| 165 |
+
**Evidence**: Issue #11649 shows head-related parameters have bugs.
|
| 166 |
+
|
| 167 |
+
**Test**: Check browser Network tab for niivue.js 404 or missing script.
|
| 168 |
+
|
| 169 |
+
### H3: demo.load() Blocking Initial Render
|
| 170 |
+
|
| 171 |
+
**Theory**: The `demo.load(initialize_case_selector, ...)` call might be blocking the initial UI render.
|
| 172 |
+
|
| 173 |
+
**Evidence**: `initialize_case_selector()` calls `list_case_ids()` which loads HuggingFace dataset.
|
| 174 |
+
|
| 175 |
+
**Test**: Remove demo.load() and see if app loads.
|
| 176 |
+
|
| 177 |
+
### H4: Double set_static_paths Causing Conflict
|
| 178 |
+
|
| 179 |
+
**Theory**: Both `app.py` (root) and `ui/app.py` call `gr.set_static_paths()`. This might cause conflicts.
|
| 180 |
+
|
| 181 |
+
**Evidence**: Gradio docs say "affects ALL Gradio apps in same interpreter session".
|
| 182 |
+
|
| 183 |
+
**Test**: Remove one of the set_static_paths calls.
|
| 184 |
+
|
| 185 |
+
### H5: Module Import Order Issue
|
| 186 |
+
|
| 187 |
+
**Theory**: The order of imports and set_static_paths calls might matter on HF Spaces but not locally.
|
| 188 |
+
|
| 189 |
+
**Evidence**: We have `noqa: E402` comments indicating non-standard import order.
|
| 190 |
+
|
| 191 |
+
**Test**: Trace exact import order and when set_static_paths is effective.
|
| 192 |
+
|
| 193 |
+
### H6: Path Resolution Different in Docker
|
| 194 |
+
|
| 195 |
+
**Theory**: `Path(__file__).resolve()` might resolve to different paths in Docker vs local.
|
| 196 |
+
|
| 197 |
+
**Evidence**: We use absolute paths for NIIVUE_JS_URL computed at import time.
|
| 198 |
+
|
| 199 |
+
**Test**: Log the actual paths being computed in Docker.
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
## Diagnostic Steps to Try
|
| 204 |
+
|
| 205 |
+
1. **Minimal Test**: Create a branch that removes ALL custom JS and test if basic Gradio loads
|
| 206 |
+
2. **Log Paths**: Add logging to show exactly what paths are computed in Docker
|
| 207 |
+
3. **Browser DevTools**: Check Network tab and Console for errors (if accessible)
|
| 208 |
+
4. **Gradio Version**: Verify we're using a version with all relevant fixes
|
| 209 |
+
5. **HF Spaces Logs**: Check full container logs for any Python errors not shown in UI
|
| 210 |
+
|
| 211 |
+
---
|
| 212 |
+
|
| 213 |
+
## Related Documentation
|
| 214 |
+
|
| 215 |
+
- [AUDIT_JS_LOADING_ISSUES.md](./AUDIT_JS_LOADING_ISSUES.md) - Previous audit of JavaScript loading issues
|
| 216 |
+
- [docs/specs/24-bug-hf-spaces-loading-forever.md](./docs/specs/24-bug-hf-spaces-loading-forever.md) - Original bug specification
|
| 217 |
+
|
| 218 |
+
---
|
| 219 |
+
|
| 220 |
+
## External Resources
|
| 221 |
+
|
| 222 |
+
- [Gradio Custom CSS and JS Guide](https://www.gradio.app/guides/custom-CSS-and-JS)
|
| 223 |
+
- [Gradio File Access Guide](https://www.gradio.app/guides/file-access)
|
| 224 |
+
- [Gradio set_static_paths Docs](https://www.gradio.app/docs/gradio/set_static_paths)
|
| 225 |
+
- [Gradio Issue #11649](https://github.com/gradio-app/gradio/issues/11649) - head_paths solution
|
| 226 |
+
- [Gradio Issue #10250](https://github.com/gradio-app/gradio/issues/10250) - head JS not executing
|
| 227 |
+
- [Gradio Issue #6426](https://github.com/gradio-app/gradio/issues/6426) - head argument bugs
|
| 228 |
+
- [HF Spaces Docker Guide](https://huggingface.co/docs/hub/spaces-sdks-docker)
|
|
@@ -1,12 +1,9 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
configures Gradio appropriately for the deployment environment.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
- docs/specs/07-hf-spaces-deployment.md
|
| 9 |
-
- https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 10 |
"""
|
| 11 |
|
| 12 |
from pathlib import Path
|
|
@@ -14,15 +11,16 @@ from pathlib import Path
|
|
| 14 |
import gradio as gr
|
| 15 |
|
| 16 |
# CRITICAL: Allow direct file serving for local assets (niivue.js)
|
| 17 |
-
# This fixes the P0 "Loading..." bug on HF Spaces (Issue #11649)
|
| 18 |
# Must be called BEFORE creating any Blocks
|
| 19 |
_ASSETS_DIR = Path(__file__).parent / "src" / "stroke_deepisles_demo" / "ui" / "assets"
|
| 20 |
gr.set_static_paths(paths=[str(_ASSETS_DIR)])
|
| 21 |
|
| 22 |
from stroke_deepisles_demo.core.config import get_settings # noqa: E402
|
| 23 |
-
from stroke_deepisles_demo.core.logging import setup_logging # noqa: E402
|
| 24 |
from stroke_deepisles_demo.ui.app import get_demo # noqa: E402
|
| 25 |
-
from stroke_deepisles_demo.ui.viewer import
|
|
|
|
|
|
|
| 26 |
|
| 27 |
# Initialize logging
|
| 28 |
settings = get_settings()
|
|
@@ -32,14 +30,15 @@ setup_logging(settings.log_level, format_style=settings.log_format)
|
|
| 32 |
demo = get_demo()
|
| 33 |
|
| 34 |
if __name__ == "__main__":
|
| 35 |
-
#
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
| 40 |
|
| 41 |
-
#
|
| 42 |
-
|
| 43 |
|
| 44 |
demo.launch(
|
| 45 |
server_name=settings.gradio_server_name,
|
|
@@ -48,5 +47,5 @@ if __name__ == "__main__":
|
|
| 48 |
theme=gr.themes.Soft(),
|
| 49 |
css="footer {visibility: hidden}",
|
| 50 |
allowed_paths=[str(_ASSETS_DIR)],
|
| 51 |
-
|
| 52 |
)
|
|
|
|
| 1 |
+
"""Alternative entry point for local development.
|
| 2 |
|
| 3 |
+
NOTE: HuggingFace Spaces Docker deployment uses `python -m stroke_deepisles_demo.ui.app`
|
| 4 |
+
(see Dockerfile CMD). This file is for local development convenience only.
|
|
|
|
| 5 |
|
| 6 |
+
For HF Spaces deployment, see: src/stroke_deepisles_demo/ui/app.py
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
from pathlib import Path
|
|
|
|
| 11 |
import gradio as gr
|
| 12 |
|
| 13 |
# CRITICAL: Allow direct file serving for local assets (niivue.js)
|
|
|
|
| 14 |
# Must be called BEFORE creating any Blocks
|
| 15 |
_ASSETS_DIR = Path(__file__).parent / "src" / "stroke_deepisles_demo" / "ui" / "assets"
|
| 16 |
gr.set_static_paths(paths=[str(_ASSETS_DIR)])
|
| 17 |
|
| 18 |
from stroke_deepisles_demo.core.config import get_settings # noqa: E402
|
| 19 |
+
from stroke_deepisles_demo.core.logging import get_logger, setup_logging # noqa: E402
|
| 20 |
from stroke_deepisles_demo.ui.app import get_demo # noqa: E402
|
| 21 |
+
from stroke_deepisles_demo.ui.viewer import get_niivue_head_html # noqa: E402
|
| 22 |
+
|
| 23 |
+
logger = get_logger(__name__)
|
| 24 |
|
| 25 |
# Initialize logging
|
| 26 |
settings = get_settings()
|
|
|
|
| 30 |
demo = get_demo()
|
| 31 |
|
| 32 |
if __name__ == "__main__":
|
| 33 |
+
# Log startup info for debugging
|
| 34 |
+
logger.info("=" * 60)
|
| 35 |
+
logger.info("STARTUP: stroke-deepisles-demo (root app.py)")
|
| 36 |
+
logger.info("Assets directory: %s", _ASSETS_DIR.resolve())
|
| 37 |
+
logger.info("Assets exists: %s", _ASSETS_DIR.exists())
|
| 38 |
+
logger.info("=" * 60)
|
| 39 |
|
| 40 |
+
# Get the NiiVue loader HTML (inline script, no file I/O needed)
|
| 41 |
+
niivue_head = get_niivue_head_html()
|
| 42 |
|
| 43 |
demo.launch(
|
| 44 |
server_name=settings.gradio_server_name,
|
|
|
|
| 47 |
theme=gr.themes.Soft(),
|
| 48 |
css="footer {visibility: hidden}",
|
| 49 |
allowed_paths=[str(_ASSETS_DIR)],
|
| 50 |
+
head=niivue_head, # Inject NiiVue loader directly
|
| 51 |
)
|
|
@@ -27,7 +27,7 @@ from stroke_deepisles_demo.ui.components import ( # noqa: E402
|
|
| 27 |
from stroke_deepisles_demo.ui.viewer import ( # noqa: E402
|
| 28 |
NIIVUE_UPDATE_JS,
|
| 29 |
create_niivue_html,
|
| 30 |
-
|
| 31 |
nifti_to_gradio_url,
|
| 32 |
render_3panel_view,
|
| 33 |
render_slice_comparison,
|
|
@@ -281,8 +281,16 @@ if __name__ == "__main__":
|
|
| 281 |
settings = get_settings()
|
| 282 |
setup_logging(settings.log_level, format_style=settings.log_format)
|
| 283 |
|
| 284 |
-
#
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
get_demo().launch(
|
| 288 |
server_name=settings.gradio_server_name,
|
|
@@ -292,5 +300,5 @@ if __name__ == "__main__":
|
|
| 292 |
css="footer {visibility: hidden}",
|
| 293 |
show_error=True, # Show full Python tracebacks in UI for debugging
|
| 294 |
allowed_paths=[str(_ASSETS_DIR)],
|
| 295 |
-
|
| 296 |
)
|
|
|
|
| 27 |
from stroke_deepisles_demo.ui.viewer import ( # noqa: E402
|
| 28 |
NIIVUE_UPDATE_JS,
|
| 29 |
create_niivue_html,
|
| 30 |
+
get_niivue_head_html,
|
| 31 |
nifti_to_gradio_url,
|
| 32 |
render_3panel_view,
|
| 33 |
render_slice_comparison,
|
|
|
|
| 281 |
settings = get_settings()
|
| 282 |
setup_logging(settings.log_level, format_style=settings.log_format)
|
| 283 |
|
| 284 |
+
# Log startup info for debugging HF Spaces issues
|
| 285 |
+
logger.info("=" * 60)
|
| 286 |
+
logger.info("STARTUP: stroke-deepisles-demo")
|
| 287 |
+
logger.info("Assets directory: %s", _ASSETS_DIR.resolve())
|
| 288 |
+
logger.info("Assets exists: %s", _ASSETS_DIR.exists())
|
| 289 |
+
logger.info("=" * 60)
|
| 290 |
+
|
| 291 |
+
# Get the NiiVue loader HTML (inline script, no file I/O needed)
|
| 292 |
+
# Using `head` param directly is simpler than `head_paths` with file generation
|
| 293 |
+
niivue_head = get_niivue_head_html()
|
| 294 |
|
| 295 |
get_demo().launch(
|
| 296 |
server_name=settings.gradio_server_name,
|
|
|
|
| 300 |
css="footer {visibility: hidden}",
|
| 301 |
show_error=True, # Show full Python tracebacks in UI for debugging
|
| 302 |
allowed_paths=[str(_ASSETS_DIR)],
|
| 303 |
+
head=niivue_head, # Inject NiiVue loader directly (simpler than head_paths)
|
| 304 |
)
|
|
@@ -32,46 +32,66 @@ _ASSET_DIR = Path(__file__).parent / "assets"
|
|
| 32 |
_NIIVUE_JS_PATH = _ASSET_DIR / "niivue.js"
|
| 33 |
|
| 34 |
# Ensure absolute path for Gradio serving
|
| 35 |
-
# NOTE: This path must be added to allowed_paths in demo.launch()
|
| 36 |
NIIVUE_JS_URL = f"/gradio_api/file={_NIIVUE_JS_PATH.resolve()}"
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
def get_niivue_loader_path() -> Path:
|
| 40 |
-
"""
|
| 41 |
-
Get path to the NiiVue loader HTML file, creating it if needed.
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
|
|
|
| 49 |
|
| 50 |
Returns:
|
| 51 |
-
|
| 52 |
|
| 53 |
Note:
|
| 54 |
-
The
|
|
|
|
| 55 |
"""
|
| 56 |
-
|
|
|
|
| 57 |
|
| 58 |
-
|
| 59 |
-
loader_content = f"""<!--
|
| 60 |
-
NiiVue Loader for Gradio (auto-generated)
|
| 61 |
-
Loads NiiVue library and exposes it globally for js_on_load handlers.
|
| 62 |
-
See: docs/specs/24-bug-hf-spaces-loading-forever.md
|
| 63 |
-
-->
|
| 64 |
<script type="module">
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
</script>
|
| 69 |
"""
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
try:
|
| 74 |
-
# Check if file exists and has correct content
|
| 75 |
if loader_path.exists():
|
| 76 |
existing_content = loader_path.read_text()
|
| 77 |
if existing_content == loader_content:
|
|
@@ -80,8 +100,6 @@ def get_niivue_loader_path() -> Path:
|
|
| 80 |
loader_path.write_text(loader_content)
|
| 81 |
logger.debug("Generated NiiVue loader at %s", loader_path)
|
| 82 |
except OSError as e:
|
| 83 |
-
# If we can't write (e.g., read-only filesystem), the file should
|
| 84 |
-
# already exist from the build process
|
| 85 |
logger.warning("Could not write loader file at %s: %s", loader_path, e)
|
| 86 |
if not loader_path.exists():
|
| 87 |
raise RuntimeError(
|
|
@@ -457,6 +475,10 @@ NIIVUE_ON_LOAD_JS = """
|
|
| 457 |
const waitForNiivue = async () => {
|
| 458 |
for (let i = 0; i < 50; i++) {
|
| 459 |
if (window.Niivue) return window.Niivue;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
await new Promise(r => setTimeout(r, 100));
|
| 461 |
}
|
| 462 |
return null;
|
|
@@ -464,7 +486,9 @@ NIIVUE_ON_LOAD_JS = """
|
|
| 464 |
|
| 465 |
const Niivue = await waitForNiivue();
|
| 466 |
if (!Niivue) {
|
| 467 |
-
|
|
|
|
|
|
|
| 468 |
}
|
| 469 |
|
| 470 |
// Initialize NiiVue
|
|
@@ -567,6 +591,10 @@ NIIVUE_UPDATE_JS = """
|
|
| 567 |
const waitForNiivue = async () => {
|
| 568 |
for (let i = 0; i < 50; i++) {
|
| 569 |
if (window.Niivue) return window.Niivue;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
await new Promise(r => setTimeout(r, 100));
|
| 571 |
}
|
| 572 |
return null;
|
|
@@ -574,7 +602,9 @@ NIIVUE_UPDATE_JS = """
|
|
| 574 |
|
| 575 |
const Niivue = await waitForNiivue();
|
| 576 |
if (!Niivue) {
|
| 577 |
-
|
|
|
|
|
|
|
| 578 |
}
|
| 579 |
|
| 580 |
// Initialize NiiVue
|
|
|
|
| 32 |
_NIIVUE_JS_PATH = _ASSET_DIR / "niivue.js"
|
| 33 |
|
| 34 |
# Ensure absolute path for Gradio serving
|
| 35 |
+
# NOTE: This path must be added to allowed_paths AND set_static_paths in demo.launch()
|
| 36 |
NIIVUE_JS_URL = f"/gradio_api/file={_NIIVUE_JS_PATH.resolve()}"
|
| 37 |
|
| 38 |
+
# Log the computed paths at module load time for debugging HF Spaces issues
|
| 39 |
+
logger.info("NiiVue assets directory: %s", _ASSET_DIR.resolve())
|
| 40 |
+
logger.info("NiiVue JS path: %s", _NIIVUE_JS_PATH.resolve())
|
| 41 |
+
logger.info("NiiVue JS URL: %s", NIIVUE_JS_URL)
|
| 42 |
+
logger.info("NiiVue JS exists: %s", _NIIVUE_JS_PATH.exists())
|
| 43 |
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
+
def get_niivue_head_html() -> str:
|
| 46 |
+
"""
|
| 47 |
+
Get HTML content to inject into page <head> for NiiVue loading.
|
| 48 |
|
| 49 |
+
This returns an inline script that loads NiiVue as a global variable.
|
| 50 |
+
Using the `head` parameter directly (instead of `head_paths` with a file)
|
| 51 |
+
is simpler and avoids file I/O issues on HF Spaces.
|
| 52 |
|
| 53 |
Returns:
|
| 54 |
+
HTML string containing the NiiVue loader script
|
| 55 |
|
| 56 |
Note:
|
| 57 |
+
The niivue.js path must be registered with gr.set_static_paths()
|
| 58 |
+
and included in allowed_paths during launch().
|
| 59 |
"""
|
| 60 |
+
# Log for debugging path resolution issues on HF Spaces
|
| 61 |
+
logger.info("Generating NiiVue head HTML with URL: %s", NIIVUE_JS_URL)
|
| 62 |
|
| 63 |
+
return f"""<!-- NiiVue Loader: Exposes window.Niivue for js_on_load handlers -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
<script type="module">
|
| 65 |
+
try {{
|
| 66 |
+
const niivueUrl = '{NIIVUE_JS_URL}';
|
| 67 |
+
console.log('[NiiVue Loader] Attempting to load from:', niivueUrl);
|
| 68 |
+
const {{ Niivue }} = await import(niivueUrl);
|
| 69 |
+
window.Niivue = Niivue;
|
| 70 |
+
console.log('[NiiVue Loader] Successfully loaded, window.Niivue:', typeof window.Niivue);
|
| 71 |
+
}} catch (error) {{
|
| 72 |
+
console.error('[NiiVue Loader] FAILED to load:', error);
|
| 73 |
+
// Surface the error visibly so we can debug on HF Spaces
|
| 74 |
+
window.NIIVUE_LOAD_ERROR = error.message;
|
| 75 |
+
}}
|
| 76 |
</script>
|
| 77 |
"""
|
| 78 |
|
| 79 |
+
|
| 80 |
+
def get_niivue_loader_path() -> Path:
|
| 81 |
+
"""
|
| 82 |
+
DEPRECATED: Use get_niivue_head_html() with the `head` parameter instead.
|
| 83 |
+
|
| 84 |
+
Get path to the NiiVue loader HTML file, creating it if needed.
|
| 85 |
+
This file-based approach is kept for backwards compatibility but
|
| 86 |
+
the direct `head` parameter approach is preferred.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Path to the niivue-loader.html file
|
| 90 |
+
"""
|
| 91 |
+
loader_path = _ASSET_DIR / "niivue-loader.html"
|
| 92 |
+
loader_content = get_niivue_head_html()
|
| 93 |
+
|
| 94 |
try:
|
|
|
|
| 95 |
if loader_path.exists():
|
| 96 |
existing_content = loader_path.read_text()
|
| 97 |
if existing_content == loader_content:
|
|
|
|
| 100 |
loader_path.write_text(loader_content)
|
| 101 |
logger.debug("Generated NiiVue loader at %s", loader_path)
|
| 102 |
except OSError as e:
|
|
|
|
|
|
|
| 103 |
logger.warning("Could not write loader file at %s: %s", loader_path, e)
|
| 104 |
if not loader_path.exists():
|
| 105 |
raise RuntimeError(
|
|
|
|
| 475 |
const waitForNiivue = async () => {
|
| 476 |
for (let i = 0; i < 50; i++) {
|
| 477 |
if (window.Niivue) return window.Niivue;
|
| 478 |
+
// Check if loader script failed (error stored in global)
|
| 479 |
+
if (window.NIIVUE_LOAD_ERROR) {
|
| 480 |
+
throw new Error('NiiVue loader failed: ' + window.NIIVUE_LOAD_ERROR);
|
| 481 |
+
}
|
| 482 |
await new Promise(r => setTimeout(r, 100));
|
| 483 |
}
|
| 484 |
return null;
|
|
|
|
| 486 |
|
| 487 |
const Niivue = await waitForNiivue();
|
| 488 |
if (!Niivue) {
|
| 489 |
+
// Provide diagnostic info about what might be wrong
|
| 490 |
+
const loadErr = window.NIIVUE_LOAD_ERROR || 'unknown';
|
| 491 |
+
throw new Error('NiiVue not loaded after 5s. Check browser console for errors. Load error: ' + loadErr);
|
| 492 |
}
|
| 493 |
|
| 494 |
// Initialize NiiVue
|
|
|
|
| 591 |
const waitForNiivue = async () => {
|
| 592 |
for (let i = 0; i < 50; i++) {
|
| 593 |
if (window.Niivue) return window.Niivue;
|
| 594 |
+
// Check if loader script failed (error stored in global)
|
| 595 |
+
if (window.NIIVUE_LOAD_ERROR) {
|
| 596 |
+
throw new Error('NiiVue loader failed: ' + window.NIIVUE_LOAD_ERROR);
|
| 597 |
+
}
|
| 598 |
await new Promise(r => setTimeout(r, 100));
|
| 599 |
}
|
| 600 |
return null;
|
|
|
|
| 602 |
|
| 603 |
const Niivue = await waitForNiivue();
|
| 604 |
if (!Niivue) {
|
| 605 |
+
// Provide diagnostic info about what might be wrong
|
| 606 |
+
const loadErr = window.NIIVUE_LOAD_ERROR || 'unknown';
|
| 607 |
+
throw new Error('NiiVue not loaded after 5s. Check browser console for errors. Load error: ' + loadErr);
|
| 608 |
}
|
| 609 |
|
| 610 |
// Initialize NiiVue
|