Этап 2: озвучка ответов через Silero
- Потоковая озвучка: ответ режется на фразы, синтез и воспроизведение в отдельных потоках - Подготовка текста: числа словами, без markdown, ссылок и блоков кода - Модель Silero скачивается в LOCALAPPDATA и прогревается при старте - Состояние «говорю», переключатель озвучки в трее, «Стоп» прерывает речь - Тесты нарезки текста и оркестратора озвучки Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7933613234
commit
fec52dd1c2
@@ -0,0 +1,93 @@
|
||||
"""Синтез речи моделями 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
|
||||
Reference in New Issue
Block a user