import os from dataclasses import dataclass from pathlib import Path def _bool_env(name: str, default: bool) -> bool: value = os.getenv(name) if value is None: return default return value.strip().lower() in {"1", "true", "yes", "on"} @dataclass(frozen=True) class Settings: data_dir: Path legacy_boundaries_file: Path admin_api_key: str client_api_key: str public_base_url: str max_upload_bytes: int max_attempt_bytes: int keep_attempt_audio: bool moss_transcribe_url: str moss_model: str moss_timeout_seconds: float moss_max_new_tokens: int moss_end_padding_ms: int pass_score: float @classmethod def from_env(cls) -> "Settings": package_dir = Path(__file__).resolve().parent data_dir = Path(os.getenv("ORAL_TRAINER_DATA_DIR", str(package_dir / "data"))) return cls( data_dir=data_dir, legacy_boundaries_file=Path( os.getenv( "SENTENCE_BOUNDARIES_FILE", str(package_dir / "data" / "sentence_boundaries.json"), ) ), admin_api_key=os.getenv("ADMIN_API_KEY", ""), client_api_key=os.getenv("CLIENT_API_KEY", ""), public_base_url=os.getenv("PUBLIC_BASE_URL", "").rstrip("/"), max_upload_bytes=int(os.getenv("MAX_VIDEO_UPLOAD_BYTES", str(12 * 1024**3))), max_attempt_bytes=int(os.getenv("MAX_ATTEMPT_UPLOAD_BYTES", str(50 * 1024**2))), keep_attempt_audio=_bool_env("KEEP_ATTEMPT_AUDIO", False), moss_transcribe_url=os.getenv("MOSS_TRANSCRIBE_URL", "").strip(), moss_model=os.getenv( "MOSS_MODEL", "OpenMOSS-Team/MOSS-Transcribe-Diarize", ), moss_timeout_seconds=float(os.getenv("MOSS_TIMEOUT_SECONDS", "1800")), moss_max_new_tokens=int(os.getenv("MOSS_MAX_NEW_TOKENS", "65536")), moss_end_padding_ms=int(os.getenv("MOSS_END_PADDING_MS", "300")), pass_score=float(os.getenv("ASSESSMENT_PASS_SCORE", "70")), ) @property def videos_dir(self) -> Path: return self.data_dir / "v" @property def work_dir(self) -> Path: return self.data_dir / "work" @property def attempts_dir(self) -> Path: return self.data_dir / "attempts" @property def database_path(self) -> Path: return self.data_dir / "oral_trainer.sqlite3" def ensure_directories(self) -> None: for path in (self.data_dir, self.videos_dir, self.work_dir, self.attempts_dir): path.mkdir(parents=True, exist_ok=True)