initial commit (backend)
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
TTS Router — озвучка текста через Edge TTS.
|
||||
Бесплатный, качественный синтез речи для польского языка.
|
||||
Кэш хранится в файлах на диске.
|
||||
"""
|
||||
import logging
|
||||
import io
|
||||
import os
|
||||
import hashlib
|
||||
import edge_tts
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi.responses import Response
|
||||
from auth import require_auth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/tts", tags=["tts"])
|
||||
|
||||
# Голоса Edge TTS для польского
|
||||
VOICE_PL = "pl-PL-MarekNeural" # Мужской голос (основной)
|
||||
VOICE_PL_FEMALE = "pl-PL-AgnieszkaNeural" # Женский голос
|
||||
|
||||
# Директория для кэша
|
||||
CACHE_DIR = "/root/filwordspl/backend/tts_cache"
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def get_cache_path(voice_name: str, text: str) -> str:
|
||||
"""Создаёт путь к файлу кэша на основе хеша текста и голоса"""
|
||||
cache_key = hashlib.md5(f"{voice_name}:{text}".encode()).hexdigest()
|
||||
return os.path.join(CACHE_DIR, f"{cache_key}.mp3")
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def text_to_speech(request: Request, text: str = "", voice: str = "male"):
|
||||
"""
|
||||
Озвучивает текст через Edge TTS.
|
||||
|
||||
Параметры:
|
||||
- text: текст для озвучки
|
||||
- voice: male (Marek) или female (Agnieszka)
|
||||
|
||||
Возвращает: audio/mpeg
|
||||
"""
|
||||
await require_auth(request)
|
||||
|
||||
if not text or len(text) > 500:
|
||||
raise HTTPException(400, "Text is required (max 500 characters)")
|
||||
|
||||
# Выбираем голос
|
||||
if voice == "female":
|
||||
voice_name = VOICE_PL_FEMALE
|
||||
else:
|
||||
voice_name = VOICE_PL
|
||||
|
||||
# Путь к файлу кэша
|
||||
cache_path = get_cache_path(voice_name, text)
|
||||
|
||||
# Проверяем файловый кэш
|
||||
if os.path.exists(cache_path):
|
||||
logger.info(f"TTS: cache hit for text: {text[:50]}...")
|
||||
with open(cache_path, 'rb') as f:
|
||||
return Response(
|
||||
content=f.read(),
|
||||
media_type="audio/mpeg",
|
||||
)
|
||||
|
||||
try:
|
||||
logger.info(f"TTS: synthesizing text: {text[:50]}...")
|
||||
|
||||
# Синтезируем речь
|
||||
communicate = edge_tts.Communicate(text, voice_name)
|
||||
audio_data = io.BytesIO()
|
||||
|
||||
async for chunk in communicate.stream():
|
||||
if chunk["type"] == "audio":
|
||||
audio_data.write(chunk["data"])
|
||||
|
||||
audio_bytes = audio_data.getvalue()
|
||||
|
||||
if not audio_bytes:
|
||||
raise HTTPException(500, "TTS generation failed")
|
||||
|
||||
# Сохраняем в файловый кэш
|
||||
with open(cache_path, 'wb') as f:
|
||||
f.write(audio_bytes)
|
||||
|
||||
logger.info(f"TTS: done, size={len(audio_bytes)} bytes, cached to {os.path.basename(cache_path)}")
|
||||
|
||||
return Response(
|
||||
content=audio_bytes,
|
||||
media_type="audio/mpeg",
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"TTS error: {str(e)}")
|
||||
raise HTTPException(500, f"TTS error: {str(e)}")
|
||||
Reference in New Issue
Block a user