All checks were successful
Mobile Android Release / android-release (push) Successful in 1m32s
467 lines
23 KiB
Markdown
467 lines
23 KiB
Markdown
# PedsHub
|
|
|
|
AI-powered pediatric learning platform. Upload PDF study materials, automatically extract or generate MCQ questions with AI, study with an AI tutor, and track your progress. Installable as a PWA.
|
|
|
|
## Features
|
|
|
|
- **PDF → Quiz**: Upload PREP PDFs, AI extracts questions, answers, and explanations
|
|
- **Generative Mode**: AI reads plain text/textbook chapters and creates MCQ questions from scratch
|
|
- **Quiz Modes**: Study (instant feedback with AI tutor) and Exam (timed, scored)
|
|
- **AI Tutor (TeachChat)**: Ask follow-up questions mid-study — AI knows the current question, correct answer, and related content. Renders markdown tables, code, and follow-up suggestion chips.
|
|
- **Tag Classification**: AI classifies questions with subjects, diseases, and keywords — filter your question bank by any combination of tags
|
|
- **Multi-Category Filtering**: Filter questions by question category, tags, or quiz source — combine multiple filters for precise study sets
|
|
- **Text-to-Speech**: LiteLLM-routed local TTS, OpenAI TTS, ElevenLabs, Google Cloud — voice selection per quiz
|
|
- **Semantic Search**: pgvector embeddings — finds questions by meaning, not just keywords
|
|
- **Question Bank**: All questions searchable, filterable by category and tags, with inline study mode
|
|
- **Image Validation**: AI `has_figure` gating — only links extracted images to questions the AI flagged as having a figure, preventing mismatched images
|
|
- **Performance Tracking**: Per-quiz attempt history with line charts, score trends, delete individual attempts
|
|
- **PDF Processing Progress**: Step-by-step progress reporting for PDF uploads and quiz extraction — see each stage in real time
|
|
- **Concurrent Quiz Protection**: Redis session locks prevent the same quiz from being resumed on multiple devices simultaneously
|
|
- **Landing Page**: Integrated with the app — Sign In / Register open as modal overlays, shared Navbar
|
|
- **PWA**: Installable on mobile/desktop (no caching — avoids stale JS issues)
|
|
- **Bot Protection**: Cloudflare Turnstile on registration and contact forms
|
|
- **Email Verification**: Required before first login; password reset via email
|
|
- **Role System**: Admin / Moderator / User with optional rate-limit exemption (unthrottle)
|
|
- **Admin User Management**: Delete users, change roles, toggle unthrottle — all from the admin dashboard
|
|
- **Nextcloud Integration**: Browse and import PDFs from your Nextcloud
|
|
- **Themes**: Default (blue) and Warm/Literary (brown, serif fonts)
|
|
- **Multi-worker**: 4 uvicorn workers with Redis singleton lock for scheduler
|
|
|
|
## Stack
|
|
|
|
| Layer | Tech |
|
|
|---|---|
|
|
| Frontend | React 18 + React Router 6, plain CSS, Nginx, PWA (network-only service worker) |
|
|
| Backend | FastAPI, SQLAlchemy, PostgreSQL 16 + pgvector |
|
|
| AI/LLM | LiteLLM proxy (Claude, Gemini, GPT, Bedrock, and more) |
|
|
| Embeddings | Configurable — any LiteLLM proxy model or direct AWS Bedrock (1024-dim) |
|
|
| Document vectors | ChromaDB |
|
|
| TTS | LiteLLM-routed local TTS, OpenAI (direct), ElevenLabs, Google Cloud TTS |
|
|
| Queue | Celery + Redis (4 fork workers) |
|
|
| Email | SMTP (smtp2go or any SMTP server) |
|
|
| Bot protection | Cloudflare Turnstile (runtime-configurable, no rebuild needed) |
|
|
|
|
For detailed architecture documentation, see [docs/architecture.md](docs/architecture.md).
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
git clone ssh://git.danvics.com:2222/danvics/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
|
|
|
|
### Backend (`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 (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=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
|
|
|
|
# Bot protection — Cloudflare Turnstile (backend secret)
|
|
TURNSTILE_SECRET_KEY=<cloudflare-turnstile-secret-key>
|
|
|
|
# Contact form admin notifications
|
|
ADMIN_EMAIL=admin@yourdomain.com
|
|
|
|
# App
|
|
APP_URL=https://your-domain.com
|
|
UPLOAD_DIR=/app/uploads
|
|
MAX_UPLOAD_SIZE=524288000
|
|
CHROMA_PERSIST_DIR=/app/chroma_data
|
|
|
|
# Optional bootstrap admin. Leave blank to let the first registered user become admin.
|
|
DEFAULT_ADMIN_EMAIL=
|
|
DEFAULT_ADMIN_PASSWORD=
|
|
```
|
|
|
|
### Frontend (`frontend/.env`)
|
|
|
|
```env
|
|
# Bot protection — Cloudflare Turnstile (public site key)
|
|
TURNSTILE_SITE_KEY=<cloudflare-turnstile-site-key>
|
|
```
|
|
|
|
The frontend env file is **not** baked into the Docker image at build time. Instead, `docker-entrypoint.sh` generates a `/config.js` file from the env vars when the container **starts**. This means:
|
|
|
|
- Change keys by editing `frontend/.env` and restarting the container (no rebuild)
|
|
- Switch captcha providers by updating the entrypoint script and the widget component
|
|
- Remove bot protection by clearing the key (empty = disabled)
|
|
|
|
## Cloudflare Turnstile (Bot Protection)
|
|
|
|
### How it works
|
|
|
|
Turnstile protects the **registration** and **contact** forms from bot submissions. It does NOT require Cloudflare DNS/proxy — it works standalone on any domain.
|
|
|
|
**Flow:**
|
|
|
|
```
|
|
1. Page loads → Turnstile JS loads from challenges.cloudflare.com
|
|
2. Widget renders (invisible or interactive depending on risk score)
|
|
3. User completes challenge → widget calls onVerify(token)
|
|
4. Frontend stores token in state → Sign Up button becomes enabled
|
|
5. User submits form → token sent as `turnstile_token` in POST body
|
|
6. Backend receives token → POSTs to Cloudflare's siteverify API:
|
|
POST https://challenges.cloudflare.com/turnstile/v0/siteverify
|
|
Body: { secret: TURNSTILE_SECRET_KEY, response: turnstile_token }
|
|
7. Cloudflare returns { success: true/false }
|
|
8. If false → 400 "Bot verification failed"
|
|
9. If true → registration proceeds normally
|
|
```
|
|
|
|
**Where Turnstile is active:**
|
|
- Register form (modal overlay on landing page)
|
|
- Register form (standalone `/register` page)
|
|
- Contact form (landing page)
|
|
|
|
**Where it is NOT active (by design):**
|
|
- Login — protected by IP-based rate limiting (10 attempts / 15 min) instead
|
|
|
|
### Setup
|
|
|
|
1. Go to https://dash.cloudflare.com → Turnstile → Add widget
|
|
2. Add your domain(s), choose "Managed" widget type
|
|
3. Copy the Site Key and Secret Key
|
|
|
|
```bash
|
|
# frontend/.env
|
|
TURNSTILE_SITE_KEY=0x4AAAAAAA...
|
|
|
|
# backend/.env
|
|
TURNSTILE_SECRET_KEY=0x4AAAAAAA...
|
|
|
|
# Restart (no rebuild needed)
|
|
docker compose restart frontend backend
|
|
```
|
|
|
|
### Testing
|
|
|
|
Cloudflare provides test keys for development:
|
|
|
|
| Purpose | Site Key | Secret Key |
|
|
|---|---|---|
|
|
| Always passes | `1x00000000000000000000AA` | `1x0000000000000000000000000000000AA` |
|
|
| Always blocks | `2x00000000000000000000AB` | `2x0000000000000000000000000000000AB` |
|
|
|
|
### Disabling
|
|
|
|
Set both keys to empty strings (or remove them), restart. The widget won't render and the backend skips verification.
|
|
|
|
### Runtime Config Architecture
|
|
|
|
```
|
|
docker-compose.yml
|
|
└─ frontend service: env_file: ./frontend/.env
|
|
|
|
Container startup (docker-entrypoint.sh):
|
|
└─ Reads $TURNSTILE_SITE_KEY from environment
|
|
└─ Writes /usr/share/nginx/html/config.js:
|
|
window.__APP_CONFIG__ = { TURNSTILE_SITE_KEY: "0x4AAA..." };
|
|
|
|
Browser loads index.html:
|
|
└─ <script src="/config.js"> → sets window.__APP_CONFIG__
|
|
└─ <script type="module" src="/src/main.jsx"> → React app starts
|
|
└─ Components read: window.__APP_CONFIG__?.TURNSTILE_SITE_KEY
|
|
```
|
|
|
|
This avoids Vite's `import.meta.env.VITE_*` which bakes values into the JS bundle at build time.
|
|
|
|
## Extraction Modes
|
|
|
|
| Mode | Use Case | How It Works |
|
|
|---|---|---|
|
|
| **Standard** | PREP PDFs with inline answers | Finds "Correct Answer: X" / "Preferred Response: X" after each question |
|
|
| **Two-Step** | PREP 2013-style (separate answer key) | Phase 1: extract questions, Phase 2: extract answer key, Phase 3: match |
|
|
| **Questions Only** | No answers available | Extracts Q + options, sets answer to "PENDING" for manual fill |
|
|
| **AI + Regex** | Non-standard answer formats | AI analyses format, generates regex, applies it for fast extraction |
|
|
| **AI Decides** | Unknown format | AI samples start + end of document, picks the best strategy |
|
|
| **Generate** | Textbook chapters, study notes | AI reads the text and **creates** MCQ questions (correct answer from text, distractors from AI knowledge) |
|
|
|
|
The extraction model is selected per-job from the dropdown on the document detail page. Models are configured in Admin → AI Models with task `extraction`.
|
|
|
|
## Tag Classification
|
|
|
|
PedsHub can automatically classify questions with structured tags using AI. Tags are organized into three types:
|
|
|
|
| Tag Type | Examples | Purpose |
|
|
|---|---|---|
|
|
| **Subject** | Cardiology, Infectious Disease, Neonatology | Medical specialty / rotation |
|
|
| **Disease** | Kawasaki Disease, Pneumonia, Type 1 Diabetes | Specific condition mentioned |
|
|
| **Keyword** | fever workup, antibiotic resistance, fluid management | Clinical concept / topic |
|
|
|
|
**How it works:**
|
|
|
|
1. Admin triggers classification from the admin dashboard (or via the `/api/admin/classify` endpoint)
|
|
2. A Celery background task (`classify_questions`) finds all untagged questions
|
|
3. Questions are sent to the AI in batches of 10 — the AI returns subjects, diseases, and keywords for each
|
|
4. Tags are stored in `question_tags` with case-insensitive deduplication and linked via `question_tag_links`
|
|
5. Users can filter the question bank by any combination of tags in the Question Bank page
|
|
|
|
The classification model is configured in Admin → AI Models with task `keyword`.
|
|
|
|
## AI Tutor (TeachChat)
|
|
|
|
The study-mode AI tutor opens as a slide-in drawer. It:
|
|
|
|
- Knows the current question, correct answer, and explanation
|
|
- Pulls related questions from the question bank via semantic similarity (pgvector)
|
|
- Supports model selection (dropdown when multiple teach models configured)
|
|
- Renders full markdown including GFM tables (via `remark-gfm` + `rehype-raw`)
|
|
- Generates follow-up suggestion chips after each response
|
|
- Uses `litellm.acompletion()` (async) so it doesn't block other requests
|
|
|
|
Models for teach are configured in Admin → AI Models with task `teach`.
|
|
|
|
## Rate Limiting
|
|
|
|
| Endpoint | Limit | Scope |
|
|
|---|---|---|
|
|
| Login | 10 attempts / 15 min | Per IP (Redis) |
|
|
| TeachChat | 30 calls / 10 min | Per user (Redis) |
|
|
| TTS | 20 calls / 5 min | Per user (Redis) |
|
|
|
|
Admins, moderators, and users marked as "unthrottled" are exempt. Rate limiting degrades gracefully if Redis is unavailable (allows requests through).
|
|
|
|
## PWA (Progressive Web App)
|
|
|
|
The app is installable on mobile and desktop. The service worker is **network-only** (no caching) — it exists solely to satisfy the PWA installability requirement. This avoids stale cached JS causing blank screens or module-not-found errors after deployments.
|
|
|
|
Files: `frontend/public/manifest.json`, `frontend/public/sw.js`, `frontend/public/icon.svg`
|
|
|
|
## Rebuild & Restart
|
|
|
|
Frontend and backend are built into Docker images — code changes require a `build` before they take effect. Environment variable changes only need a `restart`.
|
|
|
|
```bash
|
|
# 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
|
|
|
|
# Restart without rebuilding (for .env changes, including Turnstile keys)
|
|
docker compose restart frontend 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), `teach` (AI tutor), `keyword` (tag classification), `tts` (voices)
|
|
- Same model can be added to multiple tasks (e.g. `bedrock-claude-haiku-4.5` for both extraction and teach)
|
|
- 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
|
|
- Unthrottle users (exempt from rate limits)
|
|
- **Delete users** — removes the user and all associated data (documents, quizzes, attempts)
|
|
|
|
### More Settings
|
|
- **Public Registration** — enable/disable new user sign-ups
|
|
- **Classify Questions** — trigger AI tag classification for all untagged questions (runs as background task)
|
|
- **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
|
|
|
|
## CLI Management
|
|
|
|
```bash
|
|
# ── 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 ───────────────────────────────────────────────────────────
|
|
docker compose exec backend python manage.py list-sections
|
|
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
|
|
|
|
# ── Embeddings ───────────────────────────────────────────────────────────────
|
|
docker compose exec backend python manage.py reembed
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Browser
|
|
│
|
|
▼
|
|
Caddy/Nginx (HTTPS termination)
|
|
│
|
|
▼
|
|
Nginx (frontend container — serves React SPA + proxies /api to backend)
|
|
├─► React SPA (static files + runtime config.js)
|
|
└─► FastAPI backend (4 uvicorn workers)
|
|
├─ PostgreSQL 16 + pgvector ← users, quizzes, questions + 1024-dim embeddings
|
|
├─ ChromaDB ← document page chunks for RAG context
|
|
├─ Redis ← Celery queue, rate limits, settings, quiz progress, singleton lock
|
|
├─ Celery workers (x4) ← background PDF processing, quiz extraction, tag classification, emails
|
|
├─ LiteLLM proxy ← Claude/Gemini/GPT for extraction + teach + classification; embedding models
|
|
├─ AWS Bedrock ← Polly TTS; embedding fallback
|
|
├─ OpenAI ← TTS (direct, not via proxy)
|
|
└─ Cloudflare Turnstile ← bot verification for registration + contact
|
|
```
|
|
|
|
For deep architecture documentation (database schema, request flow, background tasks, vector search, auth, and more), see [docs/architecture.md](docs/architecture.md).
|
|
|
|
### Multi-Worker Setup
|
|
|
|
The backend runs 4 uvicorn workers. Two coordination mechanisms prevent worker races at startup:
|
|
|
|
- **Postgres advisory lock** (`pg_advisory_lock(8472931)`) serializes startup DDL (`Base.metadata.create_all()` + `setup_pgvector()`). Only one worker runs the schema operations; the others wait, then see the idempotent `IF NOT EXISTS` statements as no-ops. This eliminates the deadlock that used to kill one worker on boot when concurrent `ALTER TABLE`s acquired locks in different orders.
|
|
- **Redis SETNX lock** (`startup:singleton_lock`, TTL 300s) ensures only one worker starts the APScheduler and backfill thread.
|
|
|
|
Stale `idle in transaction` DB connections are killed at startup to prevent DDL migration hangs.
|
|
|
|
### Database Migrations (Alembic)
|
|
|
|
Schema changes go through Alembic (`backend/alembic/`). See [docs/migrations.md](docs/migrations.md) for the developer workflow (create → review → apply → rollback).
|
|
|
|
Alembic reads `DATABASE_URL` from the container env; there is no hardcoded URL in `alembic.ini`. The live DB tracks its version in `alembic_version`. `Base.metadata.create_all()` remains as a fallback for fresh deploys and must not be removed without first generating a baseline migration.
|
|
|
|
### Landing Page
|
|
|
|
The landing page at `/home` uses the shared `Navbar` component. When not logged in:
|
|
- Navbar shows Sign In / Register buttons
|
|
- Clicking either opens a **modal overlay** (no page navigation) with tabbed Sign In / Register forms
|
|
- Hero CTA buttons also open the modal
|
|
- Standalone `/login` and `/register` pages still exist for direct links, email verification redirects, and bookmarks
|
|
|
|
### Security
|
|
|
|
- **SQL injection**: pgvector queries use parameterized bind variables (`CAST(:vec AS vector)`)
|
|
- **API key exposure**: LiteLLM/TTS model search uses POST body, not URL query params
|
|
- **Rate limiting**: Redis INCR + TTL keys on login, teach, and TTS endpoints
|
|
- **Bot protection**: Cloudflare Turnstile on registration and contact forms
|
|
- **CSP headers**: Configured in nginx.conf for fonts, Turnstile, and self
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
├── backend/
|
|
│ ├── app/
|
|
│ │ ├── main.py # App startup, DB migrations, Redis singleton lock
|
|
│ │ ├── config.py # Settings (pydantic-settings, reads .env)
|
|
│ │ ├── models/ # SQLAlchemy ORM models
|
|
│ │ ├── routers/
|
|
│ │ │ ├── auth.py # Login, register (with Turnstile), verify email, password reset
|
|
│ │ │ ├── quizzes.py # Quiz CRUD, async extraction jobs
|
|
│ │ │ ├── attempts.py # Quiz attempts, progress save/resume, history, stats
|
|
│ │ │ ├── teach.py # AI tutor chat (async, with follow-up suggestions)
|
|
│ │ │ ├── admin.py # Model management, user roles, settings, classification trigger
|
|
│ │ │ ├── tags.py # Tag listing and filtering endpoints
|
|
│ │ │ ├── contact.py # Contact form (with Turnstile)
|
|
│ │ │ └── ...
|
|
│ │ ├── services/
|
|
│ │ │ ├── ai_service.py # LLM calls + _proxy_model() routing
|
|
│ │ │ ├── extraction_modes.py # 6 extraction strategies (standard → generate)
|
|
│ │ │ ├── embedding_service.py # pgvector embeddings (httpx → proxy)
|
|
│ │ │ ├── vector_service.py # ChromaDB document page chunks
|
|
│ │ │ └── email_service.py # Email templates + sending
|
|
│ │ ├── tasks/
|
|
│ │ │ ├── quiz_tasks.py # Celery: quiz extraction, tag classification, re-embed
|
|
│ │ │ └── pdf_tasks.py # Celery: PDF text extraction + vectorization
|
|
│ │ └── utils/
|
|
│ │ └── auth.py # JWT, password hashing, rate limiting
|
|
│ ├── manage.py # CLI: reset-password, list-users, reembed, extract
|
|
│ └── Dockerfile
|
|
├── frontend/
|
|
│ ├── src/
|
|
│ │ ├── pages/
|
|
│ │ │ ├── LandingPage.jsx # Landing + contact form + auth modal + Turnstile
|
|
│ │ │ ├── DashboardPage.jsx # Stats, performance chart, attempt history with delete
|
|
│ │ │ ├── QuizzesPage.jsx # Quiz grid, search, past attempts with delete
|
|
│ │ │ ├── QuizPage.jsx # Quiz taking (study/exam), TTS, progress save
|
|
│ │ │ ├── ResultsPage.jsx # Score card, answer review, delete attempt
|
|
│ │ │ ├── DocumentDetailPage.jsx # Sections, extraction mode picker, model selector
|
|
│ │ │ ├── RegisterPage.jsx # Standalone register (with Turnstile)
|
|
│ │ │ └── ...
|
|
│ │ ├── components/
|
|
│ │ │ ├── Navbar.jsx # Shared navbar (auth-aware, optional modal callbacks)
|
|
│ │ │ ├── TeachChat.jsx # AI tutor drawer (markdown, tables, suggestions)
|
|
│ │ │ └── ...
|
|
│ │ └── context/ # AuthContext, ThemeContext
|
|
│ ├── public/
|
|
│ │ ├── manifest.json # PWA manifest
|
|
│ │ ├── sw.js # Service worker (network-only, no cache)
|
|
│ │ ├── icon.svg # App icon
|
|
│ │ └── config.js # Dev fallback (overwritten by entrypoint in prod)
|
|
│ ├── nginx.conf # API proxy + CSP headers
|
|
│ ├── docker-entrypoint.sh # Generates /config.js from env vars at runtime
|
|
│ └── Dockerfile
|
|
├── docs/
|
|
│ └── architecture.md # Deep architecture documentation
|
|
├── frontend/.env # Frontend runtime config (Turnstile site key)
|
|
├── backend/.env # Backend config (all secrets)
|
|
└── 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
|
|
- Frontend env vars (like Turnstile site key) are injected at container startup, not build time — change and restart, no rebuild needed
|
|
|
|
## TTS Providers
|
|
|
|
| Provider | Model/voice ID format | Key needed |
|
|
|---|---|---|
|
|
| Kokoro via LiteLLM | `local-kokoro-tts:am_adam`, `local-kokoro-tts:af_bella` | `LITELLM_API_KEY` |
|
|
|
|
The part before `:` is the LiteLLM speech model route. The part after `:` is the Kokoro speaker voice.
|