182 lines
5.5 KiB
Python
Executable File
182 lines
5.5 KiB
Python
Executable File
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()
|