VibecoderMcSwaggins commited on
Commit
4994e68
·
unverified ·
1 Parent(s): bfe80c5

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 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 pathlib import Path
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
- Path("/tmp/deepisles_test"),
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=Path("/tmp/pipeline_test_output"), # Use specific 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