Ключевая находка по записям с микрофоном на столе: без выравнивания тихий дальний участник сливается с громким и разделение разваливается. Замер на трёх разговорах: доля второго участника выросла с 1.8 до 24.3 процента, переключений между репликами - с 21 до 71 процента. Работает только выравнивание: шумоподавление, полосовой фильтр и компандер не дали ничего. Параметры подобраны замером, f=400:g=3 давал 8.6 процента вместо 43.2. На распознавание не влияет - расшифровки фрагментов совпали дословно. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
169 lines
7.9 KiB
Python
169 lines
7.9 KiB
Python
"""Чтение настроек сервиса.
|
|
|
|
ЭТО КОД, А НЕ НАСТРОЙКИ. Править нужно файл 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 = "{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,
|
|
)
|