File size: 5,054 Bytes
2d3a0d2
6594ddb
 
 
 
 
 
 
954e0aa
 
 
b65231d
9ab1911
962a395
8c7a11b
962a395
 
 
3b6b3bd
962a395
8c7a11b
6f77435
6ee4b4a
 
954e0aa
b65231d
 
 
28227c9
 
 
 
 
 
 
 
 
37bcb58
 
 
 
 
 
28227c9
2d3a0d2
 
 
 
 
 
7b90b65
954e0aa
373441d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f77435
b65231d
 
962a395
93ff956
ee6424d
 
962a395
 
 
 
ee6424d
962a395
 
463e7aa
 
 
 
 
 
 
 
 
1f8bc1c
 
 
 
 
954e0aa
 
 
1f8bc1c
 
 
 
954e0aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f8bc1c
 
463e7aa
1f8bc1c
962a395
 
463e7aa
962a395
2d3a0d2
 
962a395
 
 
 
 
3b6b3bd
962a395
8c7a11b
6f77435
8c7a11b
 
 
 
 
962a395
 
 
 
 
6ee4b4a
 
 
 
 
6594ddb
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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"<green>{status}</green>"
    elif 300 <= status < 400:
        status_str = f"<cyan>{status}</cyan>"
    elif 400 <= status < 500:
        status_str = f"<red>{status}</red>"
    else:
        status_str = f"<bold><red>{status}</red></bold>"

    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)