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>
64 lines
2.2 KiB
Python
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"})
|