Home

Android app · Kotlin · version 4.2

Spanish Tutor

An offline Android app that takes an adult learner from Spanish B2 to C1. The screens are in Spanish with French alongside, and a language model on the phone corrects what she writes. I made it for one learner, and her feedback shaped it.

  • Kotlin
  • Jetpack Compose
  • llama.cpp on-device
  • Qwen3.5 2B
  • BM25 search
  • 280 lessons
  • 9 exercise types
statusv4.2Android, shaped by two rounds of the learner's feedback
curriculum68 topics280 lessons as validated JSON
questions answered223searchable in Spanish or French
network needednonemodel, audio and search on the phone

Web demo. The Android app recreated for the browser, with lessons taken from it. The app itself runs offline on Android.

Text size

Loading the lessons…

Here the tutor runs on Gemma 4 26B on Cloudflare's free tier; in the app it runs Qwen3.5 2B on the phone, offline.

How it's built

A small model, kept on a short leash

The tutor has to run offline on a phone, so most of the work happens in plain code around a 2B model. The curriculum is checked, the search needs no model, and the reply format is simple enough for the model to keep.

Compose and MVVM
One ViewModel per screen exposes its state as a flow; screens are plain composables. Navigation, Media3 for audio and kotlinx.serialization for the curriculum.
llama.cpp through JNI
A small C++ session layer runs a GGUF model on the phone. Qwen3.5 2B in Q4_0 (1.2 GB) is the recommended one; llama.cpp is vendored, so the build downloads nothing.
Prompt cached between turns
The hidden prompt depends only on the active lesson and earlier messages are sent back unchanged, so the engine reuses its cache and only reads the new message. Per-turn hints travel as a hidden note on the learner's message.
A format a 2B model can keep
Line one is «Corrección: … (reason)» or ✔, then at most four sentences. A line prefix survives streaming far better than JSON, and the correction box appears as soon as the first line arrives.
Curriculum as checked JSON
68 topics and 280 lessons in JSON, validated against a JSON Schema by a Python script that also checks cross-references, audio ranges and highlights. A second script checks that the Kotlin models read each field.
Search without a model
BM25 per field with accent folding, a light stemmer, prefix and typo matching, and a French to Spanish expansion table. 80 JVM unit tests, 25 for search; on the real content all 223 questions rank their own answer first and all 1,467 aliases reach the top three.
Goals checked by rules
Role-play goals are regular expressions run before the model, so the ✓ chips are instant and reproducible, and the missing ones become a hint for the tutor.
Narration
Listening dialogues were synthesised offline with Piper voices through sherpa-onnx, then aligned sentence by sentence for the synced transcript.

The web version is plain JavaScript modules with no framework. Search, grading, the goal matcher and the reply parser are ported from the Kotlin and tested in Node against the app's own test cases; the search port gives the same 223 of 223 and 1,467 of 1,467 on the real answers. The live tutor builds the same hidden prompt on the site's Cloudflare Worker, with one change: the lesson's sample corrections are labelled as other learners' errors, because a model on test copied them onto correct sentences. Gemma 4 was chosen over Qwen3 30B by a 20-turn test (site/tests/tutor-eval.mjs): Gemma gave the right verdict and correction every time, and Qwen showed a wrong form, such as «Está morena», as the correction in 6 of 20.

Notes

  • One topic of 68 is here, with its four lessons, its exam and all 223 answers of the question bank. The home tiles for the other packs are left out.
  • The tutor here is a much larger model on a server, so it keeps the correction format more reliably than the 2B model on the phone, and it needs a connection. It can still miss an error: it corrects at most two per turn, as the app asks. It shares a small free daily allowance, so it may run out.
  • In the app, any word opens an offline Spanish to French dictionary. Here the words of the vocabulary lesson do.
  • On the phone, 🔊 uses Android's speech engine and there is a full spoken mode with the tutor. Here 🔊 and 🎤 use your browser's own speech features, and only where it has them.
  • The app also plays the course CDs and shows pages of the books the learner owns. They belong to their publisher and are not part of this demo.
  • Not recreated: the dictionary tab, medals, looping a transcript line on long press, and the voice conversation mode.
  • Progress here is stored in this browser only.

Credits

The dialogue voices are machine-generated Piper voices: es_ES-sharvard-medium, trained on the Sharvard Corpus (Aubanel, García Lecumberri & Cooke 2014), licensed CC BY 3.0, and es_ES-davefx-medium, CC0. The typeface is Atkinson Hyperlegible by the Braille Institute, under the SIL Open Font License.