import mimetypes import os import subprocess import threading import time from pathlib import Path from dotenv import load_dotenv load_dotenv(Path(__file__).resolve().parent / ".env") from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from core.config import GRADIO_SPACE_URL, logger from routers import auth, catalog, media, pages, segmentation, sessions, share from routers.catalog import seed_catalog from services.sam2_service import lifespan mimetypes.add_type("application/javascript", ".js", strict=True) mimetypes.add_type("text/css", ".css", strict=True) mimetypes.add_type("image/svg+xml", ".svg", strict=True) logger.info("[STARTUP] GRADIO_SPACE_URL=%s", GRADIO_SPACE_URL or "(not set — using local SAM2)") app = FastAPI(title="Hyper Reality Backend", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], ) @app.middleware("http") async def remove_x_frame_options(request: Request, call_next): response = await call_next(request) if "x-frame-options" in response.headers: del response.headers["x-frame-options"] response.headers["Content-Security-Policy"] = "frame-ancestors *" return response # Routers app.include_router(pages.router) app.include_router(auth.router) app.include_router(share.router) app.include_router(media.router) app.include_router(catalog.router) app.include_router(sessions.router) app.include_router(segmentation.router) # Static files BASE_DIR = Path(__file__).resolve().parent UPLOADS_DIR = BASE_DIR / "uploads" FRONTEND_DIST = BASE_DIR.parent / "frontend" / "dist" UPLOADS_DIR.mkdir(parents=True, exist_ok=True) app.mount("/uploads", StaticFiles(directory=UPLOADS_DIR), name="uploads") if (FRONTEND_DIST / "index.html").exists(): # Montado en "/" como catch-all para SPA — los routers de API tienen prioridad app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="frontend") # Frontend watcher (development helper) FRONTEND_DIR = BASE_DIR.parent / "frontend" FRONTEND_SRC = FRONTEND_DIR / "src" def scan_frontend_sources() -> dict: if not FRONTEND_SRC.exists(): return {} files = {} for path in FRONTEND_SRC.rglob("*"): if path.is_file() and path.suffix in {".ts", ".tsx", ".js", ".jsx", ".css", ".json", ".html"}: files[path] = path.stat().st_mtime for extra in [FRONTEND_DIR / "vite.config.ts", FRONTEND_DIR / "package.json", FRONTEND_DIR / "tsconfig.json"]: if extra.exists(): files[extra] = extra.stat().st_mtime return files def run_frontend_build() -> None: if not FRONTEND_DIR.exists(): return print("[backend] Ejecutando build del frontend...") result = subprocess.run(["npm", "run", "build"], cwd=str(FRONTEND_DIR), capture_output=True, text=True) if result.returncode != 0: print("[backend] Build falló:") print(result.stdout) print(result.stderr) else: print("[backend] Build completado correctamente.") def watch_frontend_changes(interval: float = 2.0) -> None: last_state = scan_frontend_sources() while True: time.sleep(interval) current_state = scan_frontend_sources() if current_state != last_state: if last_state: print("[backend] Cambio detectado en frontend. Reconstruyendo...") run_frontend_build() last_state = current_state @app.on_event("startup") async def startup_seed_catalog(): if MONGODB_URI := os.getenv("MONGODB_URI", ""): try: await seed_catalog() except Exception as exc: logger.warning("[STARTUP] seed_catalog falló: %s", exc) @app.on_event("startup") async def startup_watch_frontend(): thread = threading.Thread(target=watch_frontend_changes, daemon=True) thread.start() if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")