import os from dataclasses import dataclass from pathlib import Path from urllib.parse import quote 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 volcano_oss_access_key: str volcano_oss_secret_key: str volcano_oss_bucket: str volcano_oss_endpoint: str volcano_oss_region: str volcano_oss_key_prefix: str volcano_oss_public_base_url: str @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")), volcano_oss_access_key=os.getenv("VOLCANO_OSS_ACCESS_KEY", "").strip(), volcano_oss_secret_key=os.getenv("VOLCANO_OSS_SECRET_KEY", "").strip(), volcano_oss_bucket=os.getenv("VOLCANO_OSS_BUCKET", "").strip(), volcano_oss_endpoint=os.getenv( "VOLCANO_OSS_ENDPOINT", "tos-cn-beijing.volces.com" ).strip().strip("/"), volcano_oss_region=os.getenv("VOLCANO_OSS_REGION", "cn-beijing").strip(), volcano_oss_key_prefix=Settings._normalize_key_prefix( os.getenv("VOLCANO_OSS_KEY_PREFIX", "videos/") ), volcano_oss_public_base_url=os.getenv( "VOLCANO_OSS_PUBLIC_BASE_URL", "" ).strip().rstrip("/"), ) @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 dub_shares_dir(self) -> Path: return self.data_dir / "dub_shares" @property def database_path(self) -> Path: return self.data_dir / "oral_trainer.sqlite3" @property def volcano_oss_enabled(self) -> bool: return all( ( self.volcano_oss_access_key, self.volcano_oss_secret_key, self.volcano_oss_bucket, self.volcano_oss_endpoint, self.volcano_oss_region, ) ) def volcano_oss_public_url(self, object_key: str) -> str: quoted_key = quote(object_key) if self.volcano_oss_public_base_url: return f"{self.volcano_oss_public_base_url}/{quoted_key}" endpoint = self.volcano_oss_endpoint if "://" in endpoint: scheme, host = endpoint.split("://", 1) else: scheme, host = "https", endpoint return f"{scheme}://{self.volcano_oss_bucket}.{host}/{quoted_key}" @staticmethod def _normalize_key_prefix(value: str) -> str: normalized = value.strip().strip("/") return f"{normalized}/" if normalized else "" def ensure_directories(self) -> None: for path in (self.data_dir, self.videos_dir, self.work_dir, self.attempts_dir, self.dub_shares_dir): path.mkdir(parents=True, exist_ok=True)