"""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= by loader.js — safe to cache for 1 year. # Section HTML uses ?v= (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")