60 lines
1.7 KiB
Python
60 lines
1.7 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
import yaml
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
class CaptureConfig(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
backend: Literal["fixture", "mss", "dxcam"] = "fixture"
|
|
monitor: int = 1
|
|
fps_limit: int = Field(default=10, ge=1, le=240)
|
|
window_title: str | None = None
|
|
|
|
|
|
class VisionConfig(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
template_threshold: float = Field(default=0.85, ge=0.0, le=1.0)
|
|
debug_output_dir: Path = Path("botty_next/debug/output")
|
|
|
|
|
|
class InputConfig(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
enabled: bool = False
|
|
dry_run: bool = True
|
|
emergency_stop_key: str = "f12"
|
|
|
|
@field_validator("dry_run")
|
|
@classmethod
|
|
def dry_run_required_when_disabled(cls, value: bool) -> bool:
|
|
return value
|
|
|
|
|
|
class BottyNextConfig(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
profile_name: str = "local"
|
|
capture: CaptureConfig = Field(default_factory=CaptureConfig)
|
|
vision: VisionConfig = Field(default_factory=VisionConfig)
|
|
input: InputConfig = Field(default_factory=InputConfig)
|
|
|
|
@field_validator("input")
|
|
@classmethod
|
|
def input_must_be_explicit_and_dry_run_by_default(cls, value: InputConfig) -> InputConfig:
|
|
if value.enabled and value.dry_run is False:
|
|
raise ValueError("live input cannot be enabled without a future explicit safety gate")
|
|
return value
|
|
|
|
|
|
def load_config(path: str | Path) -> BottyNextConfig:
|
|
config_path = Path(path)
|
|
with config_path.open("r", encoding="utf-8") as handle:
|
|
raw = yaml.safe_load(handle) or {}
|
|
return BottyNextConfig.model_validate(raw)
|