A workspace for volunteer translators to review AI-assisted translations of sponsor–child letters. An AI model drafts a translation and screens each letter for content that needs a human's attention; volunteers then compare the AI draft against the human reference in a blind side-by-side and judge the screening. It runs in two stages: an offline pre-compute step calls the model once per letter on a local workstation, and an evaluation webapp serves those pre-computed results to volunteers and aggregates their judgments into an admin dashboard. The runtime never holds an API key.
flowchart LR
C["corpus + letter PDFs"] --> G["offline pre-compute<br/>(Gemini, local workstation)"]
G --> R["result files"]
R --> DB[("SQLite")]
DB --> W["evaluation webapp<br/>(blind A/B + alert review)"]
W --> DASH["admin dashboard<br/>+ CSV export"]
New here? Start with the Architecture overview, then read whichever area you need.
| Document | Read this if you want to… |
|---|---|
| Architecture | understand how the pieces fit and the data flow |
| Data model | know the corpus schema and the database tables |
| Methodology | understand what the study measures and why |
| Operations | run the pre-compute, benchmark, analysis, or dashboard |
| Extending | add a prompt version, a category, or letters |
| Design decisions (ADR) | know why the load-bearing choices were made |
FastAPI · Jinja2 · HTMX 2 · Tailwind CSS v4 (standalone CLI, self-hosted) ·
Pydantic v2 · SQLite (stdlib) · google-genai (Gemini API) · pytest · httpx ·
python-dotenv. Python ≥ 3.11.
# 1. Virtual environment + dependencies (Python >= 3.11)
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# 2. Secrets
cp .env.example .envThen edit .env:
ACCESS_TOKENandADMIN_TOKEN, pick any two strings. Both are required: the app refuses to start withoutADMIN_TOKEN, and the whole site sits behindACCESS_TOKEN.COOKIE_SECURE=false, required for local HTTP. The session / invite / admin cookies areSecure-only by default, so overhttp://localhostthey would not round-trip and you'd be stuck on the invite notice page.GOOGLE_API_KEY(or Application Default Credentials), only used by the offline pre-processing; leave it blank for local UI work. See.env.examplefor the two auth modes.
# 3. Frontend assets (fetches the pinned Tailwind binary, vendors HTMX + Alpine +
# Inter fonts, then compiles the CSS).
make install-assets
make build-assets
# 4. Local sample data
python -m scripts.seed_devseed_dev.py populates the database with fictional letters + AI responses
(and a couple of sample votes) so both flows work end to end; it resets the local
DB on each run and writes placeholder PDFs under letters/dev/. The real corpus
and the Gemini pre-processing that normally fills these tables are separate and
not run here. (For a bare, empty database instead, use python -m db.init.)
# 5. Run (in a second terminal, `make watch-css` rebuilds CSS on change)
uvicorn main:app --reload --port 8000Then open, with the tokens from your .env:
- Volunteer flow:
http://localhost:8000/?invite=<ACCESS_TOKEN>, the token is consumed once into a cookie and stripped from the URL; later visits just need the cookie. - Admin dashboard:
http://localhost:8000/admin?token=<ADMIN_TOKEN>.
Without a valid invite you get a short notice page instead of the application.
pytestThe frontend is fully self-hosted, no CDN at runtime. HTMX and Alpine are
vendored and the CSS is compiled from static/css/app.src.css by the Tailwind
standalone binary. Visual-identity assets (logo, favicon) live in
static/branding/.
This repository never contains real letter data: the live corpus (letters/corpus.json), the letter PDFs and the AI outputs are all gitignored. The only corpus file tracked here, letters/corpus.example.json, is entirely fictional, invented names, invented letter texts, and exists solely to document the corpus schema.
MIT — see LICENSE.