pdf-quiz-generator/backend/app/main.py
ifedan-ed b876f13fac Initial commit: PDF Quiz Generator app
- 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>
2026-03-30 20:04:53 +00:00

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"}