|
|
||
|---|---|---|
| backend | ||
| frontend | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
PedQuiz
AI-powered pediatric knowledge quiz platform. Upload PDF study materials, automatically extract MCQ questions with AI, and take quizzes with text-to-speech support and semantic search.
Features
- PDF → Quiz: Upload PREP PDFs, AI extracts questions, answers, and explanations
- Quiz Modes: Study (instant feedback) and Exam (timed, scored)
- Text-to-Speech: OpenAI TTS, AWS Polly, ElevenLabs, Google Cloud — voice selection per quiz
- Semantic Search: pgvector embeddings — finds questions by meaning, not just keywords
- Embedding Model: Configurable via Admin UI or env — supports any LiteLLM proxy model or AWS Bedrock
- Job Cancellation: Cancel running extractions from the web UI
- Email Verification: Required before first login; password reset via email
- Role system: Admin / Moderator / User
- Nextcloud Integration: Browse and import PDFs from your Nextcloud
- Themes: Default and Warm Brown
Stack
| Layer | Tech |
|---|---|
| Frontend | React + React Router, plain CSS, Nginx |
| Backend | FastAPI, SQLAlchemy, PostgreSQL 16 + pgvector |
| AI/LLM | LiteLLM proxy (Claude, Gemini, GPT, and more) |
| Embeddings | Configurable — any LiteLLM proxy model or direct AWS Bedrock (1024-dim, set via Admin UI or env) |
| Document vectors | ChromaDB |
| TTS | OpenAI (direct), AWS Polly, ElevenLabs, Google Cloud TTS |
| Queue | Celery + Redis |
| SMTP (smtp2go or any SMTP server) |
Quick Start
git clone https://github.com/ifedan-ed/pdf-quiz-generator.git
cd pdf-quiz-generator
# Configure environment
cp backend/.env.example backend/.env # edit with your keys
docker compose up -d
Frontend available at http://localhost:8081. The first registered user becomes admin automatically.
Environment Variables
Create backend/.env:
# Database
DATABASE_URL=postgresql://pedquiz:<password>@postgres:5432/pedquiz
SECRET_KEY=<random-32-char-string>
# Redis
REDIS_URL=redis://redis:6379/0
# LLM — for question extraction (requires LiteLLM proxy or direct OpenAI)
LITELLM_MODEL=openai/claude-haiku-4.5 # prefix with openai/ when using proxy
LITELLM_API_KEY=<your-litellm-or-openai-key>
LITELLM_API_BASE=https://your-litellm-proxy.com # leave empty for direct OpenAI
# Embedding model — use the model name exactly as your proxy lists it (no prefix needed)
# Can also be changed live via Admin → More settings without redeploying
LITELLM_EMBEDDING_MODEL=ge-gemini-embedding-001
# OpenAI (for TTS — calls api.openai.com directly, not the proxy)
OPENAI_API_KEY=<openai-api-key>
# AWS (for Polly TTS + optional direct Bedrock embedding fallback)
AWS_ACCESS_KEY_ID=<key>
AWS_SECRET_ACCESS_KEY=<secret>
AWS_REGION=us-east-1
AWS_BEDROCK_REGION=us-east-1
# ElevenLabs TTS (optional)
ELEVENLABS_API_KEY=<key>
# Google Cloud TTS (optional)
GOOGLE_TTS_API_KEY=<key>
# Email
MAIL_SERVER=mail.smtp2go.com
MAIL_PORT=587
MAIL_USERNAME=<smtp2go-username>
MAIL_PASSWORD=<smtp2go-password>
MAIL_FROM=noreply@yourdomain.com
MAIL_STARTTLS=true
# App
APP_URL=https://your-domain.com
UPLOAD_DIR=/app/uploads
MAX_UPLOAD_SIZE=524288000
CHROMA_PERSIST_DIR=/app/chroma_data
Rebuild & Restart
Frontend and backend are built into Docker images — code changes require a build before they take effect. .env changes only need a restart.
# Rebuild and restart everything
docker compose build && docker compose up -d
# Rebuild a single service
docker compose build backend && docker compose up -d backend
docker compose build frontend && docker compose up -d frontend
docker compose build celery && docker compose up -d celery
# Restart without rebuilding (for .env changes only)
docker compose restart backend
# View logs
docker compose logs backend --tail=50
docker compose logs celery --tail=50
Admin Dashboard
Accessible at /admin for admin users. Three tabs:
AI Models
- Search models from your LiteLLM proxy — click any result to pre-fill the add form
- Configure models per task:
extraction(PDF → questions),tts(voices),general - Set a default model per task; enable/disable individual models
- Extraction models from the proxy don't need an
openai/prefix — the backend adds it automatically
Users
- Create users directly (email auto-verified)
- Change user roles: admin / moderator / user
More Settings
- Public Registration — enable/disable new user sign-ups
- AWS Polly — global enable/disable toggle for all Polly voices (individual voices still manageable in AI Models tab)
- Embedding Model — set the model used for semantic search vectors:
- Type a model name and click Save, or click Search LiteLLM to browse proxy models
- Click Test to verify the model works and returns the correct dimensions (must be 1024)
- Setting is stored in Redis and takes effect immediately — no restart needed
- Priority: Admin UI setting →
LITELLM_EMBEDDING_MODELenv → direct AWS Bedrock fallback - Model name should match exactly what your proxy lists (no prefix required)
Switching embedding models
To switch to a different embedding model:
- Go to Admin → More → click Search LiteLLM, find your model, click Select
- Click Test to confirm it returns 1024 dimensions
- Run
docker compose exec backend python manage.py reembedto regenerate all existing question embeddings with the new model
To revert to AWS Bedrock (when available):
- Set model to the Bedrock model ID as configured on your proxy, or
- Clear the Redis key to fall back to env:
docker compose exec redis redis-cli DEL settings:embedding_model - Then set
LITELLM_EMBEDDING_MODEL=to empty in.envto use direct Bedrock
CLI Management
# ── User management ──────────────────────────────────────────────────────────
docker compose exec backend python manage.py reset-password admin@example.com NewPassword123
docker compose exec backend python manage.py list-users
# ── Quiz extraction ───────────────────────────────────────────────────────────
# 1. Find document and section IDs
docker compose exec backend python manage.py list-sections
# 2a. Extract in background (monitor via navbar jobs badge)
docker compose exec backend python manage.py extract 6 --bg
docker compose exec backend python manage.py extract 6 --bg --title "PREP 2012 Full" --mode timed
# 2b. Extract inline (blocking, live output in terminal)
docker compose exec backend python manage.py extract 6
# 3. Check job status
docker compose exec backend python manage.py jobs
docker compose exec backend python manage.py jobs --user admin@example.com
# CLI extract options:
# --title "My Quiz" Custom quiz title
# --mode timed|learning Quiz mode (default: timed)
# --user email Owner of the quiz (default: first admin)
# --bg Run in background via Celery
# ── Embeddings ───────────────────────────────────────────────────────────────
# Regenerate all question embeddings (run after switching embedding model)
docker compose exec backend python manage.py reembed
How extraction works
- Upload PDF via the web UI — text is extracted and stored
- Create a section on the document page (define page range)
- Extract quiz from the web UI or CLI:
- Large sections are split into 50-page chunks automatically
- Progress shown live in the web UI; running jobs can be cancelled from the Extraction Jobs page
- Supports multiple extraction modes: standard, two-step (separate answer key), questions-only, regex, AI-decides
- Questions land in the Question Bank and can be assigned to categories
Architecture
Browser
│
▼
Nginx (frontend + API proxy)
├─► React SPA (static)
└─► FastAPI backend
├─ PostgreSQL (pgvector) ← users, quizzes, questions + 1024-dim embeddings
├─ ChromaDB ← document page chunks for semantic search
├─ Redis ← Celery queue + runtime settings (embedding model, toggles)
├─ Celery workers ← background PDF processing, emails
├─ LiteLLM proxy ← Claude/Gemini/GPT for extraction; embedding models
├─ AWS Bedrock ← Polly TTS; embedding fallback
└─ OpenAI ← TTS (direct, not via proxy)
Embeddings
Question embeddings are 1024-dimensional vectors stored in PostgreSQL via pgvector with an HNSW index for fast cosine similarity search. The embedding model is called directly via the LiteLLM proxy HTTP API (not the Python library) so the dimensions=1024 parameter is forwarded correctly.
Priority chain:
- Model set in Admin UI (stored in Redis
settings:embedding_model) LITELLM_EMBEDDING_MODELenv var- Direct AWS Bedrock (
amazon.titan-embed-text-v2:0) — automatic fallback if AWS credentials are present
Search
Quiz search uses hybrid retrieval:
- Semantic — embed the query, cosine similarity via pgvector HNSW index
- Keyword — PostgreSQL
ILIKEon question text and options - Results merged — semantic matches ranked first by score, keyword-only matches appended
TTS Providers
| Provider | Model ID format | Key needed |
|---|---|---|
| OpenAI | tts-1:alloy, tts-1:nova, tts-1:echo, tts-1:shimmer, tts-1:onyx, tts-1:fable, tts-1-hd:* |
OPENAI_API_KEY (calls api.openai.com directly) |
| AWS Polly | polly/Joanna, polly/Matthew, polly/Amy, polly/Brian |
AWS_ACCESS_KEY_ID + polly:SynthesizeSpeech IAM permission |
| ElevenLabs | elevenlabs/<voice-id> |
ELEVENLABS_API_KEY |
| Google Cloud | google/<voice-name> e.g. google/en-US-Wavenet-D |
GOOGLE_TTS_API_KEY |
AWS Polly can be globally disabled/enabled via Admin → More settings without removing the individual voice configs.
Project Structure
├── backend/
│ ├── app/
│ │ ├── main.py # App startup, seeding, pgvector setup
│ │ ├── config.py # Settings (pydantic-settings, reads .env)
│ │ ├── models/ # SQLAlchemy ORM models
│ │ ├── routers/ # FastAPI route handlers
│ │ ├── services/
│ │ │ ├── ai_service.py # LLM extraction + TTS routing
│ │ │ ├── embedding_service.py # pgvector embeddings (httpx → proxy)
│ │ │ ├── vector_service.py # ChromaDB document page chunks
│ │ │ ├── quiz_service.py # Quiz creation pipeline
│ │ │ └── email_service.py # Email templates + sending
│ │ ├── tasks/
│ │ │ ├── quiz_tasks.py # Celery: PDF extraction with cancellation support
│ │ │ └── pdf_tasks.py # Celery: PDF text extraction
│ │ └── utils/
│ ├── manage.py # CLI: reset-password, list-users, reembed
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── pages/ # All page components
│ │ ├── components/ # Navbar (with jobs badge), shared UI
│ │ └── context/ # AuthContext, ThemeContext
│ ├── nginx.conf
│ └── Dockerfile
└── docker-compose.yml
Deployment Notes
- The frontend Nginx binds to
127.0.0.1:8081— put Caddy or Nginx in front for HTTPS - PostgreSQL data persists in the
postgres_dataDocker volume — back it up regularly - Uploads live in the
uploads_datavolume — includes extracted question images - Redis data persists in
redis_datavolume — holds runtime settings and job state - Set
APP_URLto your public domain so email verification and password reset links work