import asyncio import hashlib import io import logging import wave from functools import lru_cache from typing import Optional import numpy as np import torch from .engine import get_model, get_accentor logger = logging.getLogger(__name__) # ─── LRU-кэш ответов ─────────────────────────────────────────────── # Кэшируем по хэшу текста + голоса. Убираем из кэша, если модель # переключили — для этого вызывается reset_cache(). @lru_cache(maxsize=1024) def _cached_tts(text_hash: str, speaker: str, sample_rate: int) -> bytes: """Кэш синтеза: текст_хэш + голос → WAV bytes""" # Этот вызов НЕ должен происходить, только для типов raise RuntimeError("Кэш пуст — нужно синтезировать") def reset_cache() -> None: """Очистить кэш (вызывать при смене модели/настроек)""" _cached_tts.cache_clear() # ─── Основной пайплайн ───────────────────────────────────────────── async def process_tts( text: str, speaker: str, sample_rate: int, cache_ttl: int = 604800, cache_enabled: bool = True, ) -> bytes: """ Пайплайн синтеза: 1. Хэш текста (для кэша) 2. Генерация аудио (apply_tts встроенный accent/yo) 3. tensor → WAV bytes """ # 1. Хэш текста text_hash = hashlib.md5(text.encode("utf-8")).hexdigest() # 2. Синтез (Silero apply_tts встроенный accent/yo) model, device = get_model() def _synthesize_sync(): return model.apply_tts( text=text, speaker=speaker, sample_rate=sample_rate, put_accent=True, put_yo=True, ) loop = asyncio.get_event_loop() audio_tensor = await loop.run_in_executor(None, _synthesize_sync) # 4. tensor → WAV audio_np = audio_tensor.cpu().numpy() wav_bytes = tensor_to_wav(audio_np, sample_rate) logger.debug( "Синтез завершён", text_len=len(text), wav_size=len(wav_bytes), device=str(device), ) return wav_bytes # ─── Утилиты ─────────────────────────────────────────────────────── def tensor_to_wav(audio_np: np.ndarray, sample_rate: int) -> bytes: """Конвертация numpy array в WAV bytes (16-bit mono)""" audio_int16 = (audio_np * 32767).astype(np.int16) buffer = io.BytesIO() with wave.open(buffer, "wb") as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) # 16-bit wav_file.setframerate(sample_rate) wav_file.writeframes(audio_int16.tobytes()) buffer.seek(0) return buffer.getvalue()