Files
speech/app/main.py
du5t acdf098c41 Add XTTS-v2 as first TTS backend, extensible for more
Mirrors the ASR backend pattern exactly: a BACKENDS dict in
tts/router.py maps a backend name to its worker URL, so adding the
next backend is just a new worker file + venv + supervisord entry +
one dict line. XTTS-v2 runs in its own venv (--system-site-packages,
inherits base image torch/CUDA) as a new supervisord program on
port 8005.

XTTS is zero-shot voice cloning, so a reference-voice library was
added (/tts/voices CRUD, stored under /srv/tts/voices) — synthesis
requires picking a previously uploaded voice. Results and model
cache live under /srv/tts/{results,models-cache}, new quadlet
volumes, owned by the same 983:983 user as the existing /srv/asr
dirs.

Fixed two environment issues uncovered while getting XTTS to
actually run inside the container (non-root user, root-built venvs):
- coqui-tts only pins transformers>=4.57 with no ceiling, so pip
  installed an incompatible 5.x; pinned to the last 4.x release.
- HOME defaults to /app (owned by root) for the container's runtime
  user, so numba/matplotlib/torch cache writes failed; HOME is now
  forced to /tmp in all three workers (faster_whisper, qwen3, xtts)
  before any of those libraries get imported.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:46:45 +09:00

64 lines
2.2 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware
from asr.config import ensure_runtime_dirs as ensure_asr_dirs
from asr.router import router as asr_router
from asr.router import ws_router as asr_ws_router
from core.auth import SESSION_SECRET_KEY, require_login
from core.auth import router as auth_router
from tts.config import ensure_runtime_dirs as ensure_tts_dirs
from tts.router import router as tts_router
app = FastAPI(title="Speech Gateway", docs_url="/docs", redoc_url="/redoc")
app.add_middleware(
SessionMiddleware,
secret_key=SESSION_SECRET_KEY,
same_site="lax",
https_only=True, # speech.du5t.xyz는 NPM에서 TLS 종료 후 내부로 포워딩됨
max_age=14 * 24 * 3600,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
UI_DIR = Path("/app/ui")
app.include_router(auth_router, prefix="/auth", tags=["auth"])
app.include_router(asr_router, prefix="/asr", tags=["asr"], dependencies=[Depends(require_login)])
app.include_router(asr_ws_router, prefix="/asr", tags=["asr"]) # 인증은 핸들러 내부에서 세션으로 직접 체크
app.include_router(tts_router, prefix="/tts", tags=["tts"], dependencies=[Depends(require_login)])
@app.on_event("startup")
def startup() -> None:
ensure_asr_dirs()
ensure_tts_dirs()
if UI_DIR.exists():
app.mount("/ui", StaticFiles(directory=str(UI_DIR), html=True), name="ui")
app.mount("/assets", StaticFiles(directory=str(UI_DIR / "assets")), name="assets")
@app.get("/health")
def health() -> Dict[str, Any]:
return {"status": "ok", "ui_installed": UI_DIR.exists(), "api": "/docs"}
@app.get("/", response_class=HTMLResponse, dependencies=[Depends(require_login)])
def root() -> Any:
index = UI_DIR / "index.html"
if index.exists():
return FileResponse(index)
return JSONResponse({"status": "ok", "message": "UI not installed", "api": "/docs"})