No description
Extraction fixes: - OCR normalization: 'Pref erred' → 'Preferred', 'ltem' → 'Item' applied to boundary scan, Phase 1 questions, and Phase 2 answer key content before AI processing - Chunk boundary: Phase 1 chunks now capped at (answer_section_start - 1) so no chunk bleeds into the answer section — (51, 100) becomes (51, 55) for PREP 2013 - Result: Phase 1 gets 2 clean chunks (1-50 and 51-55), Phase 2 gets pages 56-227 Category creation in DocumentDetailPage: - Replaced window.prompt() with inline input form (more reliable, no browser quirks) - Fixed option value type: String(c.id) ensures consistent string comparison with selectedQuestionCategoryId state (prevents type mismatch in controlled select) - "+ New" button toggles inline form; Enter key or Add button submits Deletion safety (confirmed): - Deleting a quiz: questions detached to bank if exclusive, kept if shared — NEVER deleted - Deleting a question category: questions uncategorized or moved — NEVER deleted Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| 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 — 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 |
| SMTP (smtp2go) |
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)
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:
# ── 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
- Upload PDF via the web UI (Upload PDF page) — the system extracts text and stores it
- Create a section on the document page (define page range, e.g. pages 1–767)
- 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
- The system auto-detects the PDF format:
- 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:
- Semantic — embed the query with Titan Embed V2, cosine similarity against all questions via pgvector HNSW index
- Keyword — PostgreSQL
ILIKEon question text and options - 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_dataDocker volume — back it up regularly - Uploads live in
uploads_datavolume — includes extracted question images - Set
APP_URLto your public domain so verification/reset email links work