Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

DeutscheLearn

Anonymous German language learning app with multiple learning modes.

Features

  • Cookie-based anonymous player identity.
  • Random generated display names.
  • Multiple learning modes: flashcards, letter-scramble vocabulary practice, Goethe-Institut styled exercises, and grammar assistant.
  • Mistake-aware RAG tutoring: wrong quiz/story answers can be sent as structured context so the assistant explains the exact learner error with cited grammar notes.
  • Lightweight RAG data loop: grammar questions are logged with status, context type, route, and cited chunks so unanswered questions can drive corpus expansion and eval cases.
  • Modular TypeScript frontend with Vite.
  • FastAPI backend with SQLAlchemy.
  • Render deployment config with Supabase Postgres.

Local Development

Install dependencies:

uv sync --locked
npm install --prefix frontend

Run the backend:

uv run uvicorn app.main:app --reload

Run the frontend dev server:

npm run dev --prefix frontend

For a production-style local run:

uv sync --locked --no-dev
npm run build --prefix frontend
uv run --no-sync uvicorn app.main:app --host 0.0.0.0 --port 8000

Or run the production container stack without Render:

docker compose up --build

The Compose stack starts:

  • frontend: Nginx serving the built Vite app on http://localhost:8000
  • backend: FastAPI API/RAG service on the internal app network
  • db: Postgres 16 with pgvector on the private database network

Useful environment overrides:

APP_PORT=8080 docker compose up --build
DEUTSCHELEARN_ADMIN_TOKEN=change-me \
DEUTSCHELEARN_COHERE_API_KEY=... \
DEUTSCHELEARN_OPENROUTER_API_KEY=... \
docker compose up --build

The backend container runs alembic upgrade head before starting Uvicorn. The frontend proxies /api/* requests to the backend service and serves all other routes as the SPA. The default Compose database credentials are local-development friendly; set DEUTSCHELEARN_POSTGRES_PASSWORD and API keys from your shell or a deployment secret store for hosted environments. If your database password contains URL-special characters, URL-encode it because Compose also uses it inside DATABASE_URL.

Production installs use uv sync --locked --no-dev to skip local-only tooling such as tests, crawling, and PDF ingestion. Use the default uv sync --locked for development.

Environment

DATABASE_URL=sqlite:///./quiz.db
COOKIE_SECURE=false
COOKIE_SAMESITE=lax
SEED_ON_STARTUP=true
COHERE_API_KEY=
COHERE_EMBEDDING_MODEL=embed-multilingual-v3.0
COHERE_EMBEDDING_DIMENSION=1024
OPENROUTER_API_KEY=
OPENROUTER_CHAT_MODEL=google/gemma-4-31b-it:free
OPENROUTER_CHAT_FALLBACK_MODELS=openrouter/free
WEBSHARE_PROXY_USERNAME=
WEBSHARE_PROXY_PASSWORD=
GRAMMAR_SIMILARITY_THRESHOLD=0.45
GRAMMAR_RELATIVE_SIMILARITY_THRESHOLD=0.85

On Render, use the included render.yaml. It provisions a web service plus Postgres and sets COOKIE_SECURE=true. Dictation uses Webshare when both proxy variables are set; without them, transcript requests connect directly. The existing PROXY_USERNAME and PROXY_PASSWORD names are also accepted. The proxy config uses the library's rotating residential setup and its default retry count for blocked requests. Only uncached transcript fetch attempts count toward the default limits: 3 per client per hour, 10 per day, and 10 new transcripts globally per day. The shared catalog caps at 500 videos; transcript size is capped at 20,000 characters and 500 segments. Cached videos remain available after fetch quotas are reached. Reports are rate-limited and queued for admin review; they do not hide videos automatically. These limits can be changed with the DICTATION_* environment settings in app/core/config.py.

Do not run production with the default SQLite URL. Render web service disks are ephemeral, so sqlite:///./quiz.db can disappear on a new deploy. Create the service from render.yaml, or manually provision a Render Postgres database and set the web service DATABASE_URL environment variable to that database connection string. The app refuses to start in ENVIRONMENT=production when DATABASE_URL still points to SQLite.

Database Migrations

Schema changes are managed with Alembic:

uv run alembic upgrade head
uv run alembic revision --autogenerate -m "describe schema change"

For a fresh Supabase database, set DATABASE_URL to the Supabase Postgres connection string with SSL enabled, then run:

DATABASE_URL="postgresql://..." uv run alembic upgrade head

Grammar RAG

The grammar assistant uses a Render-friendly split:

  • Render serves the public API and widget only.
  • Curated Markdown notes live in data/grammar.
  • Source PDFs live in local data/grammar_pdfs and are described by data/grammar_sources.json.
  • Local/dev ingestion embeds notes with Cohere embed-multilingual-v3.0 and writes chunks to the same Supabase Postgres database used by DATABASE_URL.
  • OpenRouter generates answers with OPENROUTER_CHAT_MODEL, defaulting to google/gemma-4-31b-it:free.
  • OPENROUTER_CHAT_FALLBACK_MODELS is a comma-separated fallback list, defaulting to openrouter/free, so a temporary rate limit on the preferred free model does not disable the assistant.
  • Retrieval uses a calibrated absolute floor (GRAMMAR_SIMILARITY_THRESHOLD=0.45) and a relative floor (GRAMMAR_RELATIVE_SIMILARITY_THRESHOLD=0.85) so weak neighbouring chunks are dropped before generation.

The embedding model is pinned to Cohere embed-multilingual-v3.0 at 1024 dimensions. Changing the model is a schema migration plus full re-embed, not an environment-only change.

Set DATABASE_URL to the Supabase Postgres URL, run migrations there, then ingest:

UV_CACHE_DIR=/tmp/uv-cache uv run alembic upgrade head
UV_CACHE_DIR=/tmp/uv-cache uv run python scripts/ingest_grammar.py --changed-only

PDF ingestion uses pymupdf4llm locally and fails fast when a PDF appears scanned or produces too little text. Add PDFs to data/grammar_pdfs, then add entries to data/grammar_sources.json:

[
  {
    "id": "a2-grammar-book",
    "title": "A2 Grammar Book",
    "level": "A2",
    "topic": "praeposition_dativ",
    "path": "a2-grammar-book.pdf",
    "source": "private-pdf",
    "pages": [12, 18]
  }
]

Useful ingestion commands:

UV_CACHE_DIR=/tmp/uv-cache uv run python scripts/ingest_grammar.py --source-kind pdf --extract-only
UV_CACHE_DIR=/tmp/uv-cache uv run python scripts/ingest_grammar.py --source-kind pdf --changed-only
UV_CACHE_DIR=/tmp/uv-cache uv run python scripts/ingest_grammar.py --source-kind all --changed-only --delete-missing

Use --dry-run to inspect chunk ids and --delete-missing after removing corpus files. Ingestion must not run on Render free tier. data/grammar_pdfs/ is gitignored by default.

Run the retrieval harness with:

npm run eval

The current golden set is a starter set and should grow toward roughly 60 items. Before relying on an LLM judge for answer quality, hand-score 20 generation outputs and record the agreement rate here.

The grammar API accepts optional structured context for wrong answers and reading passages. The service uses this context to enrich retrieval while keeping answer generation grounded in the same cited chunks. Each grammar request is recorded in grammar_question_logs, which gives the admin side a future source for content-gap reports such as frequently asked no-match topics, weak routes, and chunks that are over- or under-used.

The app still creates missing tables on startup for local development, but production database schema changes should be applied with Alembic before deploying application code that depends on them.

GitHub Actions and Render

The workflow in .github/workflows/render-deploy.yml runs backend linting, backend tests, frontend audit, and frontend build before Render deploys. Keep Render's GitHub auto-deploy enabled; the workflow does not call a deploy hook, avoiding a second deploy/build for the same push.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages