voice-clone-cloud / app /config.py
waltertwl's picture
Deploy voice-clone-cloud UI
8f8a746 verified
Raw
History Blame Contribute Delete
7.8 kB
"""
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
@dataclass
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
@classmethod
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),
)
@property
def is_configured(self) -> bool:
return bool(self.base_url)
@dataclass
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
@classmethod
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),
)
@property
def is_configured(self) -> bool:
# group_id is optional for plain T2A, but api_key is mandatory.
return bool(self.api_key)
@dataclass
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
@classmethod
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