Spaces:
Runtime error
feat(phase-5): polish, observability, and documentation (#6)
Browse files* feat(phase-5): add polish, observability, and documentation
Implements Phase 5 deliverables:
- Structured logging with centralized setup (src/stroke_deepisles_demo/core/logging.py)
- Enhanced pydantic-settings configuration (src/stroke_deepisles_demo/core/config.py)
- Complete documentation (README, CONTRIBUTING, CHANGELOG, .env.example, docs/guides/)
- GitHub Actions CI pipeline (.github/workflows/ci.yml)
- Integration of centralized logging into pipeline, UI, and inference modules
Quality:
- 96 tests passing (8 new tests for logging and config)
- Coverage: 82%
- ruff + mypy clean
* fix: address CodeRabbit review feedback for Phase 5
Security:
- docker.py: Redact environment variable values in debug logs to prevent
leaking secrets (e.g., HF tokens)
Bug fixes:
- logging.py: Fix get_logger() to avoid double-prefixing when __name__
already includes package name (was: stroke_deepisles_demo.stroke_deepisles_demo.module)
Documentation:
- configuration.md: Add missing STROKE_DEMO_TEMP_DIR to env var table
- configuration.md: Fix example to use get_settings() instead of imported
settings (which doesn't update after reload_settings())
- README.md: Format bare URL as proper markdown link (MD034)
- .env.example +22 -0
- .github/workflows/ci.yml +86 -0
- CHANGELOG.md +41 -0
- CONTRIBUTING.md +82 -0
- README.md +93 -2
- docs/guides/configuration.md +47 -0
- docs/guides/deployment.md +39 -0
- docs/guides/quickstart.md +67 -0
- src/stroke_deepisles_demo/core/config.py +52 -4
- src/stroke_deepisles_demo/core/logging.py +59 -0
- src/stroke_deepisles_demo/inference/deepisles.py +3 -0
- src/stroke_deepisles_demo/inference/docker.py +26 -0
- src/stroke_deepisles_demo/pipeline.py +2 -2
- src/stroke_deepisles_demo/ui/app.py +3 -3
- src/stroke_deepisles_demo/ui/components.py +2 -3
- tests/core/test_config.py +61 -0
- tests/core/test_logging.py +38 -0
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logging
|
| 2 |
+
STROKE_DEMO_LOG_LEVEL=INFO
|
| 3 |
+
STROKE_DEMO_LOG_FORMAT=simple
|
| 4 |
+
|
| 5 |
+
# HuggingFace
|
| 6 |
+
STROKE_DEMO_HF_DATASET_ID=YongchengYAO/ISLES24-MR-Lite
|
| 7 |
+
# STROKE_DEMO_HF_TOKEN=hf_...
|
| 8 |
+
|
| 9 |
+
# DeepISLES
|
| 10 |
+
STROKE_DEMO_DEEPISLES_DOCKER_IMAGE=isleschallenge/deepisles
|
| 11 |
+
STROKE_DEMO_DEEPISLES_FAST_MODE=true
|
| 12 |
+
STROKE_DEMO_DEEPISLES_TIMEOUT_SECONDS=1800
|
| 13 |
+
STROKE_DEMO_DEEPISLES_USE_GPU=true
|
| 14 |
+
|
| 15 |
+
# Paths
|
| 16 |
+
STROKE_DEMO_RESULTS_DIR=./results
|
| 17 |
+
# STROKE_DEMO_TEMP_DIR=/tmp/custom_temp
|
| 18 |
+
|
| 19 |
+
# UI
|
| 20 |
+
STROKE_DEMO_GRADIO_SERVER_NAME=0.0.0.0
|
| 21 |
+
STROKE_DEMO_GRADIO_SERVER_PORT=7860
|
| 22 |
+
STROKE_DEMO_GRADIO_SHARE=false
|
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
lint:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v4
|
| 14 |
+
|
| 15 |
+
- name: Install uv
|
| 16 |
+
uses: astral-sh/setup-uv@v4
|
| 17 |
+
|
| 18 |
+
- name: Set up Python
|
| 19 |
+
run: uv python install 3.12
|
| 20 |
+
|
| 21 |
+
- name: Install dependencies
|
| 22 |
+
run: uv sync
|
| 23 |
+
|
| 24 |
+
- name: Lint with ruff
|
| 25 |
+
run: uv run ruff check .
|
| 26 |
+
|
| 27 |
+
- name: Check formatting
|
| 28 |
+
run: uv run ruff format --check .
|
| 29 |
+
|
| 30 |
+
typecheck:
|
| 31 |
+
runs-on: ubuntu-latest
|
| 32 |
+
steps:
|
| 33 |
+
- uses: actions/checkout@v4
|
| 34 |
+
|
| 35 |
+
- name: Install uv
|
| 36 |
+
uses: astral-sh/setup-uv@v4
|
| 37 |
+
|
| 38 |
+
- name: Set up Python
|
| 39 |
+
run: uv python install 3.12
|
| 40 |
+
|
| 41 |
+
- name: Install dependencies
|
| 42 |
+
run: uv sync
|
| 43 |
+
|
| 44 |
+
- name: Type check with mypy
|
| 45 |
+
run: uv run mypy src/
|
| 46 |
+
|
| 47 |
+
test:
|
| 48 |
+
runs-on: ubuntu-latest
|
| 49 |
+
steps:
|
| 50 |
+
- uses: actions/checkout@v4
|
| 51 |
+
|
| 52 |
+
- name: Install uv
|
| 53 |
+
uses: astral-sh/setup-uv@v4
|
| 54 |
+
|
| 55 |
+
- name: Set up Python
|
| 56 |
+
run: uv python install 3.12
|
| 57 |
+
|
| 58 |
+
- name: Install dependencies
|
| 59 |
+
run: uv sync
|
| 60 |
+
|
| 61 |
+
- name: Run tests
|
| 62 |
+
run: uv run pytest --cov --cov-report=xml
|
| 63 |
+
|
| 64 |
+
- name: Upload coverage
|
| 65 |
+
uses: codecov/codecov-action@v4
|
| 66 |
+
with:
|
| 67 |
+
files: ./coverage.xml
|
| 68 |
+
token: ${{ secrets.CODECOV_TOKEN }}
|
| 69 |
+
|
| 70 |
+
integration:
|
| 71 |
+
runs-on: ubuntu-latest
|
| 72 |
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
| 73 |
+
steps:
|
| 74 |
+
- uses: actions/checkout@v4
|
| 75 |
+
|
| 76 |
+
- name: Install uv
|
| 77 |
+
uses: astral-sh/setup-uv@v4
|
| 78 |
+
|
| 79 |
+
- name: Set up Python
|
| 80 |
+
run: uv python install 3.12
|
| 81 |
+
|
| 82 |
+
- name: Install dependencies
|
| 83 |
+
run: uv sync
|
| 84 |
+
|
| 85 |
+
- name: Run integration tests
|
| 86 |
+
run: uv run pytest -m integration --timeout=600
|
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changelog
|
| 2 |
+
|
| 3 |
+
All notable changes to this project will be documented in this file.
|
| 4 |
+
|
| 5 |
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
| 6 |
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
| 7 |
+
|
| 8 |
+
## [Unreleased]
|
| 9 |
+
|
| 10 |
+
### Added
|
| 11 |
+
- **Phase 5**: Polish, Observability, and Documentation
|
| 12 |
+
- Structured logging via `stroke_deepisles_demo.core.logging`.
|
| 13 |
+
- Enhanced configuration via `pydantic-settings`.
|
| 14 |
+
- Comprehensive documentation (README, CONTRIBUTING, guides).
|
| 15 |
+
- GitHub Actions CI pipeline.
|
| 16 |
+
|
| 17 |
+
- **Phase 4**: Gradio UI and Visualization
|
| 18 |
+
- Interactive Gradio application (`ui/app.py`).
|
| 19 |
+
- NiiVue integration for 3D/multi-planar visualization.
|
| 20 |
+
- Matplotlib slice comparison plots.
|
| 21 |
+
|
| 22 |
+
- **Phase 3**: End-to-End Pipeline
|
| 23 |
+
- `PipelineResult` and `run_pipeline_on_case`.
|
| 24 |
+
- Metrics calculation (Dice score, Volume).
|
| 25 |
+
- CLI (`stroke-demo`) with `list` and `run` commands.
|
| 26 |
+
|
| 27 |
+
- **Phase 2**: DeepISLES Docker Integration
|
| 28 |
+
- Wrapper for DeepISLES Docker container.
|
| 29 |
+
- Automatic GPU detection and fallback.
|
| 30 |
+
- Input/Output validation and staging.
|
| 31 |
+
|
| 32 |
+
- **Phase 1**: Data Access Layer
|
| 33 |
+
- Integration with HuggingFace Datasets (ISLES24-MR-Lite).
|
| 34 |
+
- Local NIfTI file adapter.
|
| 35 |
+
- Lazy loading of large neuroimaging files.
|
| 36 |
+
|
| 37 |
+
- **Phase 0**: Repository Bootstrap
|
| 38 |
+
- Project structure with `uv` and `hatchling`.
|
| 39 |
+
- Strict typing with `mypy`.
|
| 40 |
+
- Linting/Formatting with `ruff`.
|
| 41 |
+
- Testing with `pytest`.
|
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing to stroke-deepisles-demo
|
| 2 |
+
|
| 3 |
+
Thank you for your interest in contributing!
|
| 4 |
+
|
| 5 |
+
## Development Setup
|
| 6 |
+
|
| 7 |
+
1. **Clone the repository**
|
| 8 |
+
```bash
|
| 9 |
+
git clone https://github.com/The-Obstacle-Is-The-Way/stroke-deepisles-demo.git
|
| 10 |
+
cd stroke-deepisles-demo
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
2. **Install uv** (if not already installed)
|
| 14 |
+
```bash
|
| 15 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
3. **Install dependencies**
|
| 19 |
+
```bash
|
| 20 |
+
uv sync
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
4. **Install pre-commit hooks**
|
| 24 |
+
```bash
|
| 25 |
+
uv run pre-commit install
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Running Tests
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
# All tests (excluding integration)
|
| 32 |
+
uv run pytest
|
| 33 |
+
|
| 34 |
+
# With coverage
|
| 35 |
+
uv run pytest --cov
|
| 36 |
+
|
| 37 |
+
# Integration tests (requires Docker)
|
| 38 |
+
uv run pytest -m integration
|
| 39 |
+
|
| 40 |
+
# Slow tests (requires Docker + DeepISLES image)
|
| 41 |
+
uv run pytest -m "integration and slow"
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
## Code Quality
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
# Lint
|
| 48 |
+
uv run ruff check .
|
| 49 |
+
|
| 50 |
+
# Format
|
| 51 |
+
uv run ruff format .
|
| 52 |
+
|
| 53 |
+
# Type check
|
| 54 |
+
uv run mypy src/
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Project Structure
|
| 58 |
+
|
| 59 |
+
```
|
| 60 |
+
src/stroke_deepisles_demo/
|
| 61 |
+
├── core/ # Shared utilities (config, types, exceptions)
|
| 62 |
+
├── data/ # HF dataset loading and case management
|
| 63 |
+
├── inference/ # DeepISLES Docker integration
|
| 64 |
+
├── ui/ # Gradio application
|
| 65 |
+
├── pipeline.py # End-to-end orchestration
|
| 66 |
+
└── metrics.py # Evaluation metrics
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## Pull Request Process
|
| 70 |
+
|
| 71 |
+
1. Create a feature branch from `main`
|
| 72 |
+
2. Write tests for new functionality
|
| 73 |
+
3. Ensure all tests pass and code quality checks pass
|
| 74 |
+
4. Update documentation if needed
|
| 75 |
+
5. Submit PR with clear description
|
| 76 |
+
|
| 77 |
+
## Code Style
|
| 78 |
+
|
| 79 |
+
- Type hints on all functions
|
| 80 |
+
- Docstrings in Google style
|
| 81 |
+
- Keep functions focused and small
|
| 82 |
+
- Prefer explicit over implicit
|
|
@@ -1,2 +1,93 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stroke DeepISLES Demo
|
| 2 |
+
|
| 3 |
+
[](https://opensource.org/licenses/MIT)
|
| 4 |
+
[](https://www.python.org/downloads/release/python-3110/)
|
| 5 |
+
[](https://github.com/astral-sh/ruff)
|
| 6 |
+
[](http://mypy-lang.org/)
|
| 7 |
+
|
| 8 |
+
A demonstration pipeline and UI for ischemic stroke lesion segmentation using **DeepISLES** and **ISLES24-MR-Lite** data.
|
| 9 |
+
|
| 10 |
+
This project provides a complete end-to-end workflow:
|
| 11 |
+
1. **Data Loading**: Lazy-loading of NIfTI neuroimaging data from HuggingFace.
|
| 12 |
+
2. **Inference**: Running DeepISLES segmentation (SEALS or Ensemble) via Docker.
|
| 13 |
+
3. **Visualization**: Interactive 3D and multi-planar viewing with NiiVue in Gradio.
|
| 14 |
+
|
| 15 |
+
> **Disclaimer**: This software is for research and demonstration purposes only. It is not intended for clinical use.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- 🧠 **State-of-the-Art Segmentation**: Uses DeepISLES (ISLES'22 winner) for accurate lesion segmentation.
|
| 20 |
+
- ☁️ **Cloud-Native Data**: Streams data directly from HuggingFace Datasets (no massive downloads).
|
| 21 |
+
- 🐳 **Dockerized Inference**: Encapsulates complex deep learning dependencies in a reproducible container.
|
| 22 |
+
- 🖥️ **Interactive UI**: Gradio-based web interface with 3D rendering (NiiVue).
|
| 23 |
+
- ⚙️ **Production Ready**: Type-safe, tested, and configurable via environment variables.
|
| 24 |
+
|
| 25 |
+
## Quickstart
|
| 26 |
+
|
| 27 |
+
### Prerequisites
|
| 28 |
+
|
| 29 |
+
- Python 3.11+
|
| 30 |
+
- Docker (for inference)
|
| 31 |
+
- [uv](https://github.com/astral-sh/uv) (recommended) or pip
|
| 32 |
+
|
| 33 |
+
### Installation
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
# Clone the repository
|
| 37 |
+
git clone https://github.com/The-Obstacle-Is-The-Way/stroke-deepisles-demo.git
|
| 38 |
+
cd stroke-deepisles-demo
|
| 39 |
+
|
| 40 |
+
# Install dependencies
|
| 41 |
+
uv sync
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### Running the Demo
|
| 45 |
+
|
| 46 |
+
1. **Pull the Docker image** (first time only):
|
| 47 |
+
```bash
|
| 48 |
+
docker pull isleschallenge/deepisles
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
2. **Launch the UI**:
|
| 52 |
+
```bash
|
| 53 |
+
uv run python -m stroke_deepisles_demo.ui.app
|
| 54 |
+
```
|
| 55 |
+
Open [http://localhost:7860](http://localhost:7860) in your browser.
|
| 56 |
+
|
| 57 |
+
3. **Run via CLI**:
|
| 58 |
+
```bash
|
| 59 |
+
# List cases
|
| 60 |
+
uv run stroke-demo list
|
| 61 |
+
|
| 62 |
+
# Run segmentation on a specific case
|
| 63 |
+
uv run stroke-demo run --case sub-stroke0001
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Documentation
|
| 67 |
+
|
| 68 |
+
- [Quickstart Guide](docs/guides/quickstart.md)
|
| 69 |
+
- [Configuration](docs/guides/configuration.md)
|
| 70 |
+
- [Deployment](docs/guides/deployment.md)
|
| 71 |
+
- [Contributing](CONTRIBUTING.md)
|
| 72 |
+
|
| 73 |
+
## Architecture
|
| 74 |
+
|
| 75 |
+
```mermaid
|
| 76 |
+
graph TD
|
| 77 |
+
HF[HuggingFace Hub] -->|Stream NIfTI| Loader[Data Loader]
|
| 78 |
+
Loader -->|Stage Files| Staging[Staging Dir]
|
| 79 |
+
Staging -->|Mount Volume| Docker[DeepISLES Container]
|
| 80 |
+
Docker -->|Inference| Results[Prediction Mask]
|
| 81 |
+
Results -->|Load| Metrics[Metrics (Dice)]
|
| 82 |
+
Results -->|Render| UI[Gradio UI / NiiVue]
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## License
|
| 86 |
+
|
| 87 |
+
MIT License. See [LICENSE](LICENSE) for details.
|
| 88 |
+
|
| 89 |
+
## Acknowledgements
|
| 90 |
+
|
| 91 |
+
- [DeepISLES](https://github.com/ezequieldlrosa/DeepIsles) team for the segmentation model.
|
| 92 |
+
- [ISLES24](https://www.isles-challenge.org/) challenge for the dataset.
|
| 93 |
+
- [NiiVue](https://github.com/niivue/niivue) for the web-based neuroimaging viewer.
|
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration
|
| 2 |
+
|
| 3 |
+
All settings can be configured via environment variables.
|
| 4 |
+
|
| 5 |
+
## Environment Variables
|
| 6 |
+
|
| 7 |
+
| Variable | Default | Description |
|
| 8 |
+
|----------|---------|-------------|
|
| 9 |
+
| `STROKE_DEMO_LOG_LEVEL` | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR) |
|
| 10 |
+
| `STROKE_DEMO_LOG_FORMAT` | `simple` | Log format (simple, detailed, json) |
|
| 11 |
+
| `STROKE_DEMO_HF_DATASET_ID` | `YongchengYAO/ISLES24-MR-Lite` | HuggingFace dataset ID |
|
| 12 |
+
| `STROKE_DEMO_HF_CACHE_DIR` | `None` | Custom HF cache directory |
|
| 13 |
+
| `STROKE_DEMO_HF_TOKEN` | `None` | HuggingFace API token (for private datasets) |
|
| 14 |
+
| `STROKE_DEMO_DEEPISLES_DOCKER_IMAGE` | `isleschallenge/deepisles` | DeepISLES Docker image |
|
| 15 |
+
| `STROKE_DEMO_DEEPISLES_FAST_MODE` | `true` | Use single-model mode |
|
| 16 |
+
| `STROKE_DEMO_DEEPISLES_TIMEOUT_SECONDS` | `1800` | Inference timeout |
|
| 17 |
+
| `STROKE_DEMO_DEEPISLES_USE_GPU` | `true` | Use GPU acceleration |
|
| 18 |
+
| `STROKE_DEMO_TEMP_DIR` | `None` | Scratch directory for intermediate files |
|
| 19 |
+
| `STROKE_DEMO_RESULTS_DIR` | `./results` | Directory for output files |
|
| 20 |
+
| `STROKE_DEMO_GRADIO_SERVER_NAME` | `0.0.0.0` | Gradio server host |
|
| 21 |
+
| `STROKE_DEMO_GRADIO_SERVER_PORT` | `7860` | Gradio server port |
|
| 22 |
+
| `STROKE_DEMO_GRADIO_SHARE` | `false` | Create public Gradio link |
|
| 23 |
+
|
| 24 |
+
## Using .env File
|
| 25 |
+
|
| 26 |
+
Create a `.env` file in the project root:
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
STROKE_DEMO_LOG_LEVEL=DEBUG
|
| 30 |
+
STROKE_DEMO_DEEPISLES_USE_GPU=false
|
| 31 |
+
STROKE_DEMO_RESULTS_DIR=/data/results
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Programmatic Configuration
|
| 35 |
+
|
| 36 |
+
```python
|
| 37 |
+
from stroke_deepisles_demo.core.config import get_settings, reload_settings
|
| 38 |
+
import os
|
| 39 |
+
|
| 40 |
+
# Check current settings
|
| 41 |
+
print(get_settings().log_level)
|
| 42 |
+
|
| 43 |
+
# Override via environment
|
| 44 |
+
os.environ["STROKE_DEMO_LOG_LEVEL"] = "DEBUG"
|
| 45 |
+
reload_settings()
|
| 46 |
+
print(get_settings().log_level) # DEBUG
|
| 47 |
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deployment
|
| 2 |
+
|
| 3 |
+
The demo is designed to be deployed on Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## Hugging Face Spaces
|
| 6 |
+
|
| 7 |
+
1. **Create a Space**: Go to [huggingface.co/spaces](https://huggingface.co/spaces) and create a new Space.
|
| 8 |
+
* **SDK**: Docker (Recommended for custom dependencies) or Gradio
|
| 9 |
+
* **Hardware**: GPU is recommended for DeepISLES inference.
|
| 10 |
+
|
| 11 |
+
2. **Configure Dockerfile (if using Docker SDK)**:
|
| 12 |
+
Ensure the Dockerfile installs Python 3.11, uv, and pulls the DeepISLES image (or handles it appropriately, though Spaces might restrict running Docker-in-Docker).
|
| 13 |
+
|
| 14 |
+
*Note*: Since DeepISLES runs as a Docker container, running it inside a HF Space (which is a container) requires Docker-in-Docker (DinD) or a compatible runtime. If DinD is not supported, you might need to adapt the inference to run directly in the python environment if possible (DeepISLES source code integration instead of Docker wrapper), but this project wraps the Docker image.
|
| 15 |
+
|
| 16 |
+
**Standard Deployment (Gradio SDK)**:
|
| 17 |
+
The project includes `app.py` at the root for standard Gradio deployment. However, checking `requirements.txt` or `pyproject.toml` is needed.
|
| 18 |
+
|
| 19 |
+
For standard Gradio Spaces, you need to ensure `docker` command is available if you stick to the current architecture. Most HF Spaces do not support running `docker run`.
|
| 20 |
+
|
| 21 |
+
**Alternative**: Use a VM (AWS/GCP/Azure) with Docker installed.
|
| 22 |
+
|
| 23 |
+
## Local Deployment
|
| 24 |
+
|
| 25 |
+
1. **Build/Pull**:
|
| 26 |
+
```bash
|
| 27 |
+
docker pull isleschallenge/deepisles
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
2. **Run App**:
|
| 31 |
+
```bash
|
| 32 |
+
uv run python -m stroke_deepisles_demo.ui.app
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## Environment Variables
|
| 36 |
+
|
| 37 |
+
Configure the deployment using environment variables (Secrets in HF Spaces):
|
| 38 |
+
- `STROKE_DEMO_HF_TOKEN`: Read-only token for accessing datasets if private.
|
| 39 |
+
- `STROKE_DEMO_DEEPISLES_USE_GPU`: Set to `false` if deploying on CPU-only instance.
|
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quickstart
|
| 2 |
+
|
| 3 |
+
Get started with stroke-deepisles-demo in 5 minutes.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
- Python 3.11+
|
| 8 |
+
- Docker (for DeepISLES inference)
|
| 9 |
+
- ~10GB disk space (for Docker image and datasets)
|
| 10 |
+
|
| 11 |
+
## Installation
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
# Clone
|
| 15 |
+
git clone https://github.com/The-Obstacle-Is-The-Way/stroke-deepisles-demo.git
|
| 16 |
+
cd stroke-deepisles-demo
|
| 17 |
+
|
| 18 |
+
# Install
|
| 19 |
+
uv sync
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Pull DeepISLES Docker Image
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
docker pull isleschallenge/deepisles
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Run Locally
|
| 29 |
+
|
| 30 |
+
### Option 1: Gradio UI
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
uv run python -m stroke_deepisles_demo.ui.app
|
| 34 |
+
# Open http://localhost:7860
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### Option 2: CLI
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
# List available cases
|
| 41 |
+
uv run stroke-demo list
|
| 42 |
+
|
| 43 |
+
# Run on a specific case
|
| 44 |
+
uv run stroke-demo run --case sub-stroke0001 --fast
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Option 3: Python API
|
| 48 |
+
|
| 49 |
+
```python
|
| 50 |
+
from stroke_deepisles_demo.pipeline import run_pipeline_on_case
|
| 51 |
+
|
| 52 |
+
result = run_pipeline_on_case("sub-stroke0001", fast=True)
|
| 53 |
+
print(f"Dice score: {result.dice_score:.3f}")
|
| 54 |
+
print(f"Prediction: {result.prediction_mask}")
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Configuration
|
| 58 |
+
|
| 59 |
+
Set environment variables or create a `.env` file:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
# .env
|
| 63 |
+
STROKE_DEMO_LOG_LEVEL=DEBUG
|
| 64 |
+
STROKE_DEMO_DEEPISLES_USE_GPU=false # If no GPU available
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
See [Configuration Guide](configuration.md) for all options.
|
|
@@ -2,28 +2,76 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 6 |
|
| 7 |
|
| 8 |
class Settings(BaseSettings):
|
| 9 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
model_config = SettingsConfigDict(
|
| 12 |
env_prefix="STROKE_DEMO_",
|
| 13 |
env_file=".env",
|
| 14 |
env_file_encoding="utf-8",
|
|
|
|
| 15 |
)
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# HuggingFace
|
| 18 |
hf_dataset_id: str = "YongchengYAO/ISLES24-MR-Lite"
|
| 19 |
-
hf_cache_dir:
|
|
|
|
| 20 |
|
| 21 |
# DeepISLES
|
| 22 |
deepisles_docker_image: str = "isleschallenge/deepisles"
|
| 23 |
-
deepisles_fast_mode: bool = True
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# Paths
|
| 26 |
-
temp_dir:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
|
|
|
|
|
|
| 29 |
settings = Settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Literal
|
| 7 |
+
|
| 8 |
+
from pydantic import Field, field_validator
|
| 9 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 10 |
|
| 11 |
|
| 12 |
class Settings(BaseSettings):
|
| 13 |
+
"""
|
| 14 |
+
Application settings loaded from environment variables.
|
| 15 |
+
|
| 16 |
+
All settings can be overridden via environment variables with
|
| 17 |
+
the STROKE_DEMO_ prefix.
|
| 18 |
+
|
| 19 |
+
Example:
|
| 20 |
+
export STROKE_DEMO_LOG_LEVEL=DEBUG
|
| 21 |
+
export STROKE_DEMO_HF_DATASET_ID=my/dataset
|
| 22 |
+
"""
|
| 23 |
|
| 24 |
model_config = SettingsConfigDict(
|
| 25 |
env_prefix="STROKE_DEMO_",
|
| 26 |
env_file=".env",
|
| 27 |
env_file_encoding="utf-8",
|
| 28 |
+
extra="ignore",
|
| 29 |
)
|
| 30 |
|
| 31 |
+
# Logging
|
| 32 |
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
| 33 |
+
log_format: Literal["simple", "detailed", "json"] = "simple"
|
| 34 |
+
|
| 35 |
# HuggingFace
|
| 36 |
hf_dataset_id: str = "YongchengYAO/ISLES24-MR-Lite"
|
| 37 |
+
hf_cache_dir: Path | None = None
|
| 38 |
+
hf_token: str | None = Field(default=None, repr=False) # Hidden from logs
|
| 39 |
|
| 40 |
# DeepISLES
|
| 41 |
deepisles_docker_image: str = "isleschallenge/deepisles"
|
| 42 |
+
deepisles_fast_mode: bool = True # SEALS-only (ISLES'22 winner, no FLAIR needed)
|
| 43 |
+
deepisles_timeout_seconds: int = 1800 # 30 minutes
|
| 44 |
+
deepisles_use_gpu: bool = True
|
| 45 |
|
| 46 |
# Paths
|
| 47 |
+
temp_dir: Path | None = None
|
| 48 |
+
results_dir: Path = Path("./results")
|
| 49 |
+
|
| 50 |
+
# UI
|
| 51 |
+
gradio_server_name: str = "0.0.0.0"
|
| 52 |
+
gradio_server_port: int = 7860
|
| 53 |
+
gradio_share: bool = False
|
| 54 |
|
| 55 |
+
@field_validator("results_dir", mode="before")
|
| 56 |
+
@classmethod
|
| 57 |
+
def ensure_results_dir_exists(cls, v: Path | str) -> Path:
|
| 58 |
+
"""Create results directory if it doesn't exist."""
|
| 59 |
+
path = Path(v)
|
| 60 |
+
path.mkdir(parents=True, exist_ok=True)
|
| 61 |
+
return path
|
| 62 |
|
| 63 |
+
|
| 64 |
+
# Global settings instance
|
| 65 |
settings = Settings()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def get_settings() -> Settings:
|
| 69 |
+
"""Get the current settings instance."""
|
| 70 |
+
return settings
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def reload_settings() -> Settings:
|
| 74 |
+
"""Reload settings from environment (useful for testing)."""
|
| 75 |
+
global settings
|
| 76 |
+
settings = Settings()
|
| 77 |
+
return settings
|
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Centralized logging configuration."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import sys
|
| 7 |
+
from typing import Literal
|
| 8 |
+
|
| 9 |
+
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def setup_logging(
|
| 13 |
+
level: LogLevel = "INFO",
|
| 14 |
+
*,
|
| 15 |
+
format_style: Literal["simple", "detailed", "json"] = "simple",
|
| 16 |
+
) -> None:
|
| 17 |
+
"""
|
| 18 |
+
Configure logging for the application.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
level: Minimum log level
|
| 22 |
+
format_style: Output format style
|
| 23 |
+
|
| 24 |
+
Example:
|
| 25 |
+
>>> setup_logging("DEBUG", format_style="detailed")
|
| 26 |
+
"""
|
| 27 |
+
formats = {
|
| 28 |
+
"simple": "%(levelname)s: %(message)s",
|
| 29 |
+
"detailed": "%(asctime)s | %(name)s | %(levelname)s | %(message)s",
|
| 30 |
+
"json": '{"time": "%(asctime)s", "name": "%(name)s", "level": "%(levelname)s", "message": "%(message)s"}',
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
logging.basicConfig(
|
| 34 |
+
level=getattr(logging, level),
|
| 35 |
+
format=formats[format_style],
|
| 36 |
+
stream=sys.stderr,
|
| 37 |
+
force=True,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Reduce noise from libraries
|
| 41 |
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
| 42 |
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 43 |
+
logging.getLogger("datasets").setLevel(logging.WARNING)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def get_logger(name: str) -> logging.Logger:
|
| 47 |
+
"""
|
| 48 |
+
Get a logger for a module.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
name: Logger name (typically __name__)
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
Configured logger instance
|
| 55 |
+
"""
|
| 56 |
+
# Avoid double-prefixing when __name__ already includes package name
|
| 57 |
+
if name.startswith("stroke_deepisles_demo."):
|
| 58 |
+
return logging.getLogger(name)
|
| 59 |
+
return logging.getLogger(f"stroke_deepisles_demo.{name}")
|
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
| 7 |
from typing import TYPE_CHECKING
|
| 8 |
|
| 9 |
from stroke_deepisles_demo.core.exceptions import DeepISLESError, MissingInputError
|
|
|
|
| 10 |
from stroke_deepisles_demo.inference.docker import (
|
| 11 |
DockerRunResult,
|
| 12 |
ensure_gpu_available_if_requested,
|
|
@@ -16,6 +17,8 @@ from stroke_deepisles_demo.inference.docker import (
|
|
| 16 |
if TYPE_CHECKING:
|
| 17 |
from pathlib import Path
|
| 18 |
|
|
|
|
|
|
|
| 19 |
# Constants
|
| 20 |
DEEPISLES_IMAGE = "isleschallenge/deepisles"
|
| 21 |
EXPECTED_INPUT_FILES = ["dwi.nii.gz", "adc.nii.gz"]
|
|
|
|
| 7 |
from typing import TYPE_CHECKING
|
| 8 |
|
| 9 |
from stroke_deepisles_demo.core.exceptions import DeepISLESError, MissingInputError
|
| 10 |
+
from stroke_deepisles_demo.core.logging import get_logger
|
| 11 |
from stroke_deepisles_demo.inference.docker import (
|
| 12 |
DockerRunResult,
|
| 13 |
ensure_gpu_available_if_requested,
|
|
|
|
| 17 |
if TYPE_CHECKING:
|
| 18 |
from pathlib import Path
|
| 19 |
|
| 20 |
+
logger = get_logger(__name__)
|
| 21 |
+
|
| 22 |
# Constants
|
| 23 |
DEEPISLES_IMAGE = "isleschallenge/deepisles"
|
| 24 |
EXPECTED_INPUT_FILES = ["dwi.nii.gz", "adc.nii.gz"]
|
|
@@ -12,11 +12,14 @@ from stroke_deepisles_demo.core.exceptions import (
|
|
| 12 |
DockerGPUNotAvailableError,
|
| 13 |
DockerNotAvailableError,
|
| 14 |
)
|
|
|
|
| 15 |
|
| 16 |
if TYPE_CHECKING:
|
| 17 |
from collections.abc import Sequence
|
| 18 |
from pathlib import Path
|
| 19 |
|
|
|
|
|
|
|
| 20 |
|
| 21 |
@dataclass(frozen=True)
|
| 22 |
class DockerRunResult:
|
|
@@ -123,15 +126,18 @@ def pull_image_if_missing(image: str, *, timeout: float = 600) -> bool:
|
|
| 123 |
check=False,
|
| 124 |
)
|
| 125 |
if result.returncode == 0:
|
|
|
|
| 126 |
return False # Image already present
|
| 127 |
|
| 128 |
# Pull the image
|
|
|
|
| 129 |
subprocess.run(
|
| 130 |
["docker", "pull", image],
|
| 131 |
capture_output=True,
|
| 132 |
timeout=timeout,
|
| 133 |
check=True,
|
| 134 |
)
|
|
|
|
| 135 |
return True
|
| 136 |
|
| 137 |
|
|
@@ -241,6 +247,19 @@ def run_container(
|
|
| 241 |
)
|
| 242 |
|
| 243 |
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
result = subprocess.run(
|
| 245 |
cmd,
|
| 246 |
capture_output=True,
|
|
@@ -250,6 +269,13 @@ def run_container(
|
|
| 250 |
)
|
| 251 |
elapsed = time.time() - start_time
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
return DockerRunResult(
|
| 254 |
exit_code=result.returncode,
|
| 255 |
stdout=result.stdout,
|
|
|
|
| 12 |
DockerGPUNotAvailableError,
|
| 13 |
DockerNotAvailableError,
|
| 14 |
)
|
| 15 |
+
from stroke_deepisles_demo.core.logging import get_logger
|
| 16 |
|
| 17 |
if TYPE_CHECKING:
|
| 18 |
from collections.abc import Sequence
|
| 19 |
from pathlib import Path
|
| 20 |
|
| 21 |
+
logger = get_logger(__name__)
|
| 22 |
+
|
| 23 |
|
| 24 |
@dataclass(frozen=True)
|
| 25 |
class DockerRunResult:
|
|
|
|
| 126 |
check=False,
|
| 127 |
)
|
| 128 |
if result.returncode == 0:
|
| 129 |
+
logger.debug("Docker image %s already present", image)
|
| 130 |
return False # Image already present
|
| 131 |
|
| 132 |
# Pull the image
|
| 133 |
+
logger.info("Pulling Docker image %s (this may take a while)", image)
|
| 134 |
subprocess.run(
|
| 135 |
["docker", "pull", image],
|
| 136 |
capture_output=True,
|
| 137 |
timeout=timeout,
|
| 138 |
check=True,
|
| 139 |
)
|
| 140 |
+
logger.info("Successfully pulled Docker image %s", image)
|
| 141 |
return True
|
| 142 |
|
| 143 |
|
|
|
|
| 247 |
)
|
| 248 |
|
| 249 |
start_time = time.time()
|
| 250 |
+
# Redact environment variable values to avoid leaking secrets in logs
|
| 251 |
+
redacted_cmd: list[str] = []
|
| 252 |
+
skip_next = False
|
| 253 |
+
for arg in cmd:
|
| 254 |
+
if skip_next:
|
| 255 |
+
redacted_cmd.append("***")
|
| 256 |
+
skip_next = False
|
| 257 |
+
elif arg == "-e":
|
| 258 |
+
redacted_cmd.append(arg)
|
| 259 |
+
skip_next = True
|
| 260 |
+
else:
|
| 261 |
+
redacted_cmd.append(arg)
|
| 262 |
+
logger.debug("Running container: %s", " ".join(redacted_cmd))
|
| 263 |
result = subprocess.run(
|
| 264 |
cmd,
|
| 265 |
capture_output=True,
|
|
|
|
| 269 |
)
|
| 270 |
elapsed = time.time() - start_time
|
| 271 |
|
| 272 |
+
if result.returncode != 0:
|
| 273 |
+
logger.error(
|
| 274 |
+
"Container execution failed (code %d). stderr: %s", result.returncode, result.stderr
|
| 275 |
+
)
|
| 276 |
+
else:
|
| 277 |
+
logger.info("Container execution completed in %.2fs", elapsed)
|
| 278 |
+
|
| 279 |
return DockerRunResult(
|
| 280 |
exit_code=result.returncode,
|
| 281 |
stdout=result.stdout,
|
|
@@ -2,7 +2,6 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import logging
|
| 6 |
import shutil
|
| 7 |
import statistics
|
| 8 |
import tempfile
|
|
@@ -12,6 +11,7 @@ from pathlib import Path
|
|
| 12 |
from typing import TYPE_CHECKING
|
| 13 |
|
| 14 |
from stroke_deepisles_demo import metrics
|
|
|
|
| 15 |
from stroke_deepisles_demo.data import load_isles_dataset, stage_case_for_deepisles
|
| 16 |
from stroke_deepisles_demo.inference import run_deepisles_on_folder
|
| 17 |
|
|
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|
| 20 |
|
| 21 |
from stroke_deepisles_demo.core.types import CaseFiles
|
| 22 |
|
| 23 |
-
logger =
|
| 24 |
|
| 25 |
|
| 26 |
@dataclass(frozen=True)
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import shutil
|
| 6 |
import statistics
|
| 7 |
import tempfile
|
|
|
|
| 11 |
from typing import TYPE_CHECKING
|
| 12 |
|
| 13 |
from stroke_deepisles_demo import metrics
|
| 14 |
+
from stroke_deepisles_demo.core.logging import get_logger
|
| 15 |
from stroke_deepisles_demo.data import load_isles_dataset, stage_case_for_deepisles
|
| 16 |
from stroke_deepisles_demo.inference import run_deepisles_on_folder
|
| 17 |
|
|
|
|
| 20 |
|
| 21 |
from stroke_deepisles_demo.core.types import CaseFiles
|
| 22 |
|
| 23 |
+
logger = get_logger(__name__)
|
| 24 |
|
| 25 |
|
| 26 |
@dataclass(frozen=True)
|
|
@@ -2,12 +2,12 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import logging
|
| 6 |
from typing import Any
|
| 7 |
|
| 8 |
import gradio as gr
|
| 9 |
-
from matplotlib.figure import Figure # noqa: TC002
|
| 10 |
|
|
|
|
| 11 |
from stroke_deepisles_demo.pipeline import run_pipeline_on_case
|
| 12 |
from stroke_deepisles_demo.ui.components import (
|
| 13 |
create_case_selector,
|
|
@@ -20,7 +20,7 @@ from stroke_deepisles_demo.ui.viewer import (
|
|
| 20 |
render_slice_comparison,
|
| 21 |
)
|
| 22 |
|
| 23 |
-
logger =
|
| 24 |
|
| 25 |
|
| 26 |
def run_segmentation(
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
import gradio as gr
|
| 8 |
+
from matplotlib.figure import Figure # noqa: TC002
|
| 9 |
|
| 10 |
+
from stroke_deepisles_demo.core.logging import get_logger
|
| 11 |
from stroke_deepisles_demo.pipeline import run_pipeline_on_case
|
| 12 |
from stroke_deepisles_demo.ui.components import (
|
| 13 |
create_case_selector,
|
|
|
|
| 20 |
render_slice_comparison,
|
| 21 |
)
|
| 22 |
|
| 23 |
+
logger = get_logger(__name__)
|
| 24 |
|
| 25 |
|
| 26 |
def run_segmentation(
|
|
@@ -2,13 +2,12 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import logging
|
| 6 |
-
|
| 7 |
import gradio as gr
|
| 8 |
|
|
|
|
| 9 |
from stroke_deepisles_demo.data import list_case_ids
|
| 10 |
|
| 11 |
-
logger =
|
| 12 |
|
| 13 |
|
| 14 |
def create_case_selector() -> gr.Dropdown:
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
import gradio as gr
|
| 6 |
|
| 7 |
+
from stroke_deepisles_demo.core.logging import get_logger
|
| 8 |
from stroke_deepisles_demo.data import list_case_ids
|
| 9 |
|
| 10 |
+
logger = get_logger(__name__)
|
| 11 |
|
| 12 |
|
| 13 |
def create_case_selector() -> gr.Dropdown:
|
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for configuration."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import TYPE_CHECKING
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
from stroke_deepisles_demo.core.config import Settings, reload_settings
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TestSettings:
|
| 15 |
+
"""Tests for Settings."""
|
| 16 |
+
|
| 17 |
+
def test_default_values(self) -> None:
|
| 18 |
+
"""Has sensible defaults."""
|
| 19 |
+
settings = Settings()
|
| 20 |
+
assert settings.log_level == "INFO"
|
| 21 |
+
assert settings.hf_dataset_id == "YongchengYAO/ISLES24-MR-Lite"
|
| 22 |
+
assert settings.deepisles_timeout_seconds == 1800
|
| 23 |
+
assert settings.results_dir == Path("./results")
|
| 24 |
+
|
| 25 |
+
def test_env_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
| 26 |
+
"""Environment variables override defaults."""
|
| 27 |
+
monkeypatch.setenv("STROKE_DEMO_LOG_LEVEL", "DEBUG")
|
| 28 |
+
monkeypatch.setenv("STROKE_DEMO_DEEPISLES_TIMEOUT_SECONDS", "60")
|
| 29 |
+
|
| 30 |
+
settings = Settings()
|
| 31 |
+
assert settings.log_level == "DEBUG"
|
| 32 |
+
assert settings.deepisles_timeout_seconds == 60
|
| 33 |
+
|
| 34 |
+
def test_hf_token_hidden_from_repr(self) -> None:
|
| 35 |
+
"""HF token is not visible in repr."""
|
| 36 |
+
settings = Settings(hf_token="secret123")
|
| 37 |
+
assert "secret123" not in repr(settings)
|
| 38 |
+
|
| 39 |
+
def test_results_dir_created(self, tmp_path: Path) -> None:
|
| 40 |
+
"""Results directory is created if it doesn't exist."""
|
| 41 |
+
# This test relies on the validator running during instantiation
|
| 42 |
+
new_dir = tmp_path / "new_results"
|
| 43 |
+
assert not new_dir.exists()
|
| 44 |
+
|
| 45 |
+
Settings(results_dir=new_dir)
|
| 46 |
+
assert new_dir.exists()
|
| 47 |
+
|
| 48 |
+
def test_reload_settings(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
| 49 |
+
"""Test that reload_settings updates the global instance."""
|
| 50 |
+
from stroke_deepisles_demo.core import config
|
| 51 |
+
|
| 52 |
+
# Set env var
|
| 53 |
+
monkeypatch.setenv("STROKE_DEMO_LOG_LEVEL", "ERROR")
|
| 54 |
+
|
| 55 |
+
# Reload
|
| 56 |
+
new_settings = reload_settings()
|
| 57 |
+
|
| 58 |
+
assert new_settings.log_level == "ERROR"
|
| 59 |
+
assert config.settings.log_level == "ERROR"
|
| 60 |
+
# Ensure it's the same object instance reference in the module
|
| 61 |
+
assert config.settings is new_settings
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for logging configuration."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
from stroke_deepisles_demo.core.logging import get_logger, setup_logging
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TestSetupLogging:
|
| 11 |
+
"""Tests for setup_logging."""
|
| 12 |
+
|
| 13 |
+
def test_sets_log_level(self) -> None:
|
| 14 |
+
"""Sets the root logger level."""
|
| 15 |
+
# Reset root logger handlers to avoid interference
|
| 16 |
+
logging.getLogger().handlers = []
|
| 17 |
+
|
| 18 |
+
setup_logging("DEBUG")
|
| 19 |
+
# Note: basicConfig might not reset if already configured unless force=True is used
|
| 20 |
+
# The implementation should use force=True
|
| 21 |
+
assert logging.getLogger().level == logging.DEBUG
|
| 22 |
+
|
| 23 |
+
def test_format_styles(self) -> None:
|
| 24 |
+
"""Different format styles work."""
|
| 25 |
+
for style in ["simple", "detailed", "json"]:
|
| 26 |
+
# Reset handlers
|
| 27 |
+
logging.getLogger().handlers = []
|
| 28 |
+
setup_logging("INFO", format_style=style) # type: ignore
|
| 29 |
+
# Should not raise
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class TestGetLogger:
|
| 33 |
+
"""Tests for get_logger."""
|
| 34 |
+
|
| 35 |
+
def test_returns_namespaced_logger(self) -> None:
|
| 36 |
+
"""Returns logger with stroke_demo prefix."""
|
| 37 |
+
logger = get_logger("my_module")
|
| 38 |
+
assert logger.name == "stroke_deepisles_demo.my_module"
|