Two-phase extraction improvements: - Auto-detect answer section boundary by scanning in 10-page steps for 'Preferred Response:' — finds exact page where questions end and answers begin (PREP 2013 answers start at page ~68, not at the end of the file) - Restrict Phase 1 question chunks to pages BEFORE the answer section - Extract answer key from answer section in CHUNKS (50 pages each) to handle large answer sections — accumulates all item→letter mappings - Previous version used last 40% which missed items 1-~135 for PREP 2013 README: full CLI extraction documentation: - list-sections: find document and section IDs - extract <section_id> [--bg] [--title] [--mode] [--user] - jobs / jobs --user <email> - Explanation of auto-format detection (inline vs separate answer key) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
215 lines
8.6 KiB
Markdown
215 lines
8.6 KiB
Markdown
# 🩺 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 — voice selection per quiz
|
||
- **Semantic Search**: pgvector + AWS Titan Embed — finds questions by meaning, not just keywords
|
||
- **Email Verification**: Required before first login; password reset via email
|
||
- **Role system**: Admin / Moderator / User
|
||
- **Nextcloud Integration**: Browse and import PDFs from your Nextcloud in Upload page
|
||
- **Themes**: Default and Markdown (GitHub-style)
|
||
|
||
## Stack
|
||
|
||
| Layer | Tech |
|
||
|---|---|
|
||
| Frontend | React + React Router, plain CSS, Nginx |
|
||
| Backend | FastAPI, SQLAlchemy, PostgreSQL 16 + pgvector |
|
||
| AI/LLM | LiteLLM proxy (Claude, Gemini, GPT, Bedrock) |
|
||
| Embeddings | AWS Bedrock Titan Embed V2 via LiteLLM proxy |
|
||
| Document vectors | ChromaDB |
|
||
| TTS | OpenAI, AWS Polly, ElevenLabs |
|
||
| Queue | Celery + Redis |
|
||
| Email | SMTP (smtp2go) |
|
||
|
||
## Quick Start
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```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)
|
||
LITELLM_MODEL=openai/claude-haiku-4.5
|
||
LITELLM_API_KEY=<your-litellm-or-openai-key>
|
||
LITELLM_API_BASE=https://your-litellm-proxy.com # or leave empty for direct OpenAI
|
||
LITELLM_EMBEDDING_MODEL=openai/titan-embed-v2
|
||
|
||
# OpenAI (for TTS — uses api.openai.com directly, not proxy)
|
||
OPENAI_API_KEY=<openai-api-key>
|
||
|
||
# AWS (for Polly TTS + 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
|
||
```
|
||
|
||
## CLI Management
|
||
|
||
All commands run inside the backend container:
|
||
|
||
```bash
|
||
# ── User management ──────────────────────────────────────────────────────────
|
||
# Reset a locked-out admin password
|
||
docker compose exec backend python manage.py reset-password admin@example.com NewPassword123
|
||
|
||
# List all users with email verification status
|
||
docker compose exec backend python manage.py list-users
|
||
|
||
# ── Quiz extraction ───────────────────────────────────────────────────────────
|
||
# 1. Find your document ID and section ID
|
||
docker compose exec backend python manage.py list-sections
|
||
|
||
# Output example:
|
||
# Doc 3: prep-PREP2012.pdf (ready, 767 pages)
|
||
# Section 6: 'ALL' pages 1–767
|
||
# Doc 4: prep-PREP2013.pdf (ready, 227 pages)
|
||
# Section 7: 'Questions 1-100' pages 1–100
|
||
|
||
# 2a. Extract in background (Celery) — returns immediately, monitor via navbar 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) — shows live output in terminal
|
||
docker compose exec backend python manage.py extract 6
|
||
|
||
# 3. Check job status (shows progress, skipped questions, errors)
|
||
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 (default: auto-generated from section name)
|
||
# --mode timed|learning Quiz mode (default: timed)
|
||
# --user email Which user owns the quiz (default: first admin)
|
||
# --bg Run in background via Celery
|
||
|
||
# ── Embeddings ───────────────────────────────────────────────────────────────
|
||
# Regenerate all question embeddings (e.g. after switching embedding model)
|
||
docker compose exec backend python manage.py reembed
|
||
```
|
||
|
||
### How extraction works
|
||
|
||
1. **Upload PDF** via the web UI (Upload PDF page) — the system extracts text and stores it
|
||
2. **Create a section** on the document page (define page range, e.g. pages 1–767)
|
||
3. **Extract quiz** — either from the web UI or CLI:
|
||
- The system auto-detects the PDF format:
|
||
- **Inline answers** (PREP 2012): "Correct Answer: X" after each question → standard extraction
|
||
- **Separate answer key** (PREP 2013): "Preferred Response: X" in a dedicated answer section → two-phase extraction (questions first, then answer key, then matched)
|
||
- Large sections are split into 50-page chunks automatically
|
||
- Progress shown live in the web UI extraction panel
|
||
4. 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 + embeddings
|
||
├─ ChromaDB ← document page chunks for quiz generation
|
||
├─ Redis ← Celery task queue
|
||
├─ Celery workers ← background PDF processing, emails
|
||
├─ LiteLLM proxy ← Claude/Gemini/GPT for extraction + embeddings
|
||
├─ AWS Bedrock ← Polly TTS, Titan embed fallback
|
||
└─ OpenAI ← TTS (direct, not via proxy)
|
||
```
|
||
|
||
### Search
|
||
|
||
Quiz search uses **hybrid retrieval**:
|
||
1. **Semantic** — embed the query with Titan Embed V2, cosine similarity against all questions via pgvector HNSW index
|
||
2. **Keyword** — PostgreSQL `ILIKE` on question text and options
|
||
3. Results merged and ranked — semantic matches shown first by score, keyword-only matches appended
|
||
|
||
### TTS Providers
|
||
|
||
| Provider | Model IDs | 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` |
|
||
| 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>` | `GOOGLE_TTS_API_KEY` |
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
├── backend/
|
||
│ ├── app/
|
||
│ │ ├── main.py # App startup, seeding, pgvector setup
|
||
│ │ ├── config.py # Settings (pydantic-settings)
|
||
│ │ ├── models/ # SQLAlchemy models
|
||
│ │ ├── routers/ # FastAPI route handlers
|
||
│ │ ├── services/
|
||
│ │ │ ├── ai_service.py # LLM extraction + TTS
|
||
│ │ │ ├── embedding_service.py # pgvector embeddings
|
||
│ │ │ ├── vector_service.py # ChromaDB (document pages)
|
||
│ │ │ ├── quiz_service.py # Quiz creation pipeline
|
||
│ │ │ └── email_service.py # Email templates + sending
|
||
│ │ └── utils/
|
||
│ ├── manage.py # CLI: reset-password, list-users, reembed
|
||
│ ├── requirements.txt
|
||
│ └── Dockerfile
|
||
├── frontend/
|
||
│ ├── src/
|
||
│ │ ├── pages/ # Dashboard, Quiz, Results, Settings, Search …
|
||
│ │ ├── components/ # Navbar, LineChart
|
||
│ │ └── context/ # AuthContext, ThemeContext
|
||
│ ├── nginx.conf
|
||
│ └── Dockerfile
|
||
└── docker-compose.yml
|
||
```
|
||
|
||
## Deployment Notes
|
||
|
||
- The frontend Nginx only binds to `127.0.0.1:8081` — put a reverse proxy (Caddy/Nginx) in front for HTTPS
|
||
- PostgreSQL data is persisted in the `postgres_data` Docker volume — back it up regularly
|
||
- Uploads live in `uploads_data` volume — includes extracted question images
|
||
- Set `APP_URL` to your public domain so verification/reset email links work
|