Files
agr-assistent/src/agr_assistent/tts/silero.py
T
mrleo1nidandClaude Opus 5 fec52dd1c2 Этап 2: озвучка ответов через Silero
- Потоковая озвучка: ответ режется на фразы, синтез и воспроизведение в отдельных потоках
- Подготовка текста: числа словами, без markdown, ссылок и блоков кода
- Модель Silero скачивается в LOCALAPPDATA и прогревается при старте
- Состояние «говорю», переключатель озвучки в трее, «Стоп» прерывает речь
- Тесты нарезки текста и оркестратора озвучки

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 03:20:07 +03:00

94 lines
3.6 KiB
Python

"""Синтез речи моделями Silero (https://github.com/snakers4/silero-models)."""
from __future__ import annotations
import logging
import os
import threading
import urllib.request
import warnings
from pathlib import Path
from typing import Any
import numpy as np
from agr_assistent.config import TTSConfig
from agr_assistent.tts.base import TTSError
log = logging.getLogger(__name__)
_MODEL_URL = "https://models.silero.ai/models/tts/{language}/{model}.pt"
_MAX_TORCH_THREADS = 4
_WARM_UP_TEXT = "Готова к работе."
class SileroTTS:
def __init__(self, config: TTSConfig, models_dir: Path) -> None:
self._config = config
self._model_path = models_dir / "silero" / f"{config.model}.pt"
self._model: Any = None
self._lock = threading.Lock()
@property
def sample_rate(self) -> int:
return self._config.sample_rate
def load(self) -> None:
with self._lock:
if self._model is not None:
return
if not self._model_path.exists():
self._download()
# torch импортируется долго, поэтому только при первой загрузке модели
import torch
torch.set_num_threads(min(_MAX_TORCH_THREADS, os.cpu_count() or 1))
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
importer = torch.package.PackageImporter(str(self._model_path))
model = importer.load_pickle("tts_models", "model")
model.to(torch.device(self._config.device))
except Exception as exc:
raise TTSError(f"Не удалось загрузить модель {self._model_path}: {exc}") from exc
if self._config.speaker not in model.speakers:
raise TTSError(
f"Голос '{self._config.speaker}' отсутствует в модели {self._config.model} "
f"(есть: {', '.join(model.speakers)})"
)
# Первый синтез заметно медленнее последующих — прогреваем заранее
self._apply_tts(model, _WARM_UP_TEXT)
self._model = model
log.info("Модель Silero %s загружена", self._config.model)
def synthesize(self, text: str) -> np.ndarray:
self.load()
return self._apply_tts(self._model, text)
def _apply_tts(self, model: Any, text: str) -> np.ndarray:
import torch
with torch.inference_mode(), warnings.catch_warnings():
warnings.simplefilter("ignore")
audio = model.apply_tts(
text=text,
speaker=self._config.speaker,
sample_rate=self._config.sample_rate,
)
return audio.cpu().numpy().astype(np.float32, copy=False)
def _download(self) -> None:
language = self._config.model.rsplit("_", 1)[-1]
url = _MODEL_URL.format(language=language, model=self._config.model)
partial_path = self._model_path.with_suffix(".part")
log.info("Скачиваю модель Silero: %s", url)
try:
self._model_path.parent.mkdir(parents=True, exist_ok=True)
urllib.request.urlretrieve(url, partial_path)
partial_path.replace(self._model_path)
except OSError as exc:
partial_path.unlink(missing_ok=True)
raise TTSError(f"Не удалось скачать модель Silero {url}: {exc}") from exc