File size: 4,322 Bytes
4eeba46
 
 
 
a544a50
bfe80c5
 
 
a544a50
4eeba46
 
 
a544a50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eeba46
bfe80c5
 
 
 
 
 
 
 
 
 
4eeba46
 
 
 
 
bfe80c5
4eeba46
 
bfe80c5
 
 
 
4eeba46
 
bfe80c5
 
4eeba46
 
 
bfe80c5
 
 
a544a50
 
4eeba46
 
bfe80c5
 
 
 
 
 
 
4eeba46
a544a50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfe80c5
 
 
 
 
 
 
4eeba46
bfe80c5
 
4eeba46
bfe80c5
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""Application configuration using pydantic-settings."""

from __future__ import annotations

import os
from pathlib import Path
from typing import Literal

from pydantic import Field, computed_field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


def is_running_in_hf_spaces() -> bool:
    """
    Detect if running inside Hugging Face Spaces environment.

    Returns:
        True if running in HF Spaces, False otherwise

    Detection methods (all env-var based for reliability):
        1. HF_SPACES=1 env var (set by our Dockerfile)
        2. SPACE_ID env var (set by HF Spaces runtime)

    Note:
        We intentionally avoid path-based detection (like checking for
        /home/user or /app) because these paths exist on many Linux
        systems and would cause false positives.
    """
    # Check explicit env vars only - no path-based fallbacks
    if os.environ.get("HF_SPACES") == "1":
        return True
    # SPACE_ID is set by HF Spaces runtime
    return bool(os.environ.get("SPACE_ID"))


def is_deepisles_direct_available() -> bool:
    """
    Check if DeepISLES can be invoked directly (without Docker).

    Returns:
        True if DEEPISLES_DIRECT_INVOCATION env var is set

    This check is intentionally simple and side-effect free.
    The env var is set by our Dockerfile when running on HF Spaces.
    Actual module path setup happens in inference/direct.py when invoked.

    Note:
        We don't attempt import-based detection here because it would
        require modifying sys.path, which is a side effect inappropriate
        for a simple availability check.
    """
    return os.environ.get("DEEPISLES_DIRECT_INVOCATION") == "1"


class Settings(BaseSettings):
    """
    Application settings loaded from environment variables.

    All settings can be overridden via environment variables with
    the STROKE_DEMO_ prefix.

    Example:
        export STROKE_DEMO_LOG_LEVEL=DEBUG
        export STROKE_DEMO_HF_DATASET_ID=my/dataset
    """

    model_config = SettingsConfigDict(
        env_prefix="STROKE_DEMO_",
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    # Logging
    log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
    log_format: Literal["simple", "detailed", "json"] = "simple"

    # HuggingFace
    hf_dataset_id: str = "YongchengYAO/ISLES24-MR-Lite"
    hf_cache_dir: Path | None = None
    hf_token: str | None = Field(default=None, repr=False)  # Hidden from logs

    # DeepISLES
    deepisles_docker_image: str = "isleschallenge/deepisles"
    deepisles_fast_mode: bool = True  # SEALS-only (ISLES'22 winner, no FLAIR needed)
    deepisles_timeout_seconds: int = 1800  # 30 minutes
    deepisles_use_gpu: bool = True
    # Path to DeepISLES repo (for direct invocation mode)
    deepisles_repo_path: Path | None = None

    # Paths
    temp_dir: Path | None = None
    results_dir: Path = Path("./results")

    # UI
    gradio_server_name: str = "0.0.0.0"
    gradio_server_port: int = 7860
    gradio_share: bool = False

    @computed_field  # type: ignore[prop-decorator]
    @property
    def is_hf_spaces(self) -> bool:
        """Check if running in HF Spaces environment."""
        return is_running_in_hf_spaces()

    @computed_field  # type: ignore[prop-decorator]
    @property
    def use_direct_invocation(self) -> bool:
        """
        Check if should use direct DeepISLES invocation (vs Docker).

        Direct invocation is used when:
        1. Running in HF Spaces (cannot run Docker-in-Docker)
        2. DeepISLES modules are available for import
        """
        return self.is_hf_spaces or is_deepisles_direct_available()

    @field_validator("results_dir", mode="before")
    @classmethod
    def ensure_results_dir_exists(cls, v: Path | str) -> Path:
        """Create results directory if it doesn't exist."""
        path = Path(v)
        path.mkdir(parents=True, exist_ok=True)
        return path


# Global settings instance
settings = Settings()


def get_settings() -> Settings:
    """Get the current settings instance."""
    return settings


def reload_settings() -> Settings:
    """Reload settings from environment (useful for testing)."""
    global settings
    settings = Settings()
    return settings