- FastAPI backend with JWT auth, roles (admin/moderator/user) - PDF upload (up to 500MB) with streaming, PyMuPDF text extraction - ChromaDB vectorization per page with metadata - LiteLLM AI question extraction from PDF (not generation) - Image extraction from PDF pages, graceful fallback - Quiz modes: timed (countdown timer) + learning (answers shown inline) - Page-by-page question navigation with dot navigator - TTS endpoint using LiteLLM (Google Vertex / OpenAI voices) - Admin dashboard: AI model management per task, user role management - Moderator role: upload PDFs, create sections, generate quizzes - Spaced repetition reminders via SMTP email (SM-2 intervals) - APScheduler daily reminder jobs - Celery + Redis for background PDF processing - React frontend with all pages - Docker Compose deployment (nginx + backend + celery + redis) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
4 KiB
Python
126 lines
4 KiB
Python
import os
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from app.config import settings
|
|
from app.database import engine, Base, SessionLocal
|
|
from app.routers import auth, documents, quizzes, attempts, admin, tts
|
|
from app.utils.auth import get_password_hash
|
|
from app.utils.scheduler import start_scheduler, stop_scheduler
|
|
|
|
|
|
def seed_admin():
|
|
"""Create default admin user if none exists."""
|
|
from app.models.user import User
|
|
db = SessionLocal()
|
|
try:
|
|
admin_exists = db.query(User).filter(User.role == "admin").first()
|
|
if not admin_exists:
|
|
admin_user = User(
|
|
email="admin@quizapp.com",
|
|
hashed_password=get_password_hash("admin123"),
|
|
name="Admin",
|
|
role="admin",
|
|
)
|
|
db.add(admin_user)
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def seed_default_models():
|
|
"""Seed default AI model configs if none exist."""
|
|
from app.models.ai_model_config import AIModelConfig
|
|
db = SessionLocal()
|
|
try:
|
|
if db.query(AIModelConfig).count() == 0:
|
|
defaults = [
|
|
AIModelConfig(
|
|
name="GPT-4o Mini (Extraction)",
|
|
model_id="gpt-4o-mini",
|
|
task="extraction",
|
|
is_active=True,
|
|
is_default=True,
|
|
),
|
|
AIModelConfig(
|
|
name="GPT-4o (Extraction)",
|
|
model_id="gpt-4o",
|
|
task="extraction",
|
|
is_active=True,
|
|
is_default=False,
|
|
),
|
|
AIModelConfig(
|
|
name="Claude Sonnet 4.6 (Extraction)",
|
|
model_id="anthropic/claude-sonnet-4-6-20250514",
|
|
task="extraction",
|
|
is_active=True,
|
|
is_default=False,
|
|
),
|
|
AIModelConfig(
|
|
name="Google Vertex TTS",
|
|
model_id="vertex_ai/google/cloud-tts",
|
|
task="tts",
|
|
is_active=True,
|
|
is_default=True,
|
|
),
|
|
AIModelConfig(
|
|
name="OpenAI TTS",
|
|
model_id="openai/tts-1",
|
|
task="tts",
|
|
is_active=True,
|
|
is_default=False,
|
|
),
|
|
]
|
|
db.add_all(defaults)
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Startup
|
|
Base.metadata.create_all(bind=engine)
|
|
os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
|
|
os.makedirs(os.path.join(settings.UPLOAD_DIR, "images"), exist_ok=True)
|
|
os.makedirs(settings.CHROMA_PERSIST_DIR, exist_ok=True)
|
|
seed_admin()
|
|
seed_default_models()
|
|
start_scheduler()
|
|
yield
|
|
# Shutdown
|
|
stop_scheduler()
|
|
|
|
|
|
app = FastAPI(
|
|
title="PDF Quiz Generator",
|
|
description="Convert PDF files into interactive quizzes with AI",
|
|
version="2.0.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["http://localhost:5173", "http://localhost:3000", "http://localhost"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Serve uploaded images as static files
|
|
app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIR), name="uploads")
|
|
|
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
|
app.include_router(documents.router, prefix="/api/documents", tags=["documents"])
|
|
app.include_router(quizzes.router, prefix="/api/quizzes", tags=["quizzes"])
|
|
app.include_router(attempts.router, prefix="/api/attempts", tags=["attempts"])
|
|
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])
|
|
app.include_router(tts.router, prefix="/api/tts", tags=["tts"])
|
|
|
|
|
|
@app.get("/api/health")
|
|
def health_check():
|
|
return {"status": "ok"}
|