VibecoderMcSwaggins's picture
feat(hf-spaces): complete deployment infrastructure for Hugging Face Spaces (#8)
a544a50 unverified
raw
history blame
6.22 kB
"""Direct DeepISLES invocation without Docker.
This module provides direct Python invocation of DeepISLES when running
inside the DeepISLES Docker image (e.g., on HF Spaces). This avoids
Docker-in-Docker which is not supported on HF Spaces.
Usage:
When running in HF Spaces, our container is based on isleschallenge/deepisles,
which has all DeepISLES dependencies pre-installed. This module imports
and calls DeepISLES directly.
See:
- https://github.com/ezequieldlrosa/DeepIsles
- docs/specs/07-hf-spaces-deployment.md
"""
from __future__ import annotations
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from stroke_deepisles_demo.core.exceptions import DeepISLESError, MissingInputError
from stroke_deepisles_demo.core.logging import get_logger
from stroke_deepisles_demo.inference.deepisles import find_prediction_mask
logger = get_logger(__name__)
# Paths where DeepISLES source might be located in the Docker image
DEEPISLES_SEARCH_PATHS = [
"/app",
"/DeepIsles",
"/opt/deepisles",
"/home/user/DeepIsles",
]
@dataclass(frozen=True)
class DirectInvocationResult:
"""Result of direct DeepISLES invocation."""
prediction_path: Path
elapsed_seconds: float
def _ensure_deepisles_importable() -> str:
"""
Ensure DeepISLES modules are importable by adding to sys.path.
Returns:
Path where DeepISLES was found
Raises:
DeepISLESError: If DeepISLES cannot be found
"""
for path in DEEPISLES_SEARCH_PATHS:
if Path(path).exists():
if path not in sys.path:
sys.path.insert(0, path)
try:
# Test import (only available in DeepISLES Docker image)
from src.isles22_ensemble import IslesEnsemble # noqa: F401
logger.debug("Found DeepISLES at %s", path)
return path
except ImportError:
continue
raise DeepISLESError(
"DeepISLES modules not found. Direct invocation requires running "
"inside the DeepISLES Docker image. Searched paths: "
f"{DEEPISLES_SEARCH_PATHS}"
)
def validate_input_files(
dwi_path: Path,
adc_path: Path,
flair_path: Path | None = None,
) -> None:
"""
Validate that input files exist.
Args:
dwi_path: Path to DWI NIfTI file
adc_path: Path to ADC NIfTI file
flair_path: Optional path to FLAIR NIfTI file
Raises:
MissingInputError: If required files are missing
"""
if not dwi_path.exists():
raise MissingInputError(f"DWI file not found: {dwi_path}")
if not adc_path.exists():
raise MissingInputError(f"ADC file not found: {adc_path}")
if flair_path is not None and not flair_path.exists():
raise MissingInputError(f"FLAIR file not found: {flair_path}")
def run_deepisles_direct(
dwi_path: Path,
adc_path: Path,
output_dir: Path,
*,
flair_path: Path | None = None,
fast: bool = True,
skull_strip: bool = False,
parallelize: bool = True,
save_team_outputs: bool = False,
results_mni: bool = False,
) -> DirectInvocationResult:
"""
Run DeepISLES segmentation via direct Python invocation.
This function calls the DeepISLES IslesEnsemble.predict_ensemble() method
directly, bypassing Docker. It's used when running inside the DeepISLES
container on HF Spaces.
Args:
dwi_path: Path to DWI NIfTI file (b=1000)
adc_path: Path to ADC NIfTI file
output_dir: Directory for output files
flair_path: Optional path to FLAIR NIfTI file
fast: If True, use SEALS model only (faster, no FLAIR needed)
skull_strip: If True, perform skull stripping
parallelize: If True, run models in parallel
save_team_outputs: If True, save individual team outputs
results_mni: If True, output results in MNI space
Returns:
DirectInvocationResult with path to prediction mask
Raises:
DeepISLESError: If invocation fails
MissingInputError: If required input files are missing
Example:
>>> result = run_deepisles_direct(
... dwi_path=Path("/data/dwi.nii.gz"),
... adc_path=Path("/data/adc.nii.gz"),
... output_dir=Path("/data/output"),
... fast=True
... )
>>> print(result.prediction_path)
"""
start_time = time.time()
# Validate inputs
validate_input_files(dwi_path, adc_path, flair_path)
# Ensure DeepISLES is importable
deepisles_path = _ensure_deepisles_importable()
# Import DeepISLES (only available in DeepISLES Docker image)
try:
from src.isles22_ensemble import IslesEnsemble
except ImportError as e:
raise DeepISLESError(f"Failed to import DeepISLES: {e}") from e
# Create output directory
output_dir.mkdir(parents=True, exist_ok=True)
logger.info(
"Running DeepISLES direct invocation: dwi=%s, adc=%s, flair=%s, fast=%s",
dwi_path,
adc_path,
flair_path,
fast,
)
try:
# Initialize the ensemble
stroke_segm = IslesEnsemble()
# Run prediction
stroke_segm.predict_ensemble(
ensemble_path=deepisles_path,
input_dwi_path=str(dwi_path),
input_adc_path=str(adc_path),
input_flair_path=str(flair_path) if flair_path else None,
output_path=str(output_dir),
skull_strip=skull_strip,
fast=fast,
save_team_outputs=save_team_outputs,
results_mni=results_mni,
parallelize=parallelize,
)
except Exception as e:
logger.exception("DeepISLES inference failed")
raise DeepISLESError(f"DeepISLES inference failed: {e}") from e
# Find the prediction mask (using shared function from deepisles module)
prediction_path = find_prediction_mask(output_dir)
elapsed = time.time() - start_time
logger.info("DeepISLES direct invocation completed in %.2fs", elapsed)
return DirectInvocationResult(
prediction_path=prediction_path,
elapsed_seconds=elapsed,
)