Spaces:
Sleeping
Sleeping
| """ | |
| backend/main.py | |
| βββββββββββββββββββββββββββββββββββββββββββββ | |
| FastAPI application β Rift Breakdown Backend | |
| Carga los 3 modelos ML al startup y expone endpoints | |
| para predicciΓ³n de win probability minuto-a-minuto. | |
| βββββββββββββββββββββββββββββββββββββββββββββ | |
| """ | |
| import logging | |
| import os | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from fastapi import FastAPI | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from backend.routers import predictions | |
| from backend.services.model_service import model_service | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Logging | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s | %(name)s | %(levelname)s | %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Lifespan (startup / shutdown) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| async def lifespan(app: FastAPI): | |
| """Load models once on startup, release on shutdown.""" | |
| logger.info("π Starting Rift Breakdown backendβ¦") | |
| try: | |
| model_service.load_models() | |
| logger.info("β All models loaded β server ready.") | |
| except Exception as e: | |
| logger.error("β Model loading failed: %s", e, exc_info=True) | |
| # Server starts anyway but /predict will return 503 | |
| yield | |
| logger.info("π Shutting down Rift Breakdown backend.") | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # App | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI( | |
| title="Rift Breakdown β LoL Win Probability Predictor", | |
| version="0.2.0", | |
| description="Post-match analysis with 3 ML models (XGBoost, LSTM, LogReg)", | |
| lifespan=lifespan, | |
| # Docs bajo /api para no colisionar con React SPA | |
| docs_url="/api/docs", | |
| redoc_url="/api/redoc", | |
| openapi_url="/api/openapi.json", | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # CORS (Secured for Production) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| allowed_origins_env = os.environ.get( | |
| "ALLOWED_ORIGINS", | |
| "http://localhost:5173,http://127.0.0.1:5173,http://localhost:3000,https://huggingface.co" | |
| ) | |
| origins = [origin.strip() for origin in allowed_origins_env.split(",") if origin.strip()] | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=origins, | |
| allow_credentials=True, | |
| allow_methods=["GET", "POST", "OPTIONS"], | |
| allow_headers=["Content-Type", "Authorization", "Accept"], | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Routers | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| app.include_router(predictions.router, prefix="/api/v1", tags=["predictions"]) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # Health check | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def health_check(): | |
| return { | |
| "status": "ok", | |
| "version": "0.2.0", | |
| "models_loaded": model_service.is_loaded, | |
| } | |
| DIST = Path(__file__).parent.parent / "frontend" / "dist" | |
| if DIST.exists(): | |
| # Archivos estΓ‘ticos (JS, CSS, imΓ‘genes) | |
| app.mount("/assets", StaticFiles(directory=DIST / "assets"), name="assets") | |
| def serve_spa(full_path: str): | |
| """Catch-all: cualquier ruta no-API sirve el index.html de React.""" | |
| return FileResponse(DIST / "index.html") | |