Files
story-generator/silero-tts-service/app/pipeline.py
T

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()