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
+181
@@ -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()
|
||||
Reference in new issue
Block a user