fix(ci): add pytest-timeout and HF Spaces deployment spec
Browse files## Summary
- Add missing pytest-timeout dependency (root cause of CI failure)
- Add comprehensive HF Spaces deployment specification
- Fix hardcoded /tmp paths in tests with temp_dir fixture
- Add
@pytest
.mark.integration markers for consistency
## CI Status
All checks passing: lint ✅ test ✅ typecheck ✅
- docs/specs/07-hf-spaces-deployment.md +938 -0
- pyproject.toml +1 -0
- tests/data/test_integration_real_data.py +2 -0
- tests/inference/test_deepisles.py +6 -3
- tests/test_pipeline.py +2 -2
- uv.lock +0 -0
docs/specs/07-hf-spaces-deployment.md
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# spec: hugging face spaces + gradio deployment
|
| 2 |
+
|
| 3 |
+
> **Version**: December 2025
|
| 4 |
+
> **Status**: APPROVED - Ready for Implementation
|
| 5 |
+
> **Last Updated**: 2025-12-05
|
| 6 |
+
> **Verified**: Cold start claims, pause/restart behavior, ZeroGPU limitations
|
| 7 |
+
|
| 8 |
+
## important: gradio 6 is now available
|
| 9 |
+
|
| 10 |
+
As of December 2025, **Gradio 6.0.2** is the latest stable release. Our `pyproject.toml` currently specifies `gradio>=5.0.0`, which will install Gradio 6.x.
|
| 11 |
+
|
| 12 |
+
**Key breaking changes affecting our codebase:**
|
| 13 |
+
|
| 14 |
+
| Change | Impact | Our Code |
|
| 15 |
+
|--------|--------|----------|
|
| 16 |
+
| `theme`, `css`, `js` moved from `Blocks()` to `launch()` | HIGH | `app.py:111` uses `gr.Blocks()`, `app.py:170` passes theme to `launch()` - **OK** |
|
| 17 |
+
| `gr.HTML` padding default `True` → `False` | LOW | No visual impact expected |
|
| 18 |
+
| Chatbot tuple format removed | NONE | We don't use Chatbot |
|
| 19 |
+
| `show_api` → `footer_links` | LOW | We don't customize this |
|
| 20 |
+
|
| 21 |
+
**Recommendation**: Pin to `gradio>=6.0.0,<7.0.0` for stability, or test with latest and update as needed.
|
| 22 |
+
|
| 23 |
+
**Migration guide**: [Gradio 6 Migration Guide](https://www.gradio.app/main/guides/gradio-6-migration-guide)
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## purpose
|
| 28 |
+
|
| 29 |
+
This spec documents the requirements, constraints, and best practices for deploying the `stroke-deepisles-demo` Gradio application to Hugging Face Spaces. It identifies potential friction points between our current implementation and HF Spaces constraints, providing concrete guidance before deployment.
|
| 30 |
+
|
| 31 |
+
## executive summary
|
| 32 |
+
|
| 33 |
+
### critical friction points identified
|
| 34 |
+
|
| 35 |
+
| Issue | Severity | Current State | Fix Required |
|
| 36 |
+
|-------|----------|---------------|--------------|
|
| 37 |
+
| **NVIDIA GPU required** | HIGH | DeepISLES needs CUDA | Use Docker SDK + GPU on HF Spaces |
|
| 38 |
+
| **JavaScript in `gr.HTML`** | HIGH | `<script type="module">` in viewer.py | May not execute; needs `js=` param pattern |
|
| 39 |
+
| **Git dependency in pyproject.toml** | MEDIUM | `datasets @ git+https://...` | Needs `requirements.txt` with git URL |
|
| 40 |
+
| **Large NIfTI files as base64** | MEDIUM | Full file loaded to memory | Should be fine with GPU tier RAM |
|
| 41 |
+
| **NiiVue version** | LOW | Currently 0.57.0 in viewer.py | Update to **0.65.0** (latest) |
|
| 42 |
+
|
| 43 |
+
### deployment strategy
|
| 44 |
+
|
| 45 |
+
> **Important**: DeepISLES requires NVIDIA GPU with CUDA. There is no CPU-only or Apple Silicon option. "Demo mode" with pre-computed results was rejected as it defeats the purpose of a real inference demo.
|
| 46 |
+
|
| 47 |
+
### Primary: Local NVIDIA GPU
|
| 48 |
+
- Develop and test locally with your NVIDIA GPU
|
| 49 |
+
- Free, unlimited, real inference
|
| 50 |
+
- Works on Windows/Linux with NVIDIA GPU (GTX 1080+, RTX series)
|
| 51 |
+
|
| 52 |
+
### Showcase: HF Spaces Docker SDK + GPU (On-Demand)
|
| 53 |
+
- Use `sdk: docker` with GPU hardware
|
| 54 |
+
- **Spin up** when demoing, **pause** when done
|
| 55 |
+
- Cost: ~$0.20-$0.40 per 30-60 min demo session
|
| 56 |
+
- Billing stops when paused ($0 while inactive)
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## critical: cold start reality
|
| 61 |
+
|
| 62 |
+
> ⚠️ **OPERATIONAL MANDATE**: Always run `api.restart_space()` **20-30 minutes** before a scheduled demo. Verify the Space is "Running" before sharing your screen.
|
| 63 |
+
|
| 64 |
+
### verified cold start times (december 2025)
|
| 65 |
+
|
| 66 |
+
| Phase | Time | Source |
|
| 67 |
+
|-------|------|--------|
|
| 68 |
+
| HF Infrastructure boot | ~2 minutes | [HF Forums](https://discuss.huggingface.co/t/slow-space-cold-boot/72154) |
|
| 69 |
+
| Docker image provision | 5-20 minutes | Large images (CUDA + nnU-Net ~15-20GB) |
|
| 70 |
+
| Application startup | 1-5 minutes | Gradio + model loading |
|
| 71 |
+
| **Total (best case)** | **8-12 minutes** | Normal conditions |
|
| 72 |
+
| **Total (worst case)** | **30-60+ minutes** | Resource contention, Feb 2025 T4 issues |
|
| 73 |
+
|
| 74 |
+
**Sources**: [T4 startup 45+ min issue (Feb 2025)](https://discuss.huggingface.co/t/staring-up-t4-instances-is-taking-45-minutes/139567), [Cold boot discussion](https://discuss.huggingface.co/t/slow-space-cold-boot/72154)
|
| 75 |
+
|
| 76 |
+
### why cold start is unavoidable
|
| 77 |
+
|
| 78 |
+
From HF Staff (forum moderator):
|
| 79 |
+
> "avoiding a cold start here is not possible"
|
| 80 |
+
|
| 81 |
+
The ~2-minute infrastructure delay is inherent to HF Spaces architecture. Docker GPU Spaces add additional time for image provisioning and GPU allocation.
|
| 82 |
+
|
| 83 |
+
### deployment risks (edge cases)
|
| 84 |
+
|
| 85 |
+
| Risk | Frequency | Mitigation |
|
| 86 |
+
|------|-----------|------------|
|
| 87 |
+
| Space stuck in "Starting" | Rare | Factory rebuild, contact HF support |
|
| 88 |
+
| Space stuck in "Paused" | Rare | Wait + retry, contact HF support |
|
| 89 |
+
| Build timeout (30-45 min limit) | Possible | Optimize Dockerfile, cache layers |
|
| 90 |
+
| GPU unavailable (resource contention) | Rare | Try again later, different hardware tier |
|
| 91 |
+
|
| 92 |
+
**Sources**: [Space stuck at Starting (Nov 2025)](https://discuss.huggingface.co/t/hf-space-stuck-at-starting/170911), [Space stuck in Paused (Oct 2025)](https://discuss.huggingface.co/t/space-stuck-in-paused/169467)
|
| 93 |
+
|
| 94 |
+
### pre-demo warm-up procedure
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
# 20-30 minutes before your demo:
|
| 98 |
+
|
| 99 |
+
# 1. Restart the Space
|
| 100 |
+
python -c "
|
| 101 |
+
from huggingface_hub import HfApi
|
| 102 |
+
api = HfApi()
|
| 103 |
+
api.restart_space('YOUR_USERNAME/stroke-deepisles-demo')
|
| 104 |
+
print('Space restart initiated...')
|
| 105 |
+
"
|
| 106 |
+
|
| 107 |
+
# 2. Monitor status (check every 2 min)
|
| 108 |
+
python -c "
|
| 109 |
+
from huggingface_hub import HfApi
|
| 110 |
+
api = HfApi()
|
| 111 |
+
info = api.space_info('YOUR_USERNAME/stroke-deepisles-demo')
|
| 112 |
+
print(f'Status: {info.runtime.stage}') # Should be 'RUNNING'
|
| 113 |
+
"
|
| 114 |
+
|
| 115 |
+
# 3. Only proceed when status = RUNNING
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### contingency plan if cold start fails
|
| 119 |
+
|
| 120 |
+
1. **Space stuck in "Starting" > 30 min**:
|
| 121 |
+
- Try "Factory rebuild" from Space Settings
|
| 122 |
+
- If still stuck, contact HF support via [Discord](https://discord.gg/hugging-face-879548962464493619)
|
| 123 |
+
|
| 124 |
+
2. **Demo starts before Space is ready**:
|
| 125 |
+
- Show local demo on your NVIDIA GPU machine instead
|
| 126 |
+
- "Let me show you on my development machine while the cloud version warms up"
|
| 127 |
+
|
| 128 |
+
3. **GPU unavailable error**:
|
| 129 |
+
- Try `a10g-small` instead of `t4-small` (different GPU pool)
|
| 130 |
+
- Wait 15 minutes and retry
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## zerogpu: why it doesn't work for us
|
| 135 |
+
|
| 136 |
+
ZeroGPU offers free, dynamic GPU allocation on H200 GPUs. However:
|
| 137 |
+
|
| 138 |
+
| Requirement | ZeroGPU | Our Need |
|
| 139 |
+
|-------------|---------|----------|
|
| 140 |
+
| SDK Support | Gradio SDK only | Docker SDK (for DeepISLES container) |
|
| 141 |
+
| Docker containers | ❌ NOT supported | ✅ Required |
|
| 142 |
+
| Custom CUDA environment | ❌ NOT supported | ✅ Required (nnU-Net) |
|
| 143 |
+
|
| 144 |
+
**Source**: [ZeroGPU Documentation](https://huggingface.co/docs/hub/en/spaces-zerogpu), [Community request for Docker support](https://huggingface.co/spaces/zero-gpu-explorers/README/discussions/27)
|
| 145 |
+
|
| 146 |
+
**Verdict**: ZeroGPU is incompatible with DeepISLES. We must use Docker SDK + paid GPU hardware.
|
| 147 |
+
|
| 148 |
+
---
|
| 149 |
+
|
| 150 |
+
## hugging face spaces constraints
|
| 151 |
+
|
| 152 |
+
### sdk options
|
| 153 |
+
|
| 154 |
+
| SDK | Use Case | Docker Access | GPU Support |
|
| 155 |
+
|-----|----------|---------------|-------------|
|
| 156 |
+
| `gradio` | Standard Gradio apps | **NO** | Via hardware upgrade |
|
| 157 |
+
| `docker` | Custom containers | **YES** | Via hardware upgrade |
|
| 158 |
+
| `static` | HTML/JS only | **NO** | N/A |
|
| 159 |
+
|
| 160 |
+
**Key insight**: The Gradio SDK **cannot run Docker containers**. Our pipeline requires the DeepISLES Docker image, creating a fundamental incompatibility.
|
| 161 |
+
|
| 162 |
+
### hardware tiers
|
| 163 |
+
|
| 164 |
+
| Tier | vCPU | RAM | Cost | GPU |
|
| 165 |
+
|------|------|-----|------|-----|
|
| 166 |
+
| cpu-basic (free) | 2 | 16GB | $0 | None |
|
| 167 |
+
| cpu-upgrade | 8 | 32GB | $0.03/hr | None |
|
| 168 |
+
| t4-small | 4 | 15GB | $0.40/hr | T4 (16GB) |
|
| 169 |
+
| t4-medium | 8 | 30GB | $0.60/hr | T4 (16GB) |
|
| 170 |
+
| a10g-small | 4 | 15GB | $1.05/hr | A10G (24GB) |
|
| 171 |
+
| a10g-large | 12 | 46GB | $3.15/hr | A10G (24GB) |
|
| 172 |
+
|
| 173 |
+
**Source**: [Hugging Face Spaces GPU Upgrades](https://huggingface.co/docs/hub/spaces)
|
| 174 |
+
|
| 175 |
+
### storage limits
|
| 176 |
+
|
| 177 |
+
| Type | Limit | Behavior |
|
| 178 |
+
|------|-------|----------|
|
| 179 |
+
| Ephemeral (root fs) | 50GB | Lost on restart |
|
| 180 |
+
| Persistent (`/data`) | 20GB-1TB | Paid tiers ($5-$100/mo) |
|
| 181 |
+
| Build cache | Varies | Can cause "storage limit exceeded" |
|
| 182 |
+
|
| 183 |
+
**Best practice**: Set `HF_HOME=/data/.huggingface` to cache models in persistent storage.
|
| 184 |
+
|
| 185 |
+
> ⚠️ **Important**: `HF_HOME` must be set in the Space's **Settings → Repository secrets** UI, not just in code. Environment variables set only in Python code won't persist across container restarts.
|
| 186 |
+
|
| 187 |
+
**Source**: [Spaces Persistent Storage](https://huggingface.co/docs/hub/en/spaces-storage)
|
| 188 |
+
|
| 189 |
+
### build limits
|
| 190 |
+
|
| 191 |
+
| Limit | Value | Notes |
|
| 192 |
+
|-------|-------|-------|
|
| 193 |
+
| Build timeout | 30-45 minutes | Large dependencies may fail |
|
| 194 |
+
| Build cache | Part of 50GB ephemeral | Can cause "storage limit exceeded" |
|
| 195 |
+
| Startup timeout | 30 minutes (default) | Configurable via `startup_duration_timeout` |
|
| 196 |
+
| Idle sleep | 48 hours | Free Spaces sleep after inactivity |
|
| 197 |
+
|
| 198 |
+
**Warning**: Heavy scientific stacks (PyTorch, large C extensions) may hit build timeout. Monitor build logs closely.
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## gradio 6 constraints (december 2025)
|
| 203 |
+
|
| 204 |
+
> **Note**: Gradio 6.0 was released in late November 2025. Our codebase was written for Gradio 5.x but is largely compatible.
|
| 205 |
+
|
| 206 |
+
### key breaking changes from gradio 5 → 6
|
| 207 |
+
|
| 208 |
+
| Change | Gradio 5.x | Gradio 6.x | Our Status |
|
| 209 |
+
|--------|------------|------------|------------|
|
| 210 |
+
| Theme/CSS/JS placement | `gr.Blocks(theme=..., css=..., js=...)` | `demo.launch(theme=..., css=..., js=...)` | ✅ Already correct in `app.py:170` |
|
| 211 |
+
| HTML padding default | `padding=True` | `padding=False` | ⚠️ Minor visual change |
|
| 212 |
+
| Chatbot message format | Tuple `[["user", "bot"]]` | Dict `{"role": ..., "content": ...}` | N/A - Not used |
|
| 213 |
+
| `show_api` parameter | `show_api=True/False` | `footer_links=["api", "gradio", "settings"]` | N/A - Not customized |
|
| 214 |
+
| Event `api_name=False` | `api_name=False` | `api_visibility="private"` | N/A - Not used |
|
| 215 |
+
|
| 216 |
+
### new in gradio 6
|
| 217 |
+
|
| 218 |
+
1. **Custom Web Components**: Write custom components in pure HTML/JS inline in Python via `gradio cc`
|
| 219 |
+
2. **Vibe Mode**: `gradio --vibe app.py` for AI-assisted app editing
|
| 220 |
+
3. **Performance**: Significantly lighter and faster
|
| 221 |
+
4. **Security**: Trail of Bits audit improvements carried forward
|
| 222 |
+
5. **Server-Side Rendering (SSR)**: Faster initial loads, better SEO
|
| 223 |
+
|
| 224 |
+
> ⚠️ **SSR Consideration**: With SSR enabled, JavaScript that references `window` or `document` may fail during server-side render. Ensure NiiVue initialization checks `typeof window !== 'undefined'` before accessing browser APIs.
|
| 225 |
+
|
| 226 |
+
### javascript execution in `gr.HTML`
|
| 227 |
+
|
| 228 |
+
**CRITICAL ISSUE**: The `gr.HTML` component does **not** execute JavaScript in `<script>` tags in the standard way.
|
| 229 |
+
|
| 230 |
+
#### current implementation (viewer.py:262-324)
|
| 231 |
+
|
| 232 |
+
```python
|
| 233 |
+
def create_niivue_html(...) -> str:
|
| 234 |
+
return f"""
|
| 235 |
+
<div style="width:100%; height:{height}px; ...">
|
| 236 |
+
<canvas id="niivue-canvas" style="width:100%; height:100%;"></canvas>
|
| 237 |
+
</div>
|
| 238 |
+
<script type="module">
|
| 239 |
+
const niivueModule = await import('https://unpkg.com/@niivue/niivue@0.65.0/dist/index.js');
|
| 240 |
+
// ... NiiVue initialization
|
| 241 |
+
</script>
|
| 242 |
+
"""
|
| 243 |
+
```
|
| 244 |
+
|
| 245 |
+
#### the problem
|
| 246 |
+
|
| 247 |
+
From the [Gradio documentation](https://www.gradio.app/guides/custom-CSS-and-JS) and [HF Forums](https://discuss.huggingface.co/t/gradio-html-component-with-javascript-code-dont-work/37316):
|
| 248 |
+
|
| 249 |
+
> "The `gr.HTML` component doesn't support loading scripts via traditional `<script>` tags. This prevents JavaScript functions from being accessible to inline event handlers."
|
| 250 |
+
|
| 251 |
+
#### recommended fix
|
| 252 |
+
|
| 253 |
+
Use `gr.Blocks(js=...)` or `demo.load(_js=...)` to inject JavaScript:
|
| 254 |
+
|
| 255 |
+
```python
|
| 256 |
+
NIIVUE_INIT_JS = """
|
| 257 |
+
async () => {
|
| 258 |
+
// Wait for NiiVue module to load
|
| 259 |
+
const niivueModule = await import('https://unpkg.com/@niivue/niivue@0.65.0/dist/index.js');
|
| 260 |
+
globalThis.Niivue = niivueModule.Niivue;
|
| 261 |
+
}
|
| 262 |
+
"""
|
| 263 |
+
|
| 264 |
+
def create_app() -> gr.Blocks:
|
| 265 |
+
with gr.Blocks(js=NIIVUE_INIT_JS) as demo:
|
| 266 |
+
# ... components
|
| 267 |
+
|
| 268 |
+
return demo
|
| 269 |
+
```
|
| 270 |
+
|
| 271 |
+
Then in the HTML component, reference the global:
|
| 272 |
+
|
| 273 |
+
```python
|
| 274 |
+
def create_niivue_html(volume_url: str, ...) -> str:
|
| 275 |
+
return f"""
|
| 276 |
+
<div id="niivue-container-{uuid}" style="...">
|
| 277 |
+
<canvas id="niivue-canvas-{uuid}"></canvas>
|
| 278 |
+
</div>
|
| 279 |
+
<script>
|
| 280 |
+
(async function() {{
|
| 281 |
+
if (typeof globalThis.Niivue === 'undefined') {{
|
| 282 |
+
console.error('NiiVue not loaded');
|
| 283 |
+
return;
|
| 284 |
+
}}
|
| 285 |
+
const nv = new globalThis.Niivue({{...}});
|
| 286 |
+
await nv.attachTo('niivue-canvas-{uuid}');
|
| 287 |
+
// ...
|
| 288 |
+
}})();
|
| 289 |
+
</script>
|
| 290 |
+
"""
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
**Note**: Even this may not work reliably. Testing on HF Spaces is required.
|
| 294 |
+
|
| 295 |
+
#### alternative: gradio custom components (`gradio cc`)
|
| 296 |
+
|
| 297 |
+
For production deployments, Gradio 6 supports first-class **Custom Components** via the `gradio cc` CLI. This is the recommended "production" solution (vs. the `js=` hack for MVP).
|
| 298 |
+
|
| 299 |
+
```bash
|
| 300 |
+
# Create a NiiVue custom component
|
| 301 |
+
gradio cc create NiiVueViewer --template HTML
|
| 302 |
+
|
| 303 |
+
# Development server with hot reload
|
| 304 |
+
gradio cc dev
|
| 305 |
+
|
| 306 |
+
# Build for distribution
|
| 307 |
+
gradio cc build
|
| 308 |
+
|
| 309 |
+
# Publish to PyPI and HF Spaces
|
| 310 |
+
gradio cc publish
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
**Pros**:
|
| 314 |
+
- First-class support, proper state management
|
| 315 |
+
- No hacky string interpolation
|
| 316 |
+
- Reusable across projects
|
| 317 |
+
|
| 318 |
+
**Cons**:
|
| 319 |
+
- Requires Node.js build step
|
| 320 |
+
- Higher complexity than `js=` parameter
|
| 321 |
+
- Overkill for MVP
|
| 322 |
+
|
| 323 |
+
**Source**: [Custom Components In Five Minutes](https://www.gradio.app/guides/custom-components-in-five-minutes)
|
| 324 |
+
|
| 325 |
+
#### alternative: `gradio-iframe` component
|
| 326 |
+
|
| 327 |
+
The [`gradio-iframe`](https://pypi.org/project/gradio-iframe/) package (v0.0.10) provides an iframe component that may execute JavaScript more reliably:
|
| 328 |
+
|
| 329 |
+
```python
|
| 330 |
+
from gradio_iframe import iFrame
|
| 331 |
+
|
| 332 |
+
viewer = iFrame(
|
| 333 |
+
value="<html>...NiiVue code...</html>",
|
| 334 |
+
label="NiiVue Viewer"
|
| 335 |
+
)
|
| 336 |
+
```
|
| 337 |
+
|
| 338 |
+
**Warning**: This is experimental and "not fully tested" per the maintainer. Use with caution.
|
| 339 |
+
|
| 340 |
+
### css restrictions
|
| 341 |
+
|
| 342 |
+
Custom CSS should use `elem_id` and `elem_classes` rather than query selectors:
|
| 343 |
+
|
| 344 |
+
> "The use of query selectors in custom JS and CSS is not guaranteed to work across Gradio versions as the Gradio HTML DOM may change."
|
| 345 |
+
|
| 346 |
+
**Source**: [Custom CSS and JS Guide](https://www.gradio.app/guides/custom-CSS-and-JS)
|
| 347 |
+
|
| 348 |
+
### security (gradio 5 audit, inherited by v6)
|
| 349 |
+
|
| 350 |
+
The Trail of Bits security audit was performed on **Gradio 5.0**. All fixes are inherited by Gradio 6.x:
|
| 351 |
+
|
| 352 |
+
- **CVE-2024-47872**: XSS via HTML/JS/SVG file uploads (fixed in 5.0.0)
|
| 353 |
+
- File type restrictions enforced server-side
|
| 354 |
+
- Our app uses `gradio>=6.0.0` - we're covered
|
| 355 |
+
|
| 356 |
+
> **Note**: There was no separate Gradio 6 audit. The security improvements from Gradio 5 persist in v6.
|
| 357 |
+
|
| 358 |
+
**Source**: [A Security Review of Gradio 5](https://huggingface.co/blog/gradio-5-security)
|
| 359 |
+
|
| 360 |
+
---
|
| 361 |
+
|
| 362 |
+
## readme.md yaml configuration
|
| 363 |
+
|
| 364 |
+
### required fields for gradio spaces
|
| 365 |
+
|
| 366 |
+
```yaml
|
| 367 |
+
---
|
| 368 |
+
title: Stroke DeepISLES Demo
|
| 369 |
+
emoji: 🧠
|
| 370 |
+
colorFrom: blue
|
| 371 |
+
colorTo: purple
|
| 372 |
+
sdk: gradio
|
| 373 |
+
sdk_version: "6.0.2" # Latest stable as of Dec 2025
|
| 374 |
+
python_version: "3.11"
|
| 375 |
+
app_file: app.py
|
| 376 |
+
pinned: false
|
| 377 |
+
license: mit
|
| 378 |
+
short_description: "Ischemic stroke lesion segmentation using DeepISLES"
|
| 379 |
+
|
| 380 |
+
# Optional but recommended
|
| 381 |
+
models:
|
| 382 |
+
- isleschallenge/deepisles # If we reference it
|
| 383 |
+
datasets:
|
| 384 |
+
- YongchengYAO/ISLES24-MR-Lite
|
| 385 |
+
tags:
|
| 386 |
+
- medical-imaging
|
| 387 |
+
- stroke
|
| 388 |
+
- segmentation
|
| 389 |
+
- neuroimaging
|
| 390 |
+
- niivue
|
| 391 |
+
|
| 392 |
+
# For CPU-only demo mode
|
| 393 |
+
suggested_hardware: cpu-basic
|
| 394 |
+
|
| 395 |
+
# If we need cross-origin isolation (e.g., SharedArrayBuffer)
|
| 396 |
+
# custom_headers:
|
| 397 |
+
# cross-origin-embedder-policy: require-corp
|
| 398 |
+
# cross-origin-opener-policy: same-origin
|
| 399 |
+
---
|
| 400 |
+
```
|
| 401 |
+
|
| 402 |
+
### configuration reference
|
| 403 |
+
|
| 404 |
+
| Field | Type | Description |
|
| 405 |
+
|-------|------|-------------|
|
| 406 |
+
| `sdk` | string | `gradio`, `docker`, or `static` |
|
| 407 |
+
| `sdk_version` | string | Gradio version (e.g., "5.0.0") |
|
| 408 |
+
| `python_version` | string | Python version (e.g., "3.11") |
|
| 409 |
+
| `app_file` | string | Entry point (default: `app.py`) |
|
| 410 |
+
| `suggested_hardware` | string | Hardware for duplicators |
|
| 411 |
+
| `disable_embedding` | bool | Prevent iframe embedding |
|
| 412 |
+
| `custom_headers` | dict | COEP/COOP/CORP headers |
|
| 413 |
+
|
| 414 |
+
**Source**: [Spaces Configuration Reference](https://huggingface.co/docs/hub/en/spaces-config-reference)
|
| 415 |
+
|
| 416 |
+
---
|
| 417 |
+
|
| 418 |
+
## dependencies
|
| 419 |
+
|
| 420 |
+
### requirements.txt for hf spaces
|
| 421 |
+
|
| 422 |
+
HF Spaces uses `requirements.txt`, not `pyproject.toml` for dependency installation.
|
| 423 |
+
|
| 424 |
+
```text
|
| 425 |
+
# requirements.txt for HF Spaces
|
| 426 |
+
|
| 427 |
+
# Core - Tobias's fork with BIDS + NIfTI lazy loading
|
| 428 |
+
git+https://github.com/CloseChoice/datasets.git@feat/bids-loader-streaming-upload-fix
|
| 429 |
+
|
| 430 |
+
# HuggingFace
|
| 431 |
+
huggingface-hub>=0.25.0
|
| 432 |
+
|
| 433 |
+
# NIfTI handling
|
| 434 |
+
nibabel>=5.2.0
|
| 435 |
+
numpy>=1.26.0
|
| 436 |
+
|
| 437 |
+
# Configuration
|
| 438 |
+
pydantic>=2.5.0
|
| 439 |
+
pydantic-settings>=2.1.0
|
| 440 |
+
|
| 441 |
+
# UI - Gradio 6.x (latest stable as of Dec 2025)
|
| 442 |
+
gradio>=6.0.0,<7.0.0
|
| 443 |
+
matplotlib>=3.8.0
|
| 444 |
+
|
| 445 |
+
# Networking
|
| 446 |
+
requests>=2.0.0
|
| 447 |
+
```
|
| 448 |
+
|
| 449 |
+
### potential issues
|
| 450 |
+
|
| 451 |
+
1. **Git dependencies**: HF Spaces supports `git+https://...` in requirements.txt
|
| 452 |
+
2. **C extensions**: nibabel/numpy compile fine on HF Spaces
|
| 453 |
+
3. **Size**: No bloated dependencies (no PyTorch required for demo mode)
|
| 454 |
+
|
| 455 |
+
---
|
| 456 |
+
|
| 457 |
+
## deployment paths
|
| 458 |
+
|
| 459 |
+
### hardware requirements
|
| 460 |
+
|
| 461 |
+
| Component | Requirement | Notes |
|
| 462 |
+
|-----------|-------------|-------|
|
| 463 |
+
| GPU | NVIDIA with CUDA 11.3+ | **Mandatory** - no CPU/MPS fallback |
|
| 464 |
+
| VRAM | 4GB minimum, 12GB+ recommended | For parallel processing |
|
| 465 |
+
| Docker | Docker + nvidia-container-toolkit | Required for DeepISLES |
|
| 466 |
+
| Python | 3.8+ (3.11 recommended) | Per project config |
|
| 467 |
+
|
| 468 |
+
> ⚠️ **Apple Silicon (M1/M2/M3) is NOT supported.** DeepISLES requires NVIDIA CUDA.
|
| 469 |
+
|
| 470 |
+
### path 1: local nvidia gpu (primary development)
|
| 471 |
+
|
| 472 |
+
For day-to-day development and testing on your own NVIDIA GPU machine.
|
| 473 |
+
|
| 474 |
+
```bash
|
| 475 |
+
# 1. Ensure Docker + nvidia-container-toolkit installed
|
| 476 |
+
docker run --rm --gpus all nvidia/cuda:11.3-base nvidia-smi
|
| 477 |
+
|
| 478 |
+
# 2. Pull DeepISLES image
|
| 479 |
+
docker pull isleschallenge/deepisles
|
| 480 |
+
|
| 481 |
+
# 3. Run the app
|
| 482 |
+
uv run python -m stroke_deepisles_demo.ui.app
|
| 483 |
+
```
|
| 484 |
+
|
| 485 |
+
**Pros**:
|
| 486 |
+
- Free (you own the hardware)
|
| 487 |
+
- Fast iteration
|
| 488 |
+
- No network dependency
|
| 489 |
+
|
| 490 |
+
**Cons**:
|
| 491 |
+
- Requires NVIDIA GPU hardware
|
| 492 |
+
|
| 493 |
+
### path 2: hf spaces docker sdk + gpu (on-demand demos)
|
| 494 |
+
|
| 495 |
+
For showcasing to others. Spin up when needed, pause when done.
|
| 496 |
+
|
| 497 |
+
#### dockerfile for hf spaces
|
| 498 |
+
|
| 499 |
+
```dockerfile
|
| 500 |
+
# Dockerfile for HF Spaces
|
| 501 |
+
FROM isleschallenge/deepisles:latest
|
| 502 |
+
|
| 503 |
+
# Add our application
|
| 504 |
+
COPY requirements.txt /app/
|
| 505 |
+
RUN pip install -r /app/requirements.txt
|
| 506 |
+
|
| 507 |
+
COPY src/ /app/src/
|
| 508 |
+
COPY app.py /app/
|
| 509 |
+
|
| 510 |
+
WORKDIR /app
|
| 511 |
+
EXPOSE 7860
|
| 512 |
+
CMD ["python", "-m", "stroke_deepisles_demo.ui.app"]
|
| 513 |
+
```
|
| 514 |
+
|
| 515 |
+
#### readme.md configuration
|
| 516 |
+
|
| 517 |
+
```yaml
|
| 518 |
+
---
|
| 519 |
+
title: Stroke DeepISLES Demo
|
| 520 |
+
emoji: 🧠
|
| 521 |
+
colorFrom: blue
|
| 522 |
+
colorTo: purple
|
| 523 |
+
sdk: docker
|
| 524 |
+
app_port: 7860
|
| 525 |
+
suggested_hardware: t4-small
|
| 526 |
+
pinned: false
|
| 527 |
+
license: mit
|
| 528 |
+
---
|
| 529 |
+
```
|
| 530 |
+
|
| 531 |
+
#### cost management: pause/restart api
|
| 532 |
+
|
| 533 |
+
```python
|
| 534 |
+
from huggingface_hub import HfApi
|
| 535 |
+
|
| 536 |
+
api = HfApi()
|
| 537 |
+
SPACE_ID = "your-username/stroke-deepisles-demo"
|
| 538 |
+
|
| 539 |
+
# PAUSE - stops billing immediately
|
| 540 |
+
api.pause_space(SPACE_ID)
|
| 541 |
+
|
| 542 |
+
# RESTART - spin up for demo
|
| 543 |
+
api.restart_space(SPACE_ID)
|
| 544 |
+
|
| 545 |
+
# AUTO-SLEEP after 30 min inactivity
|
| 546 |
+
api.set_space_sleep_time(SPACE_ID, sleep_time=1800)
|
| 547 |
+
```
|
| 548 |
+
|
| 549 |
+
#### billing breakdown
|
| 550 |
+
|
| 551 |
+
| State | Billed? | How to Enter |
|
| 552 |
+
|-------|---------|--------------|
|
| 553 |
+
| Running | ✅ $0.40/hr (T4) | `restart_space()` or visitor wakes it |
|
| 554 |
+
| Sleeping | ❌ $0 | Auto after `sleep_time` inactivity |
|
| 555 |
+
| Paused | ❌ $0 | `pause_space()` - only owner can restart |
|
| 556 |
+
|
| 557 |
+
**Typical demo session**: 30-60 minutes = **$0.20-$0.40**
|
| 558 |
+
|
| 559 |
+
**Monthly cost if paused**: **$0.00**
|
| 560 |
+
|
| 561 |
+
---
|
| 562 |
+
|
| 563 |
+
## niivue integration analysis
|
| 564 |
+
|
| 565 |
+
### current implementation
|
| 566 |
+
|
| 567 |
+
Our viewer uses NiiVue loaded from unpkg CDN with base64 data URLs:
|
| 568 |
+
|
| 569 |
+
```python
|
| 570 |
+
# viewer.py:289-324
|
| 571 |
+
return f"""
|
| 572 |
+
<div style="width:100%; height:{height}px; ...">
|
| 573 |
+
<canvas id="niivue-canvas" style="width:100%; height:100%;"></canvas>
|
| 574 |
+
</div>
|
| 575 |
+
<script type="module">
|
| 576 |
+
const niivueModule = await import('https://unpkg.com/@niivue/niivue@0.65.0/dist/index.js');
|
| 577 |
+
const Niivue = niivueModule.Niivue;
|
| 578 |
+
// ...
|
| 579 |
+
await nv.loadVolumes(volumes);
|
| 580 |
+
</script>
|
| 581 |
+
"""
|
| 582 |
+
```
|
| 583 |
+
|
| 584 |
+
### potential issues
|
| 585 |
+
|
| 586 |
+
1. **Script execution**: `<script type="module">` may not execute in `gr.HTML`
|
| 587 |
+
2. **Canvas element IDs**: Hardcoded `id="niivue-canvas"` will conflict if multiple viewers
|
| 588 |
+
3. **CSP headers**: External CDN might be blocked by Content Security Policy
|
| 589 |
+
4. **Memory**: Base64 NIfTI files loaded entirely into browser memory
|
| 590 |
+
|
| 591 |
+
### recommended fixes
|
| 592 |
+
|
| 593 |
+
```python
|
| 594 |
+
import uuid
|
| 595 |
+
|
| 596 |
+
def create_niivue_html(volume_url: str, mask_url: str | None = None, *, height: int = 400) -> str:
|
| 597 |
+
"""Create HTML/JS for NiiVue viewer with unique IDs."""
|
| 598 |
+
canvas_id = f"niivue-canvas-{uuid.uuid4().hex[:8]}"
|
| 599 |
+
|
| 600 |
+
# ... rest of implementation with unique canvas_id
|
| 601 |
+
```
|
| 602 |
+
|
| 603 |
+
### webgl compatibility
|
| 604 |
+
|
| 605 |
+
NiiVue requires WebGL2. Most modern browsers support it, but:
|
| 606 |
+
|
| 607 |
+
- HF Spaces renders in iframes
|
| 608 |
+
- Some iframe security policies restrict WebGL
|
| 609 |
+
- Cross-origin isolation may be needed for SharedArrayBuffer
|
| 610 |
+
|
| 611 |
+
**Test required**: Verify NiiVue WebGL works in HF Spaces iframe environment.
|
| 612 |
+
|
| 613 |
+
---
|
| 614 |
+
|
| 615 |
+
## memory and performance
|
| 616 |
+
|
| 617 |
+
### memory considerations
|
| 618 |
+
|
| 619 |
+
| Resource | Size | Concern |
|
| 620 |
+
|----------|------|---------|
|
| 621 |
+
| DWI NIfTI (ISLES24-MR-Lite) | ~2-5 MB | Low |
|
| 622 |
+
| Base64 encoded | ~3-7 MB | ~1.33x overhead |
|
| 623 |
+
| Multiple volumes in browser | ~15-20 MB | Moderate |
|
| 624 |
+
| Matplotlib figures | ~1-5 MB | Low |
|
| 625 |
+
| Free tier RAM | 16 GB | Sufficient |
|
| 626 |
+
|
| 627 |
+
### optimization strategies
|
| 628 |
+
|
| 629 |
+
1. **Lazy loading**: Don't load all cases at startup
|
| 630 |
+
2. **Cleanup**: Clear matplotlib figures after rendering
|
| 631 |
+
3. **Pagination**: Limit case dropdown to reasonable number
|
| 632 |
+
4. **Compression**: NIfTI files are already gzipped
|
| 633 |
+
|
| 634 |
+
---
|
| 635 |
+
|
| 636 |
+
## testing checklist
|
| 637 |
+
|
| 638 |
+
Before deploying to HF Spaces, verify:
|
| 639 |
+
|
| 640 |
+
### local testing
|
| 641 |
+
|
| 642 |
+
- [ ] `uv run python app.py` launches without errors
|
| 643 |
+
- [ ] Case dropdown populates
|
| 644 |
+
- [ ] NiiVue viewer renders (in browser, not headless)
|
| 645 |
+
- [ ] Matplotlib plots display correctly
|
| 646 |
+
- [ ] No import-time side effects (network calls)
|
| 647 |
+
|
| 648 |
+
### hf spaces testing
|
| 649 |
+
|
| 650 |
+
- [ ] Create private Space first
|
| 651 |
+
- [ ] Verify dependencies install
|
| 652 |
+
- [ ] Check JavaScript execution in `gr.HTML`
|
| 653 |
+
- [ ] Test NiiVue WebGL rendering
|
| 654 |
+
- [ ] Monitor memory usage
|
| 655 |
+
- [ ] Test on mobile browsers (if applicable)
|
| 656 |
+
|
| 657 |
+
### known issues to monitor
|
| 658 |
+
|
| 659 |
+
1. **Startup timeout**: Default is 30 minutes, may need adjustment
|
| 660 |
+
2. **Sleep behavior**: Free Spaces sleep after 48h of inactivity
|
| 661 |
+
3. **Build cache**: May cause "storage limit exceeded"
|
| 662 |
+
|
| 663 |
+
---
|
| 664 |
+
|
| 665 |
+
## deployment procedure
|
| 666 |
+
|
| 667 |
+
### step 1: verify local nvidia gpu setup
|
| 668 |
+
|
| 669 |
+
```bash
|
| 670 |
+
# Verify NVIDIA driver and Docker GPU support
|
| 671 |
+
docker run --rm --gpus all nvidia/cuda:11.3-base nvidia-smi
|
| 672 |
+
|
| 673 |
+
# Pull DeepISLES image
|
| 674 |
+
docker pull isleschallenge/deepisles
|
| 675 |
+
|
| 676 |
+
# Test local inference
|
| 677 |
+
uv run stroke-demo run --case sub-stroke0001
|
| 678 |
+
```
|
| 679 |
+
|
| 680 |
+
### step 2: create dockerfile for hf spaces
|
| 681 |
+
|
| 682 |
+
```dockerfile
|
| 683 |
+
# Dockerfile
|
| 684 |
+
FROM isleschallenge/deepisles:latest
|
| 685 |
+
|
| 686 |
+
# Install additional dependencies
|
| 687 |
+
COPY requirements.txt /app/
|
| 688 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 689 |
+
|
| 690 |
+
# Copy application code
|
| 691 |
+
COPY src/ /app/src/
|
| 692 |
+
COPY app.py /app/
|
| 693 |
+
|
| 694 |
+
WORKDIR /app
|
| 695 |
+
EXPOSE 7860
|
| 696 |
+
|
| 697 |
+
CMD ["python", "-m", "stroke_deepisles_demo.ui.app"]
|
| 698 |
+
```
|
| 699 |
+
|
| 700 |
+
### step 3: create requirements.txt
|
| 701 |
+
|
| 702 |
+
```bash
|
| 703 |
+
cat > requirements.txt << 'EOF'
|
| 704 |
+
git+https://github.com/CloseChoice/datasets.git@feat/bids-loader-streaming-upload-fix
|
| 705 |
+
huggingface-hub>=0.25.0
|
| 706 |
+
nibabel>=5.2.0
|
| 707 |
+
numpy>=1.26.0
|
| 708 |
+
pydantic>=2.5.0
|
| 709 |
+
pydantic-settings>=2.1.0
|
| 710 |
+
gradio>=6.0.0,<7.0.0
|
| 711 |
+
matplotlib>=3.8.0
|
| 712 |
+
requests>=2.0.0
|
| 713 |
+
EOF
|
| 714 |
+
```
|
| 715 |
+
|
| 716 |
+
### step 4: update readme.md for docker sdk
|
| 717 |
+
|
| 718 |
+
```yaml
|
| 719 |
+
---
|
| 720 |
+
title: Stroke DeepISLES Demo
|
| 721 |
+
emoji: 🧠
|
| 722 |
+
colorFrom: blue
|
| 723 |
+
colorTo: purple
|
| 724 |
+
sdk: docker
|
| 725 |
+
app_port: 7860
|
| 726 |
+
suggested_hardware: t4-small
|
| 727 |
+
pinned: false
|
| 728 |
+
license: mit
|
| 729 |
+
---
|
| 730 |
+
```
|
| 731 |
+
|
| 732 |
+
### step 5: deploy to private space
|
| 733 |
+
|
| 734 |
+
```bash
|
| 735 |
+
# Create Docker Space with GPU
|
| 736 |
+
huggingface-cli repo create stroke-deepisles-demo --type space --sdk docker
|
| 737 |
+
|
| 738 |
+
# Push code
|
| 739 |
+
git remote add space https://huggingface.co/spaces/YOUR_USERNAME/stroke-deepisles-demo
|
| 740 |
+
git push space main
|
| 741 |
+
```
|
| 742 |
+
|
| 743 |
+
### step 6: configure cost management
|
| 744 |
+
|
| 745 |
+
```python
|
| 746 |
+
from huggingface_hub import HfApi
|
| 747 |
+
|
| 748 |
+
api = HfApi()
|
| 749 |
+
SPACE_ID = "YOUR_USERNAME/stroke-deepisles-demo"
|
| 750 |
+
|
| 751 |
+
# Set auto-sleep after 30 min of inactivity
|
| 752 |
+
api.set_space_sleep_time(SPACE_ID, sleep_time=1800)
|
| 753 |
+
|
| 754 |
+
# After demo: pause to stop all billing
|
| 755 |
+
api.pause_space(SPACE_ID)
|
| 756 |
+
|
| 757 |
+
# Before next demo: restart
|
| 758 |
+
api.restart_space(SPACE_ID)
|
| 759 |
+
```
|
| 760 |
+
|
| 761 |
+
### step 7: monitor and iterate
|
| 762 |
+
|
| 763 |
+
- Check build logs (Docker builds can take 10-20 min)
|
| 764 |
+
- Test inference end-to-end
|
| 765 |
+
- Verify NiiVue visualization works
|
| 766 |
+
- Pause Space when done testing
|
| 767 |
+
|
| 768 |
+
---
|
| 769 |
+
|
| 770 |
+
## decision matrix
|
| 771 |
+
|
| 772 |
+
| Approach | Real Inference | Cost | Complexity | Use Case |
|
| 773 |
+
|----------|----------------|------|------------|----------|
|
| 774 |
+
| Local NVIDIA GPU | ✅ | $0 | Low | **Primary development** |
|
| 775 |
+
| HF Spaces Docker + GPU (on-demand) | ✅ | ~$0.40/demo | Medium | **Showcasing to others** |
|
| 776 |
+
| ~~Demo Mode (pre-computed)~~ | ❌ Fake | $0 | Low | ~~Rejected - defeats purpose~~ |
|
| 777 |
+
| ~~HF Spaces Gradio SDK (free)~~ | ❌ No Docker | $0 | Low | ~~Cannot run DeepISLES~~ |
|
| 778 |
+
| ~~ZeroGPU (free H200)~~ | ❌ No Docker | $0 | Low | ~~Only supports Gradio SDK~~ |
|
| 779 |
+
|
| 780 |
+
---
|
| 781 |
+
|
| 782 |
+
## sources
|
| 783 |
+
|
| 784 |
+
### official documentation
|
| 785 |
+
- [Gradio Spaces](https://huggingface.co/docs/hub/en/spaces-sdks-gradio)
|
| 786 |
+
- [Gradio 6 Migration Guide](https://www.gradio.app/main/guides/gradio-6-migration-guide)
|
| 787 |
+
- [Custom CSS and JS](https://www.gradio.app/guides/custom-CSS-and-JS)
|
| 788 |
+
- [Custom Components In Five Minutes](https://www.gradio.app/guides/custom-components-in-five-minutes)
|
| 789 |
+
- [Spaces Configuration Reference](https://huggingface.co/docs/hub/en/spaces-config-reference)
|
| 790 |
+
- [Spaces Persistent Storage](https://huggingface.co/docs/hub/en/spaces-storage)
|
| 791 |
+
- [Manage Spaces - HF Hub](https://huggingface.co/docs/huggingface_hub/main/en/guides/manage-spaces)
|
| 792 |
+
- [A Security Review of Gradio 5](https://huggingface.co/blog/gradio-5-security)
|
| 793 |
+
- [Trail of Bits Gradio Audit](https://blog.trailofbits.com/2024/10/10/auditing-gradio-5-hugging-faces-ml-gui-framework/)
|
| 794 |
+
- [Docker Spaces](https://huggingface.co/docs/hub/spaces-sdks-docker)
|
| 795 |
+
- [ZeroGPU Documentation](https://huggingface.co/docs/hub/en/spaces-zerogpu)
|
| 796 |
+
|
| 797 |
+
### forum discussions (cold start verification)
|
| 798 |
+
- [Slow Space Cold Boot](https://discuss.huggingface.co/t/slow-space-cold-boot/72154) - 2 min baseline confirmed
|
| 799 |
+
- [T4 startup taking 45+ minutes](https://discuss.huggingface.co/t/staring-up-t4-instances-is-taking-45-minutes/139567) - Feb 2025 resource issues
|
| 800 |
+
- [Space stuck at Starting](https://discuss.huggingface.co/t/hf-space-stuck-at-starting/170911) - Nov 2025 edge case
|
| 801 |
+
- [Space stuck in Paused](https://discuss.huggingface.co/t/space-stuck-in-paused/169467) - Oct 2025 edge case
|
| 802 |
+
- [ZeroGPU Docker request](https://huggingface.co/spaces/zero-gpu-explorers/README/discussions/27) - Community asking for Docker support
|
| 803 |
+
- [Gradio HTML component with javascript code don't work](https://discuss.huggingface.co/t/gradio-html-component-with-javascript-code-dont-work/37316)
|
| 804 |
+
|
| 805 |
+
### packages
|
| 806 |
+
- [NiiVue npm package](https://www.npmjs.com/package/@niivue/niivue) - v0.65.0 (latest as of Dec 2025)
|
| 807 |
+
- [gradio-iframe PyPI](https://pypi.org/project/gradio-iframe/) - v0.0.10 (experimental)
|
| 808 |
+
- [DeepISLES Docker Hub](https://hub.docker.com/r/isleschallenge/deepisles)
|
| 809 |
+
|
| 810 |
+
---
|
| 811 |
+
|
| 812 |
+
## appendix: friction points summary
|
| 813 |
+
|
| 814 |
+
### high priority (must fix before deployment)
|
| 815 |
+
|
| 816 |
+
1. **JavaScript execution in `gr.HTML`**
|
| 817 |
+
- Current: `<script type="module">` embedded in HTML string
|
| 818 |
+
- Risk: May not execute at all
|
| 819 |
+
- Fix: Use `gr.Blocks(js=...)` or `demo.load(_js=...)`
|
| 820 |
+
- Testing: Required on actual HF Spaces environment
|
| 821 |
+
|
| 822 |
+
2. **Docker + GPU requirement**
|
| 823 |
+
- Current: Pipeline requires `isleschallenge/deepisles` container with NVIDIA GPU
|
| 824 |
+
- Risk: Gradio SDK cannot run Docker; Apple Silicon not supported
|
| 825 |
+
- Fix: Use Docker SDK with GPU hardware (on-demand billing)
|
| 826 |
+
|
| 827 |
+
### medium priority (should fix)
|
| 828 |
+
|
| 829 |
+
3. **Unique canvas IDs**
|
| 830 |
+
- Current: Hardcoded `id="niivue-canvas"`
|
| 831 |
+
- Risk: Multiple viewers would conflict
|
| 832 |
+
- Fix: Generate unique IDs with UUID
|
| 833 |
+
|
| 834 |
+
4. **Git dependency in requirements**
|
| 835 |
+
- Current: `datasets @ git+https://...` in pyproject.toml
|
| 836 |
+
- Risk: HF Spaces uses requirements.txt
|
| 837 |
+
- Fix: Create requirements.txt with git URL
|
| 838 |
+
|
| 839 |
+
### low priority (nice to have)
|
| 840 |
+
|
| 841 |
+
5. **Memory optimization**
|
| 842 |
+
- Current: Full NIfTI files in base64
|
| 843 |
+
- Risk: Could hit memory limits on complex cases
|
| 844 |
+
- Fix: Implement streaming or pagination
|
| 845 |
+
|
| 846 |
+
6. **CDN reliability**
|
| 847 |
+
- Current: NiiVue from unpkg.com
|
| 848 |
+
- Risk: CDN downtime affects app
|
| 849 |
+
- Fix: Consider bundling or alternative CDN
|
| 850 |
+
|
| 851 |
+
---
|
| 852 |
+
|
| 853 |
+
## appendix: operational runbook
|
| 854 |
+
|
| 855 |
+
### daily operations
|
| 856 |
+
|
| 857 |
+
**After development session:**
|
| 858 |
+
```bash
|
| 859 |
+
# Always pause to stop billing
|
| 860 |
+
python -c "
|
| 861 |
+
from huggingface_hub import HfApi
|
| 862 |
+
api = HfApi()
|
| 863 |
+
api.pause_space('YOUR_USERNAME/stroke-deepisles-demo')
|
| 864 |
+
print('Space paused - billing stopped')
|
| 865 |
+
"
|
| 866 |
+
```
|
| 867 |
+
|
| 868 |
+
**Before scheduled demo:**
|
| 869 |
+
```bash
|
| 870 |
+
# T-30 minutes: Start warm-up
|
| 871 |
+
python -c "
|
| 872 |
+
from huggingface_hub import HfApi
|
| 873 |
+
api = HfApi()
|
| 874 |
+
api.restart_space('YOUR_USERNAME/stroke-deepisles-demo')
|
| 875 |
+
print('Warming up... check status in 5 min')
|
| 876 |
+
"
|
| 877 |
+
|
| 878 |
+
# T-25, T-20, T-15, T-10, T-5 minutes: Check status
|
| 879 |
+
python -c "
|
| 880 |
+
from huggingface_hub import HfApi
|
| 881 |
+
api = HfApi()
|
| 882 |
+
info = api.space_info('YOUR_USERNAME/stroke-deepisles-demo')
|
| 883 |
+
print(f'Status: {info.runtime.stage}')
|
| 884 |
+
# BUILDING -> Wait
|
| 885 |
+
# RUNNING_BUILDING -> Almost ready
|
| 886 |
+
# RUNNING -> Ready to demo!
|
| 887 |
+
"
|
| 888 |
+
```
|
| 889 |
+
|
| 890 |
+
**After demo:**
|
| 891 |
+
```bash
|
| 892 |
+
# Immediately pause to stop billing
|
| 893 |
+
python -c "
|
| 894 |
+
from huggingface_hub import HfApi
|
| 895 |
+
api = HfApi()
|
| 896 |
+
api.pause_space('YOUR_USERNAME/stroke-deepisles-demo')
|
| 897 |
+
print('Demo complete - billing stopped')
|
| 898 |
+
"
|
| 899 |
+
```
|
| 900 |
+
|
| 901 |
+
### troubleshooting
|
| 902 |
+
|
| 903 |
+
| Symptom | Diagnosis | Resolution |
|
| 904 |
+
|---------|-----------|------------|
|
| 905 |
+
| Status stuck on "BUILDING" > 45 min | Build timeout | Check build logs, optimize Dockerfile |
|
| 906 |
+
| Status stuck on "STARTING" > 30 min | Resource issue | Factory rebuild, or try different hardware |
|
| 907 |
+
| Status stuck on "PAUSED" after restart | API issue | Wait 5 min, retry, or use UI |
|
| 908 |
+
| "Scheduling failure" error | GPU unavailable | Try later or different hardware tier |
|
| 909 |
+
| "Storage limit exceeded" | Build cache full | Clear cache, reduce image layers |
|
| 910 |
+
|
| 911 |
+
### cost tracking
|
| 912 |
+
|
| 913 |
+
```bash
|
| 914 |
+
# Check current month's usage
|
| 915 |
+
# Visit: https://huggingface.co/settings/billing
|
| 916 |
+
|
| 917 |
+
# Estimate cost per demo:
|
| 918 |
+
# T4-small: $0.40/hr × 0.5 hr = $0.20 per 30-min demo
|
| 919 |
+
# T4-medium: $0.60/hr × 0.5 hr = $0.30 per 30-min demo
|
| 920 |
+
# A10G-small: $1.05/hr × 0.5 hr = $0.53 per 30-min demo
|
| 921 |
+
```
|
| 922 |
+
|
| 923 |
+
---
|
| 924 |
+
|
| 925 |
+
## next steps
|
| 926 |
+
|
| 927 |
+
> **Status**: Spec APPROVED - Ready for implementation
|
| 928 |
+
|
| 929 |
+
1. ~~Senior Review: Get approval on this spec~~ ✅ **APPROVED**
|
| 930 |
+
2. **Local Testing**: Verify full pipeline on local NVIDIA GPU machine
|
| 931 |
+
3. **Fix JavaScript Pattern**: Refactor NiiVue initialization for `gr.HTML`
|
| 932 |
+
4. **Create Dockerfile**: Build HF Spaces Docker image based on DeepISLES
|
| 933 |
+
5. **Create requirements.txt**: Generate from pyproject.toml
|
| 934 |
+
6. **Deploy to Private Space**: Test Docker SDK + GPU on HF Spaces
|
| 935 |
+
7. **Configure Auto-Sleep**: Set `sleep_time=1800` (30 min) to minimize costs
|
| 936 |
+
8. **Pre-Demo Test**: Practice warm-up procedure (20-30 min cold start)
|
| 937 |
+
9. **Demo & Pause**: Show to stakeholders, then `pause_space()` to stop billing
|
| 938 |
+
10. **Public Release**: Make Space public when stable (keep paused when not demoing)
|
pyproject.toml
CHANGED
|
@@ -47,6 +47,7 @@ dev = [
|
|
| 47 |
"pytest>=8.0.0",
|
| 48 |
"pytest-cov>=4.1.0",
|
| 49 |
"pytest-mock>=3.12.0",
|
|
|
|
| 50 |
"mypy>=1.19.0",
|
| 51 |
"ruff>=0.14.0",
|
| 52 |
"pre-commit>=3.6.0",
|
|
|
|
| 47 |
"pytest>=8.0.0",
|
| 48 |
"pytest-cov>=4.1.0",
|
| 49 |
"pytest-mock>=3.12.0",
|
| 50 |
+
"pytest-timeout>=2.3.0",
|
| 51 |
"mypy>=1.19.0",
|
| 52 |
"ruff>=0.14.0",
|
| 53 |
"pre-commit>=3.6.0",
|
tests/data/test_integration_real_data.py
CHANGED
|
@@ -11,6 +11,7 @@ from stroke_deepisles_demo.data.loader import load_isles_dataset
|
|
| 11 |
REAL_DATA_PATH = Path("data/isles24")
|
| 12 |
|
| 13 |
|
|
|
|
| 14 |
@pytest.mark.skipif(not REAL_DATA_PATH.exists(), reason="Real data not found in data/isles24")
|
| 15 |
def test_load_real_data_count() -> None:
|
| 16 |
"""Verify that we can load the expected number of cases from real data."""
|
|
@@ -28,6 +29,7 @@ def test_load_real_data_count() -> None:
|
|
| 28 |
assert case["ground_truth"].exists()
|
| 29 |
|
| 30 |
|
|
|
|
| 31 |
@pytest.mark.skipif(not REAL_DATA_PATH.exists(), reason="Real data not found in data/isles24")
|
| 32 |
def test_real_data_subject_ids() -> None:
|
| 33 |
"""Verify subject ID formatting on real data."""
|
|
|
|
| 11 |
REAL_DATA_PATH = Path("data/isles24")
|
| 12 |
|
| 13 |
|
| 14 |
+
@pytest.mark.integration
|
| 15 |
@pytest.mark.skipif(not REAL_DATA_PATH.exists(), reason="Real data not found in data/isles24")
|
| 16 |
def test_load_real_data_count() -> None:
|
| 17 |
"""Verify that we can load the expected number of cases from real data."""
|
|
|
|
| 29 |
assert case["ground_truth"].exists()
|
| 30 |
|
| 31 |
|
| 32 |
+
@pytest.mark.integration
|
| 33 |
@pytest.mark.skipif(not REAL_DATA_PATH.exists(), reason="Real data not found in data/isles24")
|
| 34 |
def test_real_data_subject_ids() -> None:
|
| 35 |
"""Verify subject ID formatting on real data."""
|
tests/inference/test_deepisles.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
from
|
| 6 |
from unittest.mock import MagicMock, patch
|
| 7 |
|
| 8 |
import pytest
|
|
@@ -16,6 +16,9 @@ from stroke_deepisles_demo.inference.deepisles import (
|
|
| 16 |
)
|
| 17 |
from stroke_deepisles_demo.inference.docker import check_docker_available
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
class TestValidateInputFolder:
|
| 21 |
"""Tests for validate_input_folder."""
|
|
@@ -161,7 +164,7 @@ class TestRunDeepIslesOnFolder:
|
|
| 161 |
class TestDeepIslesIntegration:
|
| 162 |
"""Integration tests requiring real Docker and DeepISLES image."""
|
| 163 |
|
| 164 |
-
def test_real_inference(self, synthetic_case_files: object) -> None:
|
| 165 |
"""Run actual DeepISLES inference on synthetic data."""
|
| 166 |
if not check_docker_available():
|
| 167 |
pytest.skip("Docker not available")
|
|
@@ -171,7 +174,7 @@ class TestDeepIslesIntegration:
|
|
| 171 |
# Stage the synthetic files
|
| 172 |
staged = stage_case_for_deepisles(
|
| 173 |
synthetic_case_files, # type: ignore
|
| 174 |
-
|
| 175 |
)
|
| 176 |
|
| 177 |
try:
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from typing import TYPE_CHECKING
|
| 6 |
from unittest.mock import MagicMock, patch
|
| 7 |
|
| 8 |
import pytest
|
|
|
|
| 16 |
)
|
| 17 |
from stroke_deepisles_demo.inference.docker import check_docker_available
|
| 18 |
|
| 19 |
+
if TYPE_CHECKING:
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
|
| 23 |
class TestValidateInputFolder:
|
| 24 |
"""Tests for validate_input_folder."""
|
|
|
|
| 164 |
class TestDeepIslesIntegration:
|
| 165 |
"""Integration tests requiring real Docker and DeepISLES image."""
|
| 166 |
|
| 167 |
+
def test_real_inference(self, synthetic_case_files: object, temp_dir: Path) -> None:
|
| 168 |
"""Run actual DeepISLES inference on synthetic data."""
|
| 169 |
if not check_docker_available():
|
| 170 |
pytest.skip("Docker not available")
|
|
|
|
| 174 |
# Stage the synthetic files
|
| 175 |
staged = stage_case_for_deepisles(
|
| 176 |
synthetic_case_files, # type: ignore
|
| 177 |
+
temp_dir / "deepisles_test",
|
| 178 |
)
|
| 179 |
|
| 180 |
try:
|
tests/test_pipeline.py
CHANGED
|
@@ -281,7 +281,7 @@ class TestPipelineIntegration:
|
|
| 281 |
"""Integration tests for full pipeline."""
|
| 282 |
|
| 283 |
@pytest.mark.slow
|
| 284 |
-
def test_run_on_real_case(self) -> None:
|
| 285 |
"""Run pipeline on actual ISLES24-MR-Lite case."""
|
| 286 |
# Requires: network, Docker, DeepISLES image
|
| 287 |
# Run with: pytest -m "integration and slow"
|
|
@@ -296,7 +296,7 @@ class TestPipelineIntegration:
|
|
| 296 |
fast=True,
|
| 297 |
gpu=False,
|
| 298 |
compute_dice=True,
|
| 299 |
-
output_dir=
|
| 300 |
)
|
| 301 |
|
| 302 |
assert result.prediction_mask.exists()
|
|
|
|
| 281 |
"""Integration tests for full pipeline."""
|
| 282 |
|
| 283 |
@pytest.mark.slow
|
| 284 |
+
def test_run_on_real_case(self, temp_dir: Path) -> None:
|
| 285 |
"""Run pipeline on actual ISLES24-MR-Lite case."""
|
| 286 |
# Requires: network, Docker, DeepISLES image
|
| 287 |
# Run with: pytest -m "integration and slow"
|
|
|
|
| 296 |
fast=True,
|
| 297 |
gpu=False,
|
| 298 |
compute_dice=True,
|
| 299 |
+
output_dir=temp_dir / "pipeline_test_output",
|
| 300 |
)
|
| 301 |
|
| 302 |
assert result.prediction_mask.exists()
|
uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|