from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path def _env(name: str, default: str = "") -> str: return os.environ.get(name, default) def _resolve_models_yaml() -> str: if env := os.environ.get("MODELS_YAML"): return env here = Path(__file__).resolve().parent # app/ candidates = [ here.parent / "models.yaml", # image/models.yaml (Docker) here.parent.parent / "models.yaml", # /models.yaml (dev) Path("/app/models.yaml"), # explicit container path ] for p in candidates: if p.exists(): return str(p) return str(candidates[0]) # let load fail with a clear path @dataclass(frozen=True) class Settings: spark1_host: str spark1_user: str spark2_host: str spark2_user: str parakeet_host: str parakeet_user: str parakeet_container: str kokoro_host: str kokoro_user: str kokoro_container: str embed_host: str embed_user: str embed_container: str qdrant_host: str qdrant_user: str qdrant_container: str qdrant_collection: str matrix_bridge_host: str matrix_bridge_user: str matrix_bridge_container: str matrix_bridge_dir: str matrix_bridge_branch: str redaction_map_db: str redaction_map_ttl: int ssh_key_path: str ssh_known_hosts: str models_yaml: str vllm_port: int parakeet_port: int kokoro_port: int embed_port: int qdrant_port: int bind_port: int open_webui_url: str ngc_api_key: str @classmethod def from_env(cls) -> "Settings": spark2_host = _env("SPARK2_HOST") spark2_user = _env("SPARK2_USER") # Parakeet (STT) and Kokoro (TTS) default to Spark 2 unless overridden. return cls( spark1_host=_env("SPARK1_HOST"), spark1_user=_env("SPARK1_USER"), spark2_host=spark2_host, spark2_user=spark2_user, parakeet_host=_env("PARAKEET_HOST") or spark2_host, parakeet_user=_env("PARAKEET_USER") or spark2_user, parakeet_container=_env("PARAKEET_CONTAINER") or "parakeet-asr", kokoro_host=_env("KOKORO_HOST") or spark2_host, kokoro_user=_env("KOKORO_USER") or spark2_user, kokoro_container=_env("KOKORO_CONTAINER") or "kokoro-tts", # Embeddings (spark-embed: bge-m3 dense + reranker) and Qdrant # (vector storage) default to Spark 2 unless overridden. embed_host=_env("EMBED_HOST") or spark2_host, embed_user=_env("EMBED_USER") or spark2_user, embed_container=_env("EMBED_CONTAINER") or "spark-embed", qdrant_host=_env("QDRANT_HOST") or spark2_host, qdrant_user=_env("QDRANT_USER") or spark2_user, qdrant_container=_env("QDRANT_CONTAINER") or "qdrant", qdrant_collection=_env("QDRANT_COLLECTION", ""), # matrix-bridge bot container, driven as its own SSH user (the owner # of the ~/matrix-bridge git clone) so git/docker run unprivileged. # The user is BLANK by default and set via the "Configure Sparks" # action; leaving it blank reports the service as unconfigured, which # hides the tile. That keeps the shared package portable — a # deployment without the bot never shows a stray tile or a hardcoded # username. Host defaults to Spark 2 (same box); container/dir/branch # are sensible defaults. All are env-overridable. matrix_bridge_host=_env("MATRIX_BRIDGE_HOST") or spark2_host, matrix_bridge_user=_env("MATRIX_BRIDGE_USER"), matrix_bridge_container=_env("MATRIX_BRIDGE_CONTAINER") or "matrix-bridge", matrix_bridge_dir=_env("MATRIX_BRIDGE_DIR") or "~/matrix-bridge", matrix_bridge_branch=_env("MATRIX_BRIDGE_BRANCH") or "master", # Redaction gateway pseudonym-map store (server-held de-anon key). redaction_map_db=_env("REDACTION_MAP_DB", "/data/redaction_maps.db"), redaction_map_ttl=int(_env("REDACTION_MAP_TTL", "7200")), ssh_key_path=_env("SSH_KEY_PATH"), ssh_known_hosts=_env("SSH_KNOWN_HOSTS"), models_yaml=_resolve_models_yaml(), vllm_port=int(_env("VLLM_PORT", "8888")), parakeet_port=int(_env("PARAKEET_PORT", "8000")), kokoro_port=int(_env("KOKORO_PORT", "8880")), embed_port=int(_env("EMBED_PORT", "8088")), qdrant_port=int(_env("QDRANT_PORT", "6333")), bind_port=int(_env("BIND_PORT", "9999")), open_webui_url=_env("OPEN_WEBUI_URL", ""), ngc_api_key=_env("NGC_API_KEY", ""), ) @property def configured(self) -> bool: return bool(self.spark1_host)