Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import os | |
| import shutil | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| PROJECT_ROOT = Path(__file__).resolve().parents[1] | |
| ENV_PATH = PROJECT_ROOT / ".env" | |
| def _load_env_file() -> None: | |
| if not ENV_PATH.exists(): | |
| return | |
| for raw_line in ENV_PATH.read_text(encoding="utf-8").splitlines(): | |
| line = raw_line.strip() | |
| if not line or line.startswith("#") or "=" not in line: | |
| continue | |
| key, value = line.split("=", 1) | |
| key = key.strip() | |
| value = value.strip().strip('"').strip("'") | |
| os.environ.setdefault(key, value) | |
| def _env_int(name: str, default: int) -> int: | |
| value = os.getenv(name) | |
| return default if not value else int(value) | |
| def _env_float(name: str, default: float) -> float: | |
| value = os.getenv(name) | |
| return default if not value else float(value) | |
| def _env_path(name: str, default: Path | None = None) -> Path | None: | |
| value = os.getenv(name) | |
| if value: | |
| path = Path(value).expanduser() | |
| return path if path.is_absolute() else PROJECT_ROOT / path | |
| return default | |
| class Settings: | |
| project_root: Path = PROJECT_ROOT | |
| data_dir: Path = PROJECT_ROOT / "data" | |
| raw_maps_dir: Path = PROJECT_ROOT / "data" / "raw" / "maps" | |
| processed_dir: Path = PROJECT_ROOT / "data" / "processed" | |
| games_dir: Path = PROJECT_ROOT / "data" / "games" | |
| junction_registry_path: Path = PROJECT_ROOT / "data" / "processed" / "junction_registry.json" | |
| game_graph_path: Path = PROJECT_ROOT / "data" / "processed" / "game_graph.json" | |
| map_metadata_path: Path = PROJECT_ROOT / "data" / "processed" / "map_metadata.json" | |
| map_atlas_path: Path = PROJECT_ROOT / "data" / "processed" / "map_atlas.json" | |
| # Default backend is the plain text llama.cpp server running OpenBMB's | |
| # MiniCPM4.1-8B (text-only, no audio). It is far more reliable at staying in | |
| # English than the MiniCPM-o omni model, whose TTS/audio branch frequently | |
| # drifts into Chinese. The omni model remains selectable for voice features. | |
| llm_provider: str = "llama_cpp_server" | |
| llamacpp_server_bin: Path | None = None | |
| llamacpp_model_path: Path | None = None | |
| llamacpp_base_url: str = "http://127.0.0.1:8080/v1" | |
| llm_model: str = "MiniCPM4.1-8B-Q4_K_M.gguf" | |
| omni_gateway_url: str = "http://127.0.0.1:8006" | |
| omni_launcher_path: Path | None = PROJECT_ROOT / "scripts" / "launch_minicpm_omni.py" | |
| comni_checkout_path: Path | None = None | |
| llamacpp_omni_root: Path | None = None | |
| minicpm_model_dir: Path | None = None | |
| minicpm_quantization: str = "" | |
| llamacpp_context_length: int = 8192 | |
| llamacpp_gpu_layers: str = "auto" | |
| minicpm_gpu_device: str = "auto" | |
| # The default text backend has no audio output, so voice is off by default. | |
| # Enabling it only has an effect when the MiniCPM-o omni provider is active. | |
| witness_chat_tts: bool = False | |
| witness_voice_dir: Path = PROJECT_ROOT / "data" / "voices" | |
| max_turns: int = 12 | |
| checks_per_turn: int = 2 | |
| blocks_per_turn: int = 1 | |
| max_active_blocks: int = 3 | |
| individual_witness_threshold: int = 12 | |
| starting_disguise_changes: int = 3 | |
| memory_corruption_per_turn: float = 0.08 | |
| def load_settings() -> Settings: | |
| _load_env_file() | |
| server_bin = _env_path("PHANTOM_GRID_LLAMACPP_SERVER_BIN") | |
| if server_bin is None: | |
| detected = shutil.which("llama-server") or shutil.which("llama-server.exe") | |
| server_bin = Path(detected) if detected else None | |
| # HF Spaces auto-injects SPACE_ID. When present, default to the in-process | |
| # ZeroGPU transformers backend so the Space never tries to invoke cmake or | |
| # spawn llama-server. Explicit PHANTOM_GRID_LLM_PROVIDER still wins. | |
| default_provider = "zerogpu_transformers" if os.getenv("SPACE_ID") else "llama_cpp_server" | |
| return Settings( | |
| games_dir=_env_path("PHANTOM_GRID_GAMES_DIR", PROJECT_ROOT / "data" / "games") or PROJECT_ROOT / "data" / "games", | |
| llm_provider=os.getenv("PHANTOM_GRID_LLM_PROVIDER", default_provider), | |
| llamacpp_server_bin=server_bin, | |
| llamacpp_model_path=_env_path("PHANTOM_GRID_LLAMACPP_MODEL_PATH"), | |
| llamacpp_base_url=os.getenv("PHANTOM_GRID_LLAMACPP_BASE_URL", "http://127.0.0.1:8080/v1"), | |
| llm_model=os.getenv("PHANTOM_GRID_LLM_MODEL", "MiniCPM4.1-8B-Q4_K_M.gguf"), | |
| omni_gateway_url=os.getenv("PHANTOM_GRID_OMNI_GATEWAY_URL", "http://127.0.0.1:8006"), | |
| omni_launcher_path=_env_path("PHANTOM_GRID_OMNI_LAUNCHER_PATH", PROJECT_ROOT / "scripts" / "launch_minicpm_omni.py"), | |
| comni_checkout_path=_env_path("PHANTOM_GRID_COMNI_CHECKOUT_PATH"), | |
| llamacpp_omni_root=_env_path("PHANTOM_GRID_LLAMACPP_OMNI_ROOT"), | |
| minicpm_model_dir=_env_path("PHANTOM_GRID_MINICPM_MODEL_DIR"), | |
| minicpm_quantization=os.getenv("PHANTOM_GRID_MINICPM_QUANTIZATION", ""), | |
| llamacpp_context_length=_env_int("PHANTOM_GRID_LLAMACPP_CONTEXT_LENGTH", 8192), | |
| llamacpp_gpu_layers=os.getenv("PHANTOM_GRID_LLAMACPP_GPU_LAYERS", "auto"), | |
| minicpm_gpu_device=os.getenv("PHANTOM_GRID_GPU_DEVICE", "auto"), | |
| witness_chat_tts=os.getenv("PHANTOM_GRID_WITNESS_CHAT_TTS", "0").strip().lower() not in {"0", "false", "off", "no"}, | |
| witness_voice_dir=_env_path("PHANTOM_GRID_WITNESS_VOICE_DIR", PROJECT_ROOT / "data" / "voices") or PROJECT_ROOT / "data" / "voices", | |
| max_turns=_env_int("PHANTOM_GRID_MAX_TURNS", 12), | |
| checks_per_turn=_env_int("PHANTOM_GRID_CHECKS_PER_TURN", 2), | |
| blocks_per_turn=_env_int("PHANTOM_GRID_BLOCKS_PER_TURN", 1), | |
| individual_witness_threshold=_env_int("PHANTOM_GRID_INDIVIDUAL_WITNESS_THRESHOLD", 12), | |
| memory_corruption_per_turn=_env_float("PHANTOM_GRID_MEMORY_CORRUPTION_PER_TURN", 0.08), | |
| ) | |