tts-voice-creator-clone-and.../server.py
mARTin-B78 40e42590cc Release v1.6.0: a11y (WCAG AA), i18n (DE), PWA, perf, tests, Cast UX
Cast: card/list views, sort & filter, online voice picker, "Hear a line"
sample button, AI character notes, import auto-save.

Platform: WCAG 2.1 AA accessibility pass; German UI translation + language
picker; installable PWA with offline shell; GZip + content-visibility
virtualization + lazy images + Rehearser PCM memory cap (mobile stability);
Playwright suite (desktop + iPhone); opt-in minified bundle build.

Fixes: screenplay parser false characters; Fish-Speech inline-tag tones;
narrator/voice pickers list full library; clone GUI rework; fish.audio
import dedup; voice-ID rename; bulk-delete modal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:23:35 +02:00

97 lines
4.6 KiB
Python

"""TTS Voice Creator - Clone and Design — FastAPI entry point (thin wrapper)."""
from __future__ import annotations
import logging
import os
from logging.handlers import RotatingFileHandler
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import Response
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.gzip import GZipMiddleware
from core.constants import STATIC_DIR, _BufferHandler
from routes import admin, settings, library, stt, sources, docker, tts, conversation
logger = logging.getLogger("uvicorn.error")
# ── App ───────────────────────────────────────────────────────────────────────
app = FastAPI(title="TTS Voice Creator - Clone and Design")
# ── Response compression ──────────────────────────────────────────────────────
# Gzip text assets (JS/CSS/HTML/JSON) above 1 KB — ~75% smaller transfer for the
# large JS bundle and voice-list JSON. Skips already-compressed audio/images.
app.add_middleware(GZipMiddleware, minimum_size=1024)
# ── In-memory log buffer handler ──────────────────────────────────────────────
_buf_handler = _BufferHandler()
_buf_handler.setLevel(logging.DEBUG)
logging.getLogger().addHandler(_buf_handler)
# ── File log handler (rotates at 5 MB, keeps 3 files) ────────────────────────
_LOG_DIR = Path(os.environ.get("LOG_DIR", "/logs"))
try:
_LOG_DIR.mkdir(parents=True, exist_ok=True)
_file_handler = RotatingFileHandler(
_LOG_DIR / "app.log",
maxBytes=5 * 1024 * 1024,
backupCount=3,
encoding="utf-8",
)
_file_handler.setLevel(logging.INFO)
_file_handler.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)-8s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
))
logging.getLogger().addHandler(_file_handler)
except Exception as _log_err:
logger.warning("Could not set up file logging at %s: %s", _LOG_DIR, _log_err)
# ── Static-asset caching middleware ──────────────────────────────────────────
# JS and CSS are served with ?v=<version> by loader.js — safe to cache for 1 year.
# Section HTML uses ?v=<timestamp> (always fresh) so no long-term cache there.
@app.middleware("http")
async def static_cache_headers(request: Request, call_next):
response: Response = await call_next(request)
path = request.url.path
has_version = bool(request.query_params.get("v"))
if has_version and (path.startswith("/static/js/") or path.endswith(".css")):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
elif path.startswith("/static/sections/"):
response.headers["Cache-Control"] = "no-store"
elif path.startswith("/static/vendor/"):
response.headers["Cache-Control"] = "public, max-age=86400"
elif path.startswith("/static/") and not has_version:
# Bootstrap files (style.css, loader.js, nav.js, …) are loaded without a ?v= query.
# Revalidate on every load so GUI edits appear after a normal reload — the server
# returns 304 when the file is unchanged, so this stays cheap.
response.headers["Cache-Control"] = "no-cache"
return response
# ── Routers ───────────────────────────────────────────────────────────────────
app.include_router(admin.router)
app.include_router(settings.router)
app.include_router(library.router)
app.include_router(stt.router)
app.include_router(sources.router)
app.include_router(docker.router)
app.include_router(tts.router)
app.include_router(conversation.router)
# ── Static files ──────────────────────────────────────────────────────────────
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# ── Dev runner ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7890, log_level="info")