Almost every language app teaches Brazilian Portuguese. That's fine for Rio, but in Lisbon people say tu, comboio and estou a falar, and swallow half their vowels, and Brazilian lessons don't prepare your ear for that. I wanted a course that was European from the first lesson, and one that works on a plane or a train with no signal.

What it does

Speeku is a gamified course from A1 to B1:

  • Nearly 1,000 bite-sized lessons, over 11,000 exercises of more than 50 kinds: pick the picture, build the sentence, fill the gap, play the scene, fill the verb table
  • Every word and sentence spoken in European Portuguese, by a cast of six voices
  • A 7,000+ entry dictionary with European pronunciation and full verb tables, searchable on the phone
  • Speaking drills that check you said the right word, judged on the device
  • Flashcards that bring back the words you're closest to forgetting

It's a one-time payment, and fully offline: the Android build doesn't even request the INTERNET permission.

How we built it

  • One codebase, two runtimes. The app is React Native + Expo. On the web/dev side it talks to a Python WebSocket backend; on Android the same frames are answered on the phone by a TypeScript runtime over expo-sqlite, so what we test on the web is what ships.
  • No speech is synthesized at runtime. Every line is rendered ahead of time (ElevenLabs), loudness-matched and played from a file. A line with no recording is silent and logged, and the release gate refuses a build with no clips.
  • The speaking gate asks which word was said, not how well. espeak-ng turns the expected answer into phonemes, and a wav2vec2 phone model (Apache-2.0, running on ONNX Runtime) scores the recording against them, with Silero VAD finding where speech starts and ends. On Android it runs in a Kotlin native module; on the web, in WebAssembly.
  • Our own pronunciation rules. We vendor espeak-ng and maintain European Portuguese rules ourselves, following Mira Mateus's phonology, so the IPA in the dictionary matches what you hear in Lisbon.
  • The dictionary is built from a checked-in JSONL file into SQLite. The phone's search is a statement-by-statement port of the Python one, held to it by parity tests over the real data.
  • Lessons are data: JSON files that are linted for teaching rules (no word tested before it's taught, every drill winnable by knowing the language) and reviewed on internal "desks" before they ship.

Challenges we ran into

  • Speed on the web. The speaking gate took 1.9 s to judge two seconds of audio. ONNX Runtime silently falls back to one thread unless the page is cross-origin isolated; with the right COOP/COEP headers it drops to ~0.3 s.
  • The phone draws less than your browser. An SVG icon that looks perfect in every viewer rendered as a solid black silhouette in react-native-svg, because it relied on a <style> block. Now a test renders every icon the way the phone will.
  • Honest speech checking. A phone model can't grade an accent, so the app never pretends to: it tells you which word it heard, nothing more.
  • Getting the variety right. Portuguese corpora and tools default to Brazilian. Every lesson had to be actively written for Portugal rather than merely allowed to be European.

What we learned

A green test suite isn't the same as a working app: some of our worst bugs (an empty speech bubble, a silent speaking drill) were invisible to every test and obvious the moment someone opened the app. And going offline-first, with no server to lean on, forces you to be explicit about every byte you ship.

What's next

More European Portuguese beyond B1, the iOS version, and courses for other languages, including Swiss German, German and Brazilian Portuguese.

Built With

Share this project:

Updates