initial: silero-tts-service + README
This commit is contained in:
1 parent
8c60c25ad2
commit
984b1fec6d
18 files changed
+2080
No files matched your search
Executable
+95
@@ -0,0 +1,95 @@
|
||||
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()
|
||||
Reference in new issue
Block a user