"""Чтение настроек сервиса. ЭТО КОД, А НЕ НАСТРОЙКИ. Править нужно файл config.toml рядом со start.bat - он создаётся при первом запуске и обновлением не затрагивается. Правки в этом файле не подействуют (config.toml уже существует) и пропадут при обновлении. """ import os import secrets import tomllib from dataclasses import dataclass, field from pathlib import Path __all__ = ["Settings", "load_settings", "BASE_DIR"] # Корень пакета: при запуске из ZIP это папка, куда его распаковали. BASE_DIR = Path(os.environ.get("TALKSCORE_ASR_HOME") or Path(__file__).resolve().parent.parent) _DEFAULT_CONFIG = """# Настройки talkscore-asr [server] # 0.0.0.0 - принимать со всех интерфейсов, 127.0.0.1 - только с этой машины host = "0.0.0.0" port = 8756 [security] # Токен для заголовка: Authorization: Bearer # Пустое значение = сервис никого не пустит. Сгенерирован при первом запуске. token = "{token}" # Кто может обращаться. Пусто = разрешены все адреса. # Примеры: "192.168.1.0/24, 10.8.0.5, 127.0.0.1" allow_ips = "127.0.0.1, ::1" # Открывать ли /docs и /openapi.json. Они не требуют токена и показывают # устройство API всем, кто знает адрес, поэтому по умолчанию выключены. docs = false [processing] # Потоков на одну задачу. 0 = половина ядер, но не больше 8. # Оптимум зависит от процессора: подберите его через POST /v1/benchmark # и впишите сюда полученное число. threads = 0 # Сколько записей обрабатывать одновременно. Значение больше 1 включает # отдельные процессы: библиотеки держат GIL, и в потоках задачи выстраиваются # в очередь вместо параллельной работы. Каждый процесс держит свою копию # моделей, это около 1 ГБ памяти на процесс. workers = 1 # Ожидаемое число говорящих в записи. 0 = определять автоматически # (на реальных звонках работает плохо, для диалога ставьте 2). speakers = 2 # Выравнивать громкость перед обработкой. Для записей с микрофоном на столе # это решающая настройка: без неё тихий участник сливается с громким и # разделение по говорящим разваливается. На распознавание не влияет. normalize = true # Максимальный размер загружаемого файла, МБ max_upload_mb = 500 # Сколько часов хранить результаты завершённых задач keep_results_hours = 72 [webhook] # Куда сообщать о готовых задачах. Пусто = не сообщать, забирайте опросом. # Можно переопределить для отдельной задачи параметром webhook в запросе. url = "" # Секрет для подписи: сервис положит её в заголовок X-Talkscore-Signature, # чтобы принимающая сторона убедилась, что запрос от вас. secret = "" [update] # Проверять обновления кода при каждом запуске. Обновляется только папка app, # это десятки килобайт: Python, библиотеки и модели остаются на месте. enabled = true server = "https://git.netranking.ru" repo = "bryzgalov/talkscore-asr" # Токен Gitea с правом чтения репозитория. Для публичного репозитория не нужен. token = "" """ @dataclass class Settings: host: str = "0.0.0.0" port: int = 8756 token: str = "" allow_ips: str = "" docs: bool = False threads: int = 0 workers: int = 0 speakers: int = 2 normalize: bool = True max_upload_mb: int = 500 keep_results_hours: float = 72.0 webhook_url: str = "" webhook_secret: str = "" update_enabled: bool = False update_server: str = "https://git.netranking.ru" update_repo: str = "bryzgalov/talkscore-asr" update_token: str = "" base_dir: Path = field(default_factory=lambda: BASE_DIR) @property def models_dir(self) -> Path: return self.base_dir / "models" @property def data_dir(self) -> Path: return self.base_dir / "data" @property def replacements_path(self) -> Path: return self.base_dir / "replacements.txt" @property def max_upload_bytes(self) -> int: return self.max_upload_mb * 1024 * 1024 # Масштабирование сильно зависит от процессора, и универсального числа нет. # На Ryzen 9 9950X всё растёт до 16 потоков (распознавание x14.7 -> x61.3), # на Apple M4 после 4 потоков начинается спад: там половина ядер медленные # и работа, попавшая на них, тормозит остальные. # Поэтому дефолт умеренный, а точное значение подбирается через /v1/benchmark. MAX_DEFAULT_THREADS = 8 MAX_WORKERS = 4 def effective_threads(self) -> int: if self.threads > 0: return self.threads cores = os.cpu_count() or 4 return max(1, min(self.MAX_DEFAULT_THREADS, cores // 2)) def effective_workers(self) -> int: if self.workers > 0: return self.workers cores = os.cpu_count() or 4 return max(1, min(self.MAX_WORKERS, cores // self.effective_threads())) def load_settings(config_path: Path | None = None) -> Settings: """Читает config.toml, создавая его с новым токеном при первом запуске.""" path = Path(config_path or BASE_DIR / "config.toml") if not path.is_file(): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(_DEFAULT_CONFIG.format(token=secrets.token_urlsafe(32)), encoding="utf-8") data = tomllib.loads(path.read_text(encoding="utf-8")) server = data.get("server", {}) security = data.get("security", {}) proc = data.get("processing", {}) upd = data.get("update", {}) hook = data.get("webhook", {}) return Settings( host=server.get("host", "0.0.0.0"), port=int(server.get("port", 8756)), token=str(security.get("token", "")), allow_ips=str(security.get("allow_ips", "")), docs=bool(security.get("docs", False)), threads=int(proc.get("threads", 0)), workers=int(proc.get("workers", 1)), speakers=int(proc.get("speakers", 2)), normalize=bool(proc.get("normalize", True)), max_upload_mb=int(proc.get("max_upload_mb", 500)), keep_results_hours=float(proc.get("keep_results_hours", 72)), webhook_url=str(hook.get("url", "")), webhook_secret=str(hook.get("secret", "")), update_enabled=bool(upd.get("enabled", False)), update_server=str(upd.get("server", "https://git.netranking.ru")), update_repo=str(upd.get("repo", "bryzgalov/talkscore-asr")), update_token=str(upd.get("token", "")), base_dir=path.parent, )