Spaces:
Running
Running
| """ | |
| Runtime configuration for the voice-cloning app. | |
| All values come from environment variables. Nothing is hard-coded except | |
| the canonical language catalogue (see LANGUAGE_CATALOG) which defines the | |
| UI-level language options we want to expose; the backend `/languages` | |
| endpoint is still queried at startup and the intersection is used. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Dict | |
| # Load .env from the repo root (or current working directory) before any | |
| # os.getenv() calls in this module run. Silent no-op if python-dotenv isn't | |
| # installed or .env is absent. | |
| try: | |
| from dotenv import load_dotenv as _load_dotenv | |
| # Look for .env walking up from this file's directory first, then CWD. | |
| _here = Path(__file__).resolve().parent | |
| for _candidate in [_here.parent / ".env", _here / ".env", Path.cwd() / ".env"]: | |
| if _candidate.is_file(): | |
| _load_dotenv(_candidate, override=False) | |
| break | |
| else: | |
| # Fallback: let python-dotenv use its own search (walks up from CWD). | |
| _load_dotenv(override=False) | |
| except ImportError: | |
| pass | |
| # --------------------------------------------------------------------------- | |
| # Canonical language catalogue | |
| # --------------------------------------------------------------------------- | |
| # Maps ISO-639 codes used by our UI to a (native name, english name, provider- | |
| # hint) triple. `provider_hint` indicates which backend natively handles the | |
| # language: | |
| # - "chatterbox": native in Chatterbox Multilingual (23 langs). | |
| # - "minimax": only served cleanly by MiniMax (language_boost=Chinese,Yue). | |
| # The client auto-routes per hint. | |
| # | |
| # Cantonese (yue) is intentionally at the top because it is a first-class | |
| # requirement for this project (Hong Kong users). | |
| LANGUAGE_CATALOG: Dict[str, Dict[str, str]] = { | |
| "yue": {"native": "粵語 (Cantonese)", "english": "Cantonese", "provider_hint": "minimax"}, | |
| "en": {"native": "English", "english": "English", "provider_hint": "chatterbox"}, | |
| "zh": {"native": "普通話 (Mandarin)", "english": "Mandarin", "provider_hint": "chatterbox"}, | |
| "ja": {"native": "日本語", "english": "Japanese", "provider_hint": "chatterbox"}, | |
| "ko": {"native": "한국어", "english": "Korean", "provider_hint": "chatterbox"}, | |
| "fr": {"native": "Français", "english": "French", "provider_hint": "chatterbox"}, | |
| "es": {"native": "Español", "english": "Spanish", "provider_hint": "chatterbox"}, | |
| } | |
| def _as_bool(val: str | None, default: bool = False) -> bool: | |
| if val is None: | |
| return default | |
| return val.strip().lower() in {"1", "true", "yes", "on"} | |
| def _as_int(val: str | None, default: int) -> int: | |
| try: | |
| return int(val) if val is not None and val.strip() != "" else default | |
| except ValueError: | |
| return default | |
| def _as_float(val: str | None, default: float) -> float: | |
| try: | |
| return float(val) if val is not None and val.strip() != "" else default | |
| except ValueError: | |
| return default | |
| class ChatterboxProviderConfig: | |
| """Config for the Chatterbox-compatible HTTP backend (our Modal deploy).""" | |
| base_url: str = "" | |
| api_key: str | None = None | |
| default_model: str = "chatterbox-multilingual" | |
| timeout_seconds: int = 180 | |
| def from_env(cls) -> "ChatterboxProviderConfig": | |
| return cls( | |
| base_url=(os.getenv("CHATTERBOX_API_BASE_URL", "") or "").rstrip("/"), | |
| api_key=os.getenv("CHATTERBOX_API_KEY") or None, | |
| default_model=os.getenv("CHATTERBOX_DEFAULT_MODEL", "chatterbox-multilingual"), | |
| timeout_seconds=_as_int(os.getenv("CHATTERBOX_TIMEOUT_SECONDS"), 180), | |
| ) | |
| def is_configured(self) -> bool: | |
| return bool(self.base_url) | |
| class MinimaxProviderConfig: | |
| """Config for the MiniMax provider (Cantonese-capable).""" | |
| api_base_url: str = "https://api.minimax.io" | |
| api_key: str | None = None | |
| group_id: str | None = None | |
| default_model: str = "speech-2.8-hd" | |
| timeout_seconds: int = 180 | |
| def from_env(cls) -> "MinimaxProviderConfig": | |
| return cls( | |
| api_base_url=(os.getenv("MINIMAX_API_BASE_URL", "https://api.minimax.io") or "").rstrip("/"), | |
| api_key=os.getenv("MINIMAX_API_KEY") or None, | |
| group_id=os.getenv("MINIMAX_GROUP_ID") or None, | |
| default_model=os.getenv("MINIMAX_DEFAULT_MODEL", "speech-2.8-hd"), | |
| timeout_seconds=_as_int(os.getenv("MINIMAX_TIMEOUT_SECONDS"), 180), | |
| ) | |
| def is_configured(self) -> bool: | |
| # group_id is optional for plain T2A, but api_key is mandatory. | |
| return bool(self.api_key) | |
| class Settings: | |
| """Top-level settings object used by the app.""" | |
| # Upload limits (reference audio). | |
| max_reference_mb: int = 25 | |
| min_reference_seconds: float = 3.0 | |
| recommended_reference_seconds_min: float = 10.0 | |
| recommended_reference_seconds_max: float = 30.0 | |
| max_reference_seconds: float = 60.0 | |
| # Generation defaults (RESOLVED CONFLICT between notes and UI). | |
| # Chatterbox docs: exaggeration default 0.5 (range 0.25-2.0), | |
| # cfg_weight default 0.5 (range 0.0-1.0). The old report's "3.0" was | |
| # a documentation error. We honour the upstream-documented ranges. | |
| default_exaggeration: float = 0.5 | |
| default_cfg_weight: float = 0.5 | |
| default_temperature: float = 0.8 | |
| exaggeration_range: tuple = (0.25, 2.0) | |
| cfg_weight_range: tuple = (0.0, 1.0) | |
| temperature_range: tuple = (0.05, 5.0) | |
| # Runtime behaviour. | |
| temp_dir: Path = field(default_factory=lambda: Path(os.getenv("TEMP_DIR", "/tmp/voice-clone-cloud"))) | |
| retry_on_transient: bool = True | |
| request_retry_once: bool = True | |
| log_provider_errors: bool = True | |
| # Feature flags. | |
| # Watermarking: we honour the backend's default. Only disable if a | |
| # provider explicitly cannot install the `perth` library. The old HPC | |
| # build disabled it because HF Hub was offline and `perth` wasn't | |
| # vendored in the cluster cache — not a real runtime constraint. | |
| disable_watermark: bool = False | |
| chatterbox: ChatterboxProviderConfig = field(default_factory=ChatterboxProviderConfig.from_env) | |
| minimax: MinimaxProviderConfig = field(default_factory=MinimaxProviderConfig.from_env) | |
| # Gradio server. | |
| gradio_server_name: str = "0.0.0.0" | |
| gradio_server_port: int = 7860 | |
| def from_env(cls) -> "Settings": | |
| s = cls( | |
| max_reference_mb=_as_int(os.getenv("MAX_REFERENCE_MB"), 25), | |
| min_reference_seconds=_as_float(os.getenv("MIN_REFERENCE_SECONDS"), 3.0), | |
| max_reference_seconds=_as_float(os.getenv("MAX_REFERENCE_SECONDS"), 60.0), | |
| default_exaggeration=_as_float(os.getenv("DEFAULT_EXAGGERATION"), 0.5), | |
| default_cfg_weight=_as_float(os.getenv("DEFAULT_CFG_WEIGHT"), 0.5), | |
| default_temperature=_as_float(os.getenv("DEFAULT_TEMPERATURE"), 0.8), | |
| disable_watermark=_as_bool(os.getenv("DISABLE_WATERMARK"), False), | |
| gradio_server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), | |
| gradio_server_port=_as_int(os.getenv("GRADIO_SERVER_PORT"), 7860), | |
| ) | |
| s.temp_dir.mkdir(parents=True, exist_ok=True) | |
| return s | |
| # Cached singleton (lazy). | |
| _settings: Settings | None = None | |
| def get_settings() -> Settings: | |
| global _settings | |
| if _settings is None: | |
| _settings = Settings.from_env() | |
| return _settings | |
| def reset_settings() -> None: | |
| """For tests: drop the cached settings so env changes take effect.""" | |
| global _settings | |
| _settings = None | |