96 lines
3.1 KiB
Python
Executable File
96 lines
3.1 KiB
Python
Executable File
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()
|