import os import sys current_dir = os.path.dirname(os.path.abspath(__file__)) parent_dir = os.path.dirname(current_dir) if parent_dir not in sys.path: sys.path.insert(0, parent_dir) from src.logger import setup_logging, logger setup_logging() from contextlib import asynccontextmanager from fastapi import FastAPI, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse from src.materials.routes import router as materials_router from src.summary_generator.routes import router as summary_router from src.rag.routes import router as tutor_router, ws_router from src.quiz_generator.routes import router as quiz_router from src.auth.routes import router as auth_router from src.asr.routes import router as asr_router from src.store import get_usage from src.dependencies import get_current_user_id from src.config import settings @asynccontextmanager async def lifespan(app: FastAPI): # Run pending Alembic database migrations automatically on server startup try: import subprocess logger.info("Running database migrations via Alembic...") subprocess.run(["alembic", "upgrade", "head"], check=True) logger.info("Database migrations completed successfully.") except Exception as e: logger.warning(f"Database migration step failed or skipped: {e}") try: from src.database import warmup_database warmup_database() except Exception as e: logger.warning(f"Database warmup failed: {e}") try: from src.rag.rag import get_embedder get_embedder() logger.info("Embedder loaded successfully.") except Exception as e: logger.warning(f"Embedder failed to load: {e}") # Eagerly load ASR models so warmup runs at startup, not on first request try: from src.asr.models import get_audio_model_en get_audio_model_en() except Exception as e: logger.warning(f"English ASR model failed to load: {e}") try: from src.asr.models import get_audio_model_ar get_audio_model_ar() except Exception as e: logger.warning(f"Arabic ASR model failed to load: {e}") from src.rag.batch_workers import start_workers start_workers() from src.asr.batch_workers import start_asr_workers start_asr_workers() yield from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware app = FastAPI( title="AI Tutor API", description="Backend API for the AI Tutor for Students application", version="1.0.0", lifespan=lifespan, ) # When allow_credentials=True, browsers REJECT responses with "Access-Control-Allow-Origin: *" # and refuse to store or send cookies. We must always use explicit origins. _DEFAULT_ORIGINS = [ "https://www.studybuddyai.dev", "https://studybuddyai.dev", "https://hamdy005-study-buddy.hf.space", "http://localhost:3000", "http://localhost:3001", ] _raw_origins = settings.cors_allowed_origins if settings.cors_allowed_origins else _DEFAULT_ORIGINS # Remove '*' if present to avoid browser credential rejection _cors_origins = [o.strip() for o in _raw_origins if o.strip() and o.strip() != "*"] or _DEFAULT_ORIGINS @app.middleware("http") async def log_request_timing(request, call_next): import time start = time.perf_counter() # Fix double slashes in paths (e.g., //api/usage -> /api/usage) path = request.scope.get("path") if path and "//" in path: request.scope["path"] = path.replace("//", "/") response = await call_next(request) duration_sec = time.perf_counter() - start status = response.status_code if 200 <= status < 300: status_str = f"{status}" elif 300 <= status < 400: status_str = f"{status}" elif 400 <= status < 500: status_str = f"{status}" else: status_str = f"{status}" logger.opt(colors=True).info(f"{request.method} {request.url.path} - {status_str} ({duration_sec:.2f}s)") return response app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*") # CORSMiddleware MUST be added LAST so it becomes the outermost layer in Starlette's middleware stack. app.add_middleware( CORSMiddleware, allow_origins=_cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(materials_router) app.include_router(summary_router) app.include_router(tutor_router) app.include_router(ws_router) app.include_router(quiz_router) app.include_router(auth_router) app.include_router(asr_router) @app.get("/") async def root(): return RedirectResponse(url="/docs") @app.get("/api/health") async def health_check(): return {"status": "ok", "service": "AI Tutor API"} @app.get("/api/usage") async def get_user_usage(user_id: str = Depends(get_current_user_id)): return get_usage(user_id) if __name__ == "__main__": import uvicorn uvicorn.run("src.main:app", host="0.0.0.0", port=8000, reload=True)