No description
Find a file
Daniel ecc4f64ad5 Update README with embedding config, admin UI, cancellation, and deployment notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 20:48:07 +02:00
backend Add switchable embedding model, Polly toggle, job cancellation, and UI fixes 2026-04-03 20:44:11 +02:00
frontend Add switchable embedding model, Polly toggle, job cancellation, and UI fixes 2026-04-03 20:44:11 +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 Update README with embedding config, admin UI, cancellation, and deployment notes 2026-04-03 20:48:07 +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, 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
Email 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_MODEL env → 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:

  1. Go to Admin → More → click Search LiteLLM, find your model, click Select
  2. Click Test to confirm it returns 1024 dimensions
  3. Run docker compose exec backend python manage.py reembed to 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 .env to 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

  1. Upload PDF via the web UI — text is extracted and stored
  2. Create a section on the document page (define page range)
  3. 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
  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 + 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:

  1. Model set in Admin UI (stored in Redis settings:embedding_model)
  2. LITELLM_EMBEDDING_MODEL env var
  3. Direct AWS Bedrock (amazon.titan-embed-text-v2:0) — automatic fallback if AWS credentials are present

Quiz search uses hybrid retrieval:

  1. Semantic — embed the query, cosine similarity via pgvector HNSW index
  2. Keyword — PostgreSQL ILIKE on question text and options
  3. 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_data Docker volume — back it up regularly
  • Uploads live in the uploads_data volume — includes extracted question images
  • Redis data persists in redis_data volume — holds runtime settings and job state
  • Set APP_URL to your public domain so email verification and password reset links work