A local-first German learning notebook. Capture real media (video, podcast, or plain text), let an AI agent turn it into CEFR-tagged vocabulary and exercises, and keep it all in memory with daily spaced-repetition review — backed by an offline German↔English dictionary.
Everything runs on your machine: your materials, your progress, and your dictionary all live in a local SQLite database. Nothing is sent anywhere unless you point it at a cloud LLM yourself.
New to the method? Read the companion Learner's Guide — the "why" and "how" of going from A1 to B2 on your own.
Contributing or using an AI coding agent? See AGENTS.md for the architecture, conventions, and the Ubuntu/WSL developer workflow.
- Library — add German media (video / podcast / text) by pasting a transcript or a link. Each material stores its transcript, an optional translation, and your notes.
- Today's news — one click fetches a current German article (easy-German nachrichtenleicht or Deutsche Welle), translates it, and turns it into vocabulary and exercises. Also available as a scheduled command-line script (see backend/README.md).
- AI generation — from a material, an agent extracts high-frequency vocabulary (lemmatized, POS- and CEFR-tagged) and generates grammar, vocab, and interpretation exercises with answer keys.
- Vocabulary — a searchable personal word list with semantic search, fed by the media you actually consume.
- Daily review — an FSRS scheduler surfaces the vocab and exercises that are due, weighting weak spots so you review at the right moment.
- Course — an A1→B2 grammar taxonomy you can work through lesson by lesson.
- Import — paste external grammar content or exercises (JSON) and merge them into your library, practice, and review.
- Offline dictionary — hover any German word for an instant WikDict definition, no network required.
- Reminders — an optional daily nudge to clear your review queue.
| Layer | Stack |
|---|---|
| Backend | Python 3.12 · FastAPI · SQLModel / SQLite · pydantic-settings · FSRS · APScheduler · uv |
| AI (optional) | litellm + instructor — pluggable local (Ollama) or cloud (OpenAI, Anthropic, …) models with Pydantic-validated structured output |
| Dictionary | WikDict de-en (CC BY-SA 4.0), built into a local SQLite file |
| Frontend | React 19 · TypeScript · Vite · Tailwind CSS · React Router |
learning/
backend/ FastAPI app (package: sprachheft)
main.py dev server launcher (uvicorn)
src/sprachheft/ api · agents · llm · dictionary · srs · ingest · services
tests/ pytest suite
frontend/ React + Vite single-page app
src/features/ dashboard · course · library · material · vocab · review · importer
content/ course + taxonomy JSON (git-friendly backbone)
data/ local SQLite databases (app + dictionary)
run-dev.ps1 dev launcher (Windows PowerShell)
.claude/run-dev.sh dev launcher (WSL / Linux / macOS)
LEARNING-GUIDE.md the method: how to actually learn German with this app
One command installs every dependency (uv + Python 3.12, Node, and all backend
and frontend packages), creates backend/.env, and builds the offline
dictionary:
-
Windows — double-click
setup.bat, or from a terminal:.\setup.bat # install everything .\setup.bat -Run # install, then start the app
-
WSL / Linux / macOS:
./setup.sh # install everything ./setup.sh --run # install, then start the app
The scripts are idempotent, so they are safe to re-run. Add --help (-Help on
Windows) to see every option (--minimal, --with-transcribe, --skip-dict).
Prefer to set things up by hand? Follow the manual steps below.
The app has two parts — a backend API and a frontend UI. Run each in its own terminal.
1. Backend (API — http://127.0.0.1:8000)
cd backend
uv sync --extra llm # base install + the AI generation agent (omit --extra llm for a lighter install)
uv run python main.py # dev server with reloadVerify it's up: open http://127.0.0.1:8000/health, or the interactive API docs at http://127.0.0.1:8000/docs.
Download and build the offline WikDict database into data/dict.sqlite:
cd backend
uv run python -m sprachheft.dictionary.loader3. Frontend (UI — http://localhost:5173)
cd frontend
npm install
npm run devOpen http://localhost:5173 in your browser.
To start the backend and frontend together:
- Windows:
powershell -ExecutionPolicy Bypass -File .\run-dev.ps1 - WSL / Linux / macOS:
bash ./run-dev.sh
Both print the backend and frontend URLs and remind you to build the dictionary on first run.
Backend settings use the SPRACHHEFT_ prefix and live in a backend/.env file.
The setup scripts create it from
backend/.env.example — which defaults to an offline
fake LLM so the app runs with no model configured. See
backend/src/sprachheft/config.py for every
option. Common ones:
| Variable | Purpose | Default |
|---|---|---|
SPRACHHEFT_LLM_MODEL |
litellm model string, e.g. ollama/llama3.1, gpt-4o-mini, claude-3-5-sonnet-latest |
ollama/llama3.1 |
SPRACHHEFT_LLM_API_BASE |
Base URL for local providers (e.g. Ollama) | — |
SPRACHHEFT_LLM_API_KEY |
API key for a cloud provider | — |
SPRACHHEFT_DEFAULT_LEVEL |
Default CEFR level for new content | A2 |
SPRACHHEFT_REMINDER_TIME |
Daily review reminder (local HH:MM) |
18:00 |
The frontend reads the backend URL from VITE_API_BASE (defaults to
http://127.0.0.1:8000).
The AI generation agent needs the
llmextra (uv sync --extra llm) and a configured model. PointSPRACHHEFT_LLM_MODELat a local Ollama model to keep everything fully offline, or at a cloud model with an API key.
Already worked through some German with an AI — a grammar explanation, a
vocabulary list, a transcript you discussed? You can turn that conversation into
importable study material without a configured model. Paste the anchor prompt
below (with your conversation) into any chat model, then paste the JSON it returns
into the app's Import → Prompt-pack JSON tab (or POST /imports/json).
Want full control over difficulty and scaffolding (LEVEL / STAGE dials, or a printable worksheet)? Use the fuller instruction pack in prompts/ instead.
One object with a material, a vocabulary[] list, and an exercises[] list.
The material's level (A1–B2) sets the CEFR level for everything imported:
{
"material": {
"title": "Ein Tag im Büro",
"level": "A2",
"media_type": "text",
"source_url": null,
"themes": ["Alltag", "Arbeit"],
"transcript": "optional source text the material is based on"
},
"vocabulary": [
{
"word": "r Bahnhof",
"lemma": "Bahnhof",
"pos": "noun",
"meaning_en": "train station",
"cefr": "A2",
"example_de": "Der Zug fährt vom Bahnhof ab.",
"example_en": "The train departs from the station.",
"grammar_tags": ["a2.dative"]
}
],
"exercises": [
{
"type": "fill-in-blank",
"cefr": "A2",
"grammar_tags": ["a2.dative"],
"instructions": "Setze das richtige Artikelwort ein.",
"payload": { "items": [{ "prompt": "Ich fahre mit ___ Bus.", "hint": "Dativ" }], "hints": [] },
"answer_key": { "items": [{ "answer": "dem" }] }
}
]
}Each exercise type has its own payload / answer_key shape (answers align to
payload items by index):
- fill-in-blank — payload
{ "items": [{ "prompt": "… ___ …", "hint": "?" }] }· key{ "items": [{ "answer": "…" }] } - conjugation — payload
{ "verb": "gehen", "tense": "Präsens", "items": [{ "person": "ich" }] }· key{ "items": [{ "answer": "gehe" }] } - translation — payload
{ "direction": "en-de", "items": [{ "prompt": "…" }] }· key{ "items": [{ "answer": "…", "accept": ["…"] }] } - multiple-choice — payload
{ "items": [{ "prompt": "…", "options": ["…"] }] }· key{ "items": [{ "answer": "…", "explanation": "…" }] }—answermust be exactly one of theoptions - reorder — payload
{ "items": [{ "tokens": ["…"] }] }· key{ "items": [{ "answer": "…" }] } - reading — payload
{ "text": "…", "questions": [{ "prompt": "…" }] }· key{ "questions": [{ "answer": "…" }] } - interpretation — payload
{ "prompt": "…", "guiding_points": ["…"] }· key{ "sample_answer": "…", "rubric": ["…"] } - writing — payload
{ "theme": "…", "task": "…", "useful_phrases": ["…"], "checklist": ["…"] }· key{ "model_answer": "…", "rubric": ["…"] }
The first five types are auto-graded during practice; reading,
interpretation, and writing reveal a model answer instead. Nouns use the
article shorthand r / e / s = der / die / das.
Copy this, paste your conversation where marked, and send. The model returns a single JSON object you can import as-is:
You are an expert German-as-a-foreign-language (DaF) teacher and assessment
item-writer. Convert the German learning conversation / notes I provide into
Sprachheft study material.
Output ONLY one valid JSON object — no markdown fences, no commentary — with
exactly this shape:
{ "material": { … }, "vocabulary": [ … ], "exercises": [ … ] }
Rules:
- Choose a CEFR level (A1–B2) matching the German and set material.level; tag
every item's "cefr".
- vocabulary: 8–15 useful items that actually appear in the conversation. Write
nouns with the article shorthand r/e/s (der/die/das); include lemma, pos, a
concise English meaning_en, and one short example (example_de + example_en).
- exercises: use ONLY these "type" values — fill-in-blank, conjugation,
translation, multiple-choice, reorder, reading, interpretation, writing.
Include a mix, with exactly one interpretation task and one writing task.
- Each "answer_key" aligns to its "payload" items by index. For multiple-choice,
"answer" must be exactly one of the "options" strings.
- Valid JSON only: double quotes, no trailing commas, no comments, null for empty
optionals.
Conversation / notes to convert:
<<<
(paste your AI conversation or notes here)
>>>
cd backend
uv sync --extra dev
uv run pytest # test suite
uv run ruff check # lintcd frontend
npm run lint # oxlint
npm run build # type-check + production build- Transcription (
uv sync --extra transcribe) — yt-dlp + faster-whisper to auto-transcribe audio/video instead of pasting a transcript manually. Requiresffmpegon your PATH. - Embeddings (
uv sync --extra embeddings) — fastembed for higher-quality semantic vocabulary search (falls back to a local hashing method otherwise).