Why I built it

I started learning Spanish with Duolingo. It was good to a point, but I couldn’t really speak Spanish.

Then I found a paid AI language app, and it was a great example of how AI could help someone learn a language. I used it and liked it, but it was limited and very simple. Its top plan costs about $200 a year, and a cheaper plan comes with tighter limits.

By then I’d built a lot of apps with LLM integrations, so building my own seemed easy. I built it with spec-driven development. Open Language is the result: spoken role-play conversations with an AI partner in the language you’re learning. I use it to practice my Spanish.

It runs locally by default for a specific reason. Speaking practice means recording yourself making mistakes, over and over, and that audio shouldn’t have to go anywhere.

How it works

You pick a role-play scenario, like buying a train ticket or calling an estate agent, and speak or type in the language you’re practicing. The partner answers in character, out loud. Every message has learning tools: grammar check, translation, alternative phrasing, and word lookup. Words you save become flashcards with spaced repetition. Suggested responses are only hints, so you still have to say or type them yourself. Spanish, German, and Italian are supported today.

It’s built to run on your own computer, not as a hosted service. Offering it to other people as a service would be a significant architecture change.

Every AI piece sits behind an interface

Each capability has a provider interface, and the default provider runs on your machine:

  • Speech to text: faster-whisper, in process.
  • Conversation partner: Ollama serving llama3.1 8B by default, or Claude through your own signed-in Claude Code, if you choose it.
  • Voice: Piper, from local voice files.
  • Storage: one SQLite file.

With the defaults, the only network call the app makes is to Ollama on localhost. Choosing Claude sends the text of your conversations and learning-tool requests to Anthropic under your own account. Audio never leaves, because speech recognition and the voice have no cloud provider. The app never asks for or stores an API key, and it refuses an API-key login so usage always counts against your Claude plan.

The backend is FastAPI with SQLAlchemy, and the frontend is React, TypeScript, and Vite, talking to the API over fetch and server-sent events. Whisper, Piper, and model calls all block, so each one runs off the event loop, and generated audio and LLM explanations are cached so they’re never computed twice.

Podcasts

Podcasts show what I like most about this setup: when I have an idea, I can build it into the app. Instead of a role-play partner, you talk with one host, join a panel of two, or just listen while two hosts talk. Shows can be ready-made, generated from an idea you type, or picked at random, and any conversation can be summarized in its own language or in English. It’s a fun, interactive way to have a conversation.

Languages are data

Each practice language is a generated data file plus an evaluation set, written by a language kit that a Claude Code skill walks through: check whether the language can be supported, scaffold a pack, validate it, apply it, and benchmark it. A language has to be written left to right with spaces between words, be supported by Whisper, and have a single-speaker Piper voice. Italian, the third practice language, came in through the kit.

Built from specs

Every feature starts as a specification in Spec Kit, under a written constitution: tests come first, the build fails below 90% coverage, and every external dependency sits behind an abstraction. There are ten numbered feature specs so far, from the first role-play chat to recurring podcast shows.

The hard part

Most of my effort went into two things: pinning down the first use case, and then making sure the solution wasn’t heavily coupled to one large language model provider. I was instructing an AI model to build the app, so an architecture that could grow had to be something I asked for. The challenge was keeping the app generic and still useful.

The separation was there from the start. Ollama always sat behind its own interface, because my other projects had already taught me not to tie anything to a specific model or model provider. Claude arrived as a second conversation partner in September 2026, and the same day the project’s constitution gained a new principle, Provider Independence: every AI capability behind an interface, local by default, cloud opt-in, no credentials held, and no silent fallback.

The spec-driven process itself was the easy part. I’d prompt, run through the process, wait for an implementation, and then go back and forth on cleanup to fix mishaps. Manual verification is still needed, especially as a specification grows more complex. Not everything turns out the way you thought it would, and that’s a chance to make it better.

Choosing a model was easy. Not depending on one was the work.

What I learned

I’d done a fair amount of spec-driven development before this, so the surprise was somewhere else. Small models can do a lot, and it’s surprisingly easy to build a powerful application on language models.

They also have a limit, and that part was familiar. I’d already hit it running transcription and cleanup locally in my ASR and LLM text cleanup pipeline, and I wrote about it in detail in Running LLMs Locally.

Grammar correction hit the same wall. On a fixed 20-sentence benchmark, llama3.1 8B caught all 10 real errors, and it also flagged all 10 correct sentences. Some of its fixes were wrong in their own right: it changed hermana to hermano. Rewording the prompt didn’t fix it, and the larger models I tried wouldn’t fit in the 8 GB of VRAM on my RTX 3070 Ti. Mistral fit, and flipped the problem: it flagged at most one correct sentence but missed three of the ten real errors, including gender agreement and the subjunctive. Corrections ship as experimental, with a note in Settings that they can be wrong.

None of that surprised me. A correction feature isn’t going to be nearly as accurate on a smaller model.

If I started over, I wouldn’t change much. I might start with Claude instead of a local model, since it’s faster and runs on my plan limits, for now.

All you need is the right interface.

What’s next

Benchmarking comes first. The correction benchmark, for one, hasn’t been run against Claude yet.

After that, I want to share it. The code is already open, and I’d like feedback and contributions from the people who use it. I think it’s a genuinely compelling option for learning a language. I’m also going to add a lot more languages through the language kit, because it’s that easy.

What amazes me about language models is how they bridge the gap between languages. Learning one shouldn’t cost an insane amount of money when the technology is this ubiquitous, and I want to help bring those paywalls down.