from __future__ import annotations import json import os from dataclasses import dataclass from functools import lru_cache from pathlib import Path from typing import Any, Dict, List, Optional CONFIG_FILE = Path(__file__).with_name("app-config.json") DEFAULT_LABELS: List[str] = [ "Heart", "Oblong", "Oval", "Round", "Square", ] DEFAULT_ALLOWED_ORIGINS: List[str] = [ "http://localhost:3000", "http://127.0.0.1:3000", ] DEFAULT_PORT = 7860 def _load_config_file() -> Dict[str, Any]: if not CONFIG_FILE.exists(): return {} try: return json.loads(CONFIG_FILE.read_text()) except json.JSONDecodeError as exc: raise ValueError(f"Konfigurasi JSON tidak valid: {CONFIG_FILE}") from exc def _parse_labels(raw: Any) -> List[str]: if raw is None: return DEFAULT_LABELS.copy() if isinstance(raw, str): parts = [label.strip() for label in raw.split(",") if label.strip()] return parts or DEFAULT_LABELS.copy() if isinstance(raw, list): filtered = [str(label).strip() for label in raw if str(label).strip()] return filtered or DEFAULT_LABELS.copy() return DEFAULT_LABELS.copy() def _parse_origins(raw: Any) -> List[str]: if raw is None: return DEFAULT_ALLOWED_ORIGINS.copy() if isinstance(raw, str): parts = [origin.strip() for origin in raw.split(",") if origin.strip()] return parts or DEFAULT_ALLOWED_ORIGINS.copy() if isinstance(raw, list): filtered = [str(origin).strip() for origin in raw if str(origin).strip()] return filtered or DEFAULT_ALLOWED_ORIGINS.copy() return DEFAULT_ALLOWED_ORIGINS.copy() def _parse_bool(raw: Any, fallback: bool = False) -> bool: if raw is None: return fallback if isinstance(raw, bool): return raw return str(raw).strip().lower() in {"1", "true", "yes", "on"} def _parse_int(raw: Any, fallback: int) -> int: if raw is None: return fallback try: value = int(raw) except (TypeError, ValueError): return fallback return value if value > 0 else fallback def _resolve_model_path(file_value: Optional[str]) -> str: env_value = os.environ.get("MODEL_PATH") candidate = env_value or file_value or "model/best_model.keras" path = Path(candidate) if not path.is_absolute(): path = (CONFIG_FILE.parent / path).resolve() return str(path) @dataclass(frozen=True) class Settings: model_path: str labels: List[str] port: int cors_allowed_origins: List[str] gradio_auth_token: str | None gradio_username: str | None gradio_password: str | None share: bool _CONFIG_CACHE = _load_config_file() @lru_cache(maxsize=1) def get_settings() -> Settings: labels_raw = os.environ.get("MODEL_LABELS") origins_raw = os.environ.get("CORS_ALLOWED_ORIGINS") share_raw: Any = os.environ.get("GRADIO_SHARE") port_raw: Any = os.environ.get("PORT") return Settings( model_path=_resolve_model_path(_CONFIG_CACHE.get("model_path")), labels=_parse_labels(labels_raw if labels_raw is not None else _CONFIG_CACHE.get("labels")), port=_parse_int(port_raw if port_raw is not None else _CONFIG_CACHE.get("port"), DEFAULT_PORT), cors_allowed_origins=_parse_origins( origins_raw if origins_raw is not None else _CONFIG_CACHE.get("cors_allowed_origins") ), gradio_auth_token=os.environ.get("GRADIO_AUTH_TOKEN") or _CONFIG_CACHE.get("gradio_auth_token"), gradio_username=os.environ.get("GRADIO_USERNAME") or _CONFIG_CACHE.get("gradio_username"), gradio_password=os.environ.get("GRADIO_PASSWORD") or _CONFIG_CACHE.get("gradio_password"), share=_parse_bool(share_raw if share_raw is not None else _CONFIG_CACHE.get("share"), False), ) settings = get_settings()