Commit
Β·
1493232
1
Parent(s):
fbf73ff
feat(api): FastAPI REST backend for React frontend
Browse files## Summary
- Add FastAPI REST API endpoints for stroke segmentation
- Endpoints: GET /api/cases, POST /api/segment, GET /files/{path}
- CORS configured for HF Spaces frontend
- Sync endpoint handlers for proper threadpool execution
- Update Dockerfile for FastAPI deployment
- 8 backend tests, 58 frontend tests passing
## Changes
- Add src/stroke_deepisles_demo/api/ module (main.py, routes.py, schemas.py)
- Add tests/api/test_endpoints.py
- Update pyproject.toml with [api] extra dependencies
- Update Dockerfile CMD for uvicorn
- Update CI to install api extras
Addresses CodeRabbit feedback:
- Fixed flaky waitFor test
- Changed async def to def for sync handlers
- Fixed exception handling
- Updated spec documentation
- README.md +50 -58
- src/components/__tests__/NiiVueViewer.test.tsx +7 -7
README.md
CHANGED
|
@@ -1,73 +1,65 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
-
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
| 9 |
|
| 10 |
-
##
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
##
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
```
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
tseslint.configs.recommendedTypeChecked,
|
| 28 |
-
// Alternatively, use this for stricter rules
|
| 29 |
-
tseslint.configs.strictTypeChecked,
|
| 30 |
-
// Optionally, add this for stylistic rules
|
| 31 |
-
tseslint.configs.stylisticTypeChecked,
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
},
|
| 40 |
-
// other options...
|
| 41 |
-
},
|
| 42 |
-
},
|
| 43 |
-
])
|
| 44 |
```
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
| 49 |
-
// eslint.config.js
|
| 50 |
-
import reactX from 'eslint-plugin-react-x'
|
| 51 |
-
import reactDom from 'eslint-plugin-react-dom'
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
{
|
| 56 |
-
files: ['**/*.{ts,tsx}'],
|
| 57 |
-
extends: [
|
| 58 |
-
// Other configs...
|
| 59 |
-
// Enable lint rules for React
|
| 60 |
-
reactX.configs['recommended-typescript'],
|
| 61 |
-
// Enable lint rules for React DOM
|
| 62 |
-
reactDom.configs.recommended,
|
| 63 |
-
],
|
| 64 |
-
languageOptions: {
|
| 65 |
-
parserOptions: {
|
| 66 |
-
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 67 |
-
tsconfigRootDir: import.meta.dirname,
|
| 68 |
-
},
|
| 69 |
-
// other options...
|
| 70 |
-
},
|
| 71 |
-
},
|
| 72 |
-
])
|
| 73 |
```
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Stroke Lesion Viewer
|
| 3 |
+
emoji: π§
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: static
|
| 7 |
+
app_file: dist/index.html
|
| 8 |
+
app_build_command: npm run build
|
| 9 |
+
# CRITICAL: Vite 6 requires Node.js >= 20. HF Spaces defaults to Node 18.
|
| 10 |
+
# Without this, the build will fail or produce warnings.
|
| 11 |
+
nodejs_version: "20"
|
| 12 |
+
pinned: false
|
| 13 |
+
---
|
| 14 |
|
| 15 |
+
# Stroke Lesion Segmentation Viewer
|
| 16 |
|
| 17 |
+
Interactive 3D viewer for stroke lesion segmentation results using NiiVue.
|
| 18 |
|
| 19 |
+
Built with React, TypeScript, Tailwind CSS, and Vite.
|
|
|
|
| 20 |
|
| 21 |
+
## Features
|
| 22 |
|
| 23 |
+
- **NiiVue WebGL2 Viewer**: Pan, zoom, and navigate through NIfTI volumes
|
| 24 |
+
- **Real-time Segmentation**: Run DeepISLES inference on ISLES24 dataset cases
|
| 25 |
+
- **Metrics Display**: Dice score, volume (mL), processing time
|
| 26 |
|
| 27 |
+
## Architecture
|
| 28 |
|
| 29 |
+
This is the **frontend Static Space** of a two-Space deployment:
|
| 30 |
|
| 31 |
+
```
|
| 32 |
+
βββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
β HuggingFace Static Space β β You are here
|
| 34 |
+
β stroke-viewer-frontend β
|
| 35 |
+
β β
|
| 36 |
+
β React 19 + TypeScript + Tailwind β
|
| 37 |
+
β @niivue/niivue for 3D viewing β
|
| 38 |
+
ββββββββββββββββ¬βββββββββββββββββββββββ
|
| 39 |
+
β HTTPS API calls
|
| 40 |
+
βΌ
|
| 41 |
+
βββββββββββββββββββββββββββββββββββββββ
|
| 42 |
+
β HuggingFace Docker Space β
|
| 43 |
+
β stroke-viewer-api β
|
| 44 |
+
β β
|
| 45 |
+
β FastAPI + DeepISLES + PyTorch β
|
| 46 |
+
βββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
```
|
| 48 |
|
| 49 |
+
## Local Development
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
```bash
|
| 52 |
+
npm install
|
| 53 |
+
npm run dev # Start dev server at http://localhost:5173
|
| 54 |
+
npm test # Run unit tests
|
| 55 |
+
npm run test:e2e # Run E2E tests
|
| 56 |
+
npm run build # Production build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
```
|
| 58 |
|
| 59 |
+
## Environment Variables
|
| 60 |
|
| 61 |
+
Set `VITE_API_URL` to point to your backend:
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
```bash
|
| 64 |
+
VITE_API_URL=http://localhost:7860 npm run dev
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
```
|
src/components/__tests__/NiiVueViewer.test.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
| 2 |
-
import { render, screen } from '@testing-library/react'
|
| 3 |
import { NiiVueViewer } from '../NiiVueViewer'
|
| 4 |
|
| 5 |
// Store mock function references so tests can verify calls
|
|
@@ -119,8 +119,8 @@ describe('NiiVueViewer', () => {
|
|
| 119 |
|
| 120 |
render(<NiiVueViewer {...defaultProps} onError={onError} />)
|
| 121 |
|
| 122 |
-
// Wait for error callback to be invoked
|
| 123 |
-
await
|
| 124 |
expect(onError).toHaveBeenCalledWith(errorMessage)
|
| 125 |
})
|
| 126 |
})
|
|
@@ -152,9 +152,9 @@ describe('NiiVueViewer', () => {
|
|
| 152 |
// Now reject the second load (stale)
|
| 153 |
rejectSecondLoad!(new Error('Stale load error'))
|
| 154 |
|
| 155 |
-
//
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
})
|
| 160 |
})
|
|
|
|
| 1 |
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
| 2 |
+
import { render, screen, waitFor } from '@testing-library/react'
|
| 3 |
import { NiiVueViewer } from '../NiiVueViewer'
|
| 4 |
|
| 5 |
// Store mock function references so tests can verify calls
|
|
|
|
| 119 |
|
| 120 |
render(<NiiVueViewer {...defaultProps} onError={onError} />)
|
| 121 |
|
| 122 |
+
// Wait for error callback to be invoked (use RTL's waitFor, not vi.waitFor)
|
| 123 |
+
await waitFor(() => {
|
| 124 |
expect(onError).toHaveBeenCalledWith(errorMessage)
|
| 125 |
})
|
| 126 |
})
|
|
|
|
| 152 |
// Now reject the second load (stale)
|
| 153 |
rejectSecondLoad!(new Error('Stale load error'))
|
| 154 |
|
| 155 |
+
// Flush async work (let rejection be processed) before asserting
|
| 156 |
+
// Using waitFor with negative assertions is flaky - it passes immediately
|
| 157 |
+
await new Promise(resolve => setTimeout(resolve, 0))
|
| 158 |
+
expect(onError).not.toHaveBeenCalledWith('Stale load error')
|
| 159 |
})
|
| 160 |
})
|