initial: silero-tts-service + README

This commit is contained in:
Никита Тарасов committed 2026-08-24 21:28:44 +05:00
1 parent 8c60c25ad2
commit 984b1fec6d
18 files changed
+2080

No files matched your search

+181
View File
@@ -0,0 +1,181 @@
import logging
import signal
import sys
import time
from contextlib import asynccontextmanager
from typing import Optional
import torch
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from slowapi import Limiter
from .api import router as tts_router, SPEAKERS
from .config import Settings
from .engine import load_models, get_model
from .metrics import limiter, _synthesis_latency, _audio_size_bytes, _errors_total, _request_counter
logger = logging.getLogger(__name__)
# ─── Глобальный state ─────────────────────────────────────────────
config: Optional[Settings] = None
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Управление жизненным циклом приложения"""
global config
cfg = app.state.config
logger.info("=" * 60)
logger.info("🚀 Запуск Silero TTS API")
logger.info("=" * 60)
# Показываем конфигурацию
logger.info("Конфигурация:")
logger.info(f" Модель: {cfg.model}")
logger.info(f" Устройство: {cfg.device}")
logger.info(f" Sample rate: {cfg.sample_rate} Hz")
logger.info(f" Голоса: {', '.join(SPEAKERS.keys())}")
logger.info(f" CORS: {cfg.cors_origins}")
logger.info(f" Логирование: {cfg.log_level}")
logger.info("-" * 60)
# Загрузка моделей
load_models(cfg)
logger.info("✅ ТTS API готов к работе")
logger.info("=" * 60)
yield # ← сервер работает
# Graceful shutdown
logger.info("🛑 Остановка сервиса...")
# Освобождаем GPU память
if cfg.device.startswith("cuda"):
torch.cuda.empty_cache()
logger.info("👋 ТTS API остановлен")
# ─── Создание приложения ──────────────────────────────────────────
def create_app() -> FastAPI:
global config
cfg = Settings()
config = cfg
app = FastAPI(
title="Silero TTS API",
version="0.2.0",
description="REST API синтеза русской речи на Silero v5.5",
lifespan=lifespan,
)
# ── Настройка логгера ──
logging.basicConfig(
level=getattr(logging, cfg.log_level.upper(), logging.INFO),
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%H:%M:%S",
handlers=[logging.StreamHandler(sys.stdout)],
)
# ── Конфиг в app.state ──
app.state.config = cfg
# ── SlowAPI лимитер ──
app.state.limiter = limiter
# ── SlowAPI middleware ──
from slowapi.errors import RateLimitExceeded
from starlette.responses import JSONResponse
@app.exception_handler(RateLimitExceeded)
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
return JSONResponse(
status_code=429,
content={
"detail": "Превышен лимит запросов. Попробуйте позже."
},
)
# ── CORS middleware ──
app.add_middleware(
CORSMiddleware,
allow_origins=cfg.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── Request logging middleware ──
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration_ms = (time.perf_counter() - start) * 1000
logger.info(
"%s %s → %s (%.1f ms)",
request.method,
request.url.path,
response.status_code,
duration_ms,
)
# ── Prometheus метрика ──
if request.url.path.startswith("/tts"):
_request_counter.labels(
endpoint=request.url.path,
status=str(response.status_code),
).inc()
return response
# ── Prometheus /metrics endpoint ──
@app.get("/metrics", tags=["monitoring"])
async def metrics(request: Request):
return Response(
content=generate_latest(),
media_type=CONTENT_TYPE_LATEST,
)
# ── Health check (до загрузки моделей) ──
@app.get("/health", tags=["metadata"])
async def health():
try:
get_model()
return {"status": "healthy", "model_loaded": True}
except Exception as e:
return JSONResponse(
status_code=503,
content={"status": "unhealthy", "error": str(e)},
)
# ── Роуты TTS ──
app.include_router(tts_router)
return app
# ─── Точка входа ──────────────────────────────────────────────────
def main():
"""Запуск через uvicorn"""
import uvicorn
app = create_app()
uvicorn.run(
app,
host="0.0.0.0",
port=8001,
log_level="info",
)
if __name__ == "__main__":
main()