No description
Find a file
Daniel 8a220bb12e Fix PREP 2013 extraction: OCR normalization, correct chunk boundary; category UI fix
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>
2026-04-01 04:09:55 +02:00
backend Fix PREP 2013 extraction: OCR normalization, correct chunk boundary; category UI fix 2026-04-01 04:09:55 +02:00
frontend Fix PREP 2013 extraction: OCR normalization, correct chunk boundary; category UI fix 2026-04-01 04:09:55 +02:00
.gitignore Initial commit: PDF Quiz Generator app 2026-03-30 20:04:53 +00:00
docker-compose.yml Better fonts, hamburger mobile nav, jobs indicator, warm theme refinement 2026-04-01 02:14:18 +02:00
README.md Fix two-phase extraction boundary detection; README CLI docs; scroll fix 2026-04-01 03:36:12 +02:00

🩺 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

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 1767
# Doc 4: prep-PREP2013.pdf (ready, 227 pages)
#   Section 7: 'Questions 1-100'  pages 1100

# 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 1767)
  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)

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