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