File size: 5,064 Bytes
4eeba46
 
 
 
a544a50
bfe80c5
 
 
a544a50
4eeba46
 
 
a544a50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eeba46
bfe80c5
 
 
 
 
 
 
 
 
 
4eeba46
 
 
 
 
bfe80c5
4eeba46
 
bfe80c5
 
 
 
4eeba46
8290bc9
a2223b1
bfe80c5
4eeba46
 
 
bfe80c5
 
 
4eeba46
 
8290bc9
ba32591
 
 
 
8290bc9
 
 
ba32591
 
 
 
 
 
 
bfe80c5
 
 
 
 
785d976
 
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
142
143
144
145
146
147
148
149
150
151
152
153
154
"""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
    # Note: To control HF cache location, use HF_HOME env var (set in Dockerfile)
    hf_dataset_id: str = "hugging-science/isles24-stroke"
    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

    # Paths
    # Note: To control temp location, use TMPDIR env var (Python tempfile respects it)
    # Results directory - MUST be /tmp for HF Spaces (only /tmp is writable)
    results_dir: Path = Path("/tmp/stroke-results")

    # API Settings
    # Concurrency control - default to 1 for single-GPU safety (T4 has 16GB VRAM)
    # Increase via STROKE_DEMO_MAX_CONCURRENT_JOBS if you have multiple GPUs
    max_concurrent_jobs: int = 1

    # CORS - frontend origins allowed to call this API
    frontend_origins: list[str] = Field(default=["http://localhost:5173", "http://localhost:3000"])

    # Public URL for constructing absolute file URLs in responses
    # If not set, uses request.base_url (works for local dev)
    backend_public_url: str | None = None

    # UI
    gradio_server_name: str = "0.0.0.0"
    gradio_server_port: int = 7860
    gradio_share: bool = False
    # Show full Python tracebacks in Gradio UI (security: disable in production)
    gradio_show_error: 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