← All projects
Personal · Live on homelab · 2026 · … min read

Resume Tutor — voice drilling on your own CV

Every interview I've ever had got harder when the interviewer asked the second question. Not "what did you build" — "tell me again, from a different angle, why that choice?" I built a homelab tool that asks me those questions on a loop, in voice, against my own CV, and tracks which claims I keep tripping over.

FastAPI HTMX Jinja2 faster-whisper OpenAI gpt-5.4 tiered Polars Pydantic v2 SM-2

The shape of the problem

A resume is a list of claims. "Built a RAG pipeline." "Cut grading time by ~80%." "Real-time agentic CRM." Each claim needs to survive five minutes of cross-examination from someone who has actually built one of those things. The failure mode isn't that I forget what I did — it's that I remember the headline and lose the texture.

So the unit of work isn't the resume; it's the claim. Each claim points to one or more underlying concepts (FAISS, RAG eval, WebRTC signalling, etc.), and each concept gets drilled, reviewed, and reinforced on its own schedule.

What I built

  • Voice drill loop. Pick a claim → app asks an opening question → I answer in voice → faster-whisper transcribes locally → an OpenAI mid-tier evaluator scores the answer per-question, surfaces misconceptions, and queues a sharper follow-up. Transcripts and reviews are written per session as plain Markdown + JSON on disk.
  • Tiered LLM substrate. Three OpenAI tiers behind a single TieredLLM facade — flagship (gpt-5.4) for synthesis and lab notebooks, mid (gpt-5.4-mini) for the questioner / evaluator / lab grader, nano (gpt-5.4-nano) for review verdicts and JD scoring. End-to-end cost is ~$0.04–0.09 per drill, capped at $1/day by a BudgetTracker that raises before the next call.
  • In-app interactive labs. Concepts come with executable lessons stored as Marimo .py files but rendered inside the app — lesson markdown, setup, exercise prose, hidden solution. POST /lab/<slug>/grade calls the lab grader (~$0.001) and returns a verdict + feedback as an HTMX partial. Same grader does retrieval checks on plain concept cards.
  • SM-2 spaced repetition. Verdicts (wrong / partial / right) feed an SM-2 implementation with bounded ease (1.3–3.0) and bounded interval (1–90 days). Schedule is recomputed lazily on the home page; no live event hooks, no background queue, no drift.
  • Daily 8 AM IST Gmail digest. Idempotent multipart HTML+text email — what's due today, sparkline of last 14 days, open misconceptions, prereq warnings. Sent via Gmail SMTP from a cron line, suppressed when there's nothing to say.
  • Career-ops integration. The drill page lists JDs from my career-ops tracker. list_jds(auto_refresh=True) stat-checks applications.md's mtime; if it's newer than the last pull, the JDs auto-rebuild on the next page load. No webhooks, no message bus — just a file timestamp and a function call.

Architecture highlights

$0.04–$0.09
end-to-end cost per drill
3 tiers
flagship / mid / nano OpenAI routing
SM-2
bounded ease + interval, replayed from disk
273
tests · ruff + pyright strict clean

What I learned

Three things this project taught me that I wouldn't have learned by reading about spaced repetition:

  • The model isn't the bottleneck — the prompt is. The pedagogy bar lives in prompts/notebook_generate.md and prompts/concept_explain.md, not in template code. When a generated lab feels weak, the fix is almost always tightening one of those files; rarely the rendering layer.
  • Idempotency is what makes home-grown tools survive. The digest is idempotent (one row in digests.jsonl per day per recipient). The SR sync is idempotent (replays only sessions not in last_synced_session_ids). The JD pull is idempotent (mtime-gated). I can reboot the box, lose state, run the same command twice — nothing breaks.
  • One "easy" feature paid for the rest. Wiring the daily digest took half a Saturday. It's the single feature that keeps me coming back, because it ships review work to me instead of asking me to remember to log in.
This is a single-user, single-server tool with no users to delight, no roadmap, no tickets. It exists because it makes my interviews go better. It's the kind of project I think every applied engineer should keep around — a thing that's small enough to never break, useful enough that it doesn't.

Stack

FastAPI · HTMX · Jinja2 · Polars · Pydantic v2 · Typer · structlog · faster-whisper · OpenAI Python SDK · Gmail SMTP · cron · systemd · Tailscale