Files
mule-image/backend/app/config.py
dtoro bbb8e4850c feat: GPU acceleration support for ONNX Runtime inference
Centralize execution provider selection in providers.py with
auto-detection and graceful fallback. All ONNX sessions (embedder,
detector, face processor, recognizer) now use the configured providers.

- New VISION_EXECUTION_PROVIDERS env var: "auto" for GPU auto-detect,
  or explicit "CUDAExecutionProvider,CPUExecutionProvider"
- Provider priority: CUDA > ROCm > OpenVINO > CPU (when set to "auto")
- docker-compose.yml includes commented-out NVIDIA GPU deploy section
- Supports onnxruntime-gpu as a drop-in replacement for onnxruntime

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:12:21 +02:00

213 lines
7.2 KiB
Python

"""
Application configuration using Pydantic Settings
"""
from pydantic_settings import BaseSettings
from pydantic import BaseModel, Field
from typing import Optional
import yaml
from pathlib import Path
class ThumbnailSettings(BaseModel):
"""Thumbnail generation settings"""
small: int = 240
medium: int = 640
large: int = 1280
quality: int = 85
format: str = "webp"
class ScannerSettings(BaseModel):
"""File scanner settings"""
watch: bool = True
initial_scan_on_start: bool = True
batch_size: int = 100
concurrent_workers: int = 4
class PerformanceSettings(BaseModel):
"""Performance tuning settings"""
max_concurrent_thumbnails: int = 10
cache_ttl: int = 3600
db_pool_size: int = 5
db_pool_max_overflow: int = 5
db_pool_recycle: int = 3600
class EmbedderSettings(BaseModel):
"""CLIP / SigLIP embedding model settings.
Supported: "openclip_vitb32" (512-d), "siglip2_vitb16" (768-d, default)."""
name: str = "siglip2_vitb16"
batch_size: int = 8
class OCRSettings(BaseModel):
"""PaddleOCR / rapidocr settings"""
enabled: bool = True
languages: list[str] = ["en"]
min_confidence: float = 0.5
class DetectorSettings(BaseModel):
"""YOLOv8n object detection settings"""
enabled: bool = True
min_confidence: float = 0.35
max_detections: int = 50
class FacesSettings(BaseModel):
"""YuNet + SFace face detection/recognition settings"""
enabled: bool = True
min_face_size: int = 40
recognition_threshold: float = 0.65
cluster_eps: float = 0.5
class ClassifierSettings(BaseModel):
"""CLIP zero-shot content classification settings"""
enabled: bool = True
min_confidence: float = 0.3
class VisionSettings(BaseModel):
"""AI vision pipeline settings. Disabled when running on SQLite
(pgvector is required for embedding storage)."""
enabled: bool = True
backend: str = "onnx" # "onnx" | "rocm" (future)
models_dir: str = "/data/models"
# ONNX Runtime execution providers in priority order.
# Auto-detected at startup; falls back to CPU if GPU is unavailable.
# Options: "CUDAExecutionProvider", "ROCMExecutionProvider",
# "OpenVINOExecutionProvider", "CPUExecutionProvider"
execution_providers: list[str] = ["CPUExecutionProvider"]
embedder: EmbedderSettings = EmbedderSettings()
ocr: OCRSettings = OCRSettings()
detector: DetectorSettings = DetectorSettings()
faces: FacesSettings = FacesSettings()
classifier: ClassifierSettings = ClassifierSettings()
worker_concurrency: int = 2
class MulitaConfig(BaseModel):
"""Main configuration from YAML file. Source roots and the discard
workflow are owned by the database now — only operational settings
live here."""
thumbnails: ThumbnailSettings = ThumbnailSettings()
scanner: ScannerSettings = ScannerSettings()
performance: PerformanceSettings = PerformanceSettings()
vision: VisionSettings = VisionSettings()
class Settings(BaseSettings):
"""Application settings"""
# Database — Postgres + pgvector by default. The SQLite escape hatch
# remains supported via the docker-compose.sqlite.yml override and by
# setting DATABASE_URL=sqlite+aiosqlite:///... in .env for local dev.
database_url: str = Field(
default="postgresql+asyncpg://mulita:mulita@db:5432/mulita",
env="DATABASE_URL"
)
# Redis
redis_url: str = Field(
default="redis://localhost:6379",
env="REDIS_URL"
)
# Celery
celery_broker_url: str = Field(
default="redis://localhost:6379",
env="CELERY_BROKER_URL"
)
celery_result_backend: str = Field(
default="redis://localhost:6379",
env="CELERY_RESULT_BACKEND"
)
# Photo directories
photo_dirs: str = Field(
default="/photos",
env="PHOTO_DIRS"
)
# API settings
api_host: str = Field(default="0.0.0.0", env="API_HOST")
api_port: int = Field(default=8000, env="API_PORT")
# CORS — comma-separated list of allowed origins, or "*" for any.
# Same-origin requests (the normal case behind nginx / vite proxy)
# never trip CORS, so this is only for direct browser access from
# other origins (LAN IP, reverse proxy, dev tools).
allowed_origins: str = Field(default="*", env="ALLOWED_ORIGINS")
# Logging — accepts standard python levels (DEBUG, INFO, WARNING,
# ERROR, CRITICAL). Bumped from INFO when chasing a problem.
log_level: str = Field(default="INFO", env="LOG_LEVEL")
# Auth — JWT signing key. Set SECRET_KEY in .env for production.
# If unset, a deterministic fallback is used (acceptable for
# single-machine homelab deploys, but set a real key if the instance
# is network-exposed).
secret_key: str = Field(
default="mulita-dev-secret-change-me",
env="SECRET_KEY",
)
access_token_expire_minutes: int = Field(default=60, env="ACCESS_TOKEN_EXPIRE_MINUTES")
refresh_token_expire_days: int = Field(default=30, env="REFRESH_TOKEN_EXPIRE_DAYS")
@property
def cors_origins(self) -> list[str]:
"""Parse the ALLOWED_ORIGINS env var into a list. Accepts:
- "*" → wildcard (single-element list ["*"])
- "http://a.com,http://b.com" → split + strip
Empty entries are dropped.
"""
raw = (self.allowed_origins or "").strip()
if not raw or raw == "*":
return ["*"]
return [o.strip() for o in raw.split(",") if o.strip()]
# App configuration from YAML
_config: Optional[MulitaConfig] = None
@property
def config(self) -> MulitaConfig:
"""Load configuration from YAML file"""
if self._config is None:
config_path = Path("/app/config/mulita.yml")
if not config_path.exists():
config_path = Path("mulita.yml")
if config_path.exists():
with open(config_path, "r") as f:
config_data = yaml.safe_load(f)
self._config = MulitaConfig(**config_data)
else:
self._config = MulitaConfig()
return self._config
@property
def thumbnails(self) -> ThumbnailSettings:
return self.config.thumbnails
@property
def scanner(self) -> ScannerSettings:
return self.config.scanner
@property
def performance(self) -> PerformanceSettings:
return self.config.performance
# ONNX Runtime execution providers, overridable via env var.
# Comma-separated: "CUDAExecutionProvider,CPUExecutionProvider"
# or "auto" for GPU auto-detection.
vision_execution_providers: str = Field(
default="CPUExecutionProvider",
env="VISION_EXECUTION_PROVIDERS",
)
@property
def vision(self) -> VisionSettings:
v = self.config.vision
# Override execution_providers from env if set.
providers = [p.strip() for p in self.vision_execution_providers.split(",") if p.strip()]
if providers:
v.execution_providers = providers
return v
class Config:
env_file = ".env"
case_sensitive = False
# Global settings instance
settings = Settings()