I’ve been kicking myself since launching Reverse Wiktionary because one of the first friends I sent it to said, “You should add pronunciations.” The Wiktextract data includes both International Phonetic Alphabet (IPA) UTF-8 pronunciations and, for many words, URLs for user-created audio. This should have been a no-brainer!

Using the IPA for text-to-speech became a small side project: ipa-to-mespeak , a portable encoder that translates Unicode IPA into the phoneme notation expected by meSpeak/eSpeak-style speech synthesis. In Reverse Wiktionary, that makes it possible to fall back to generated pronunciation audio when a recording is not available but a usable IPA transcription is.

Why This Became Its Own Project

IPA looks like text, but treating it like ordinary text is a quick way to get into trouble. A single displayed sound can involve multiple Unicode code points. Some marks combine with the symbol before them. Stress, length, ties, syllable breaks, and diacritics all carry meaning. If the encoder is just a string replacement table, it is easy to produce something that seems plausible while silently handling edge cases incorrectly.

When I looked for prior open implementations, I found useful starting points, but not the kind of audit trail I wanted. At least one older public tool documented that its mapping was incomplete and might contain mistakes in its interpretation of the conversion scheme. That is an understandable state for a small open project, but it was not the right foundation for something I wanted to run automatically over a large multilingual dataset.

So the project became less about writing a clever converter and more about building a defensible rule set.

Agentic Research, With Receipts

The workflow I used was deliberately evidence-heavy. I gave a Codex agent the task of compiling conversion rules, but with an important constraint: rules needed supporting evidence from reliable sources before they could be treated as valid.

That sounds simple, but it changed the shape of the work. This was not a “generate a table and hope” task. It became 1,000s of mini research tasks requiring their own artifacts, citations, tests, and tests for the tests.

I made the agent build that defensibility into the workflow at three levels: a correctness test suite to validate the compiled encoder’s outputs, a validation test suite to mechanically check rule citations against machine-parsable sources, and a meta-validation test suite to verify that the validation tests themselves could not be faked (yes, three levels!). I even had the Codex agent write a report on its findings . Pretty interesting stuff! 🤓

All this made the task longer and more expensive than a normal coding pass. The agent had to keep track of what had been checked, what still required more research, and which mini-task it was currently working on. To make that feasible across a long-running research task, I had Codex create an external memory: essentially a structured working log for sources, rule candidates, unresolved questions, and decisions.

That was the part I found most interesting. The agent was not just generating code. It was maintaining a research state outside the immediate context window so it could return to the problem, organize partial progress, and avoid backtracking.

The Result

The output is a fast Rust encoder that can be packaged inside a Python application. That matters for Reverse Wiktionary because the pronunciation pipeline needs to be practical, repeatable, and easy to deploy alongside the rest of the data processing stack.

This is also where I get to brag a little. The repo’s language support profile currently reports 22 human-language rows, 25 runtime voices, 1,727 compiled rules, and 2,825 rule-source references. Coverage is uneven, because language data is uneven, but the strong cases are legitimately strong: Spanish, Italian, Catalan, Esperanto, Polish, Hungarian, and Romanian are all over 99% current support. German, Czech, French, Finnish, Greek, Latin, and Dutch are all over 94%. Chinese was the hardest case by far: it got the most rules, separate Mandarin and Cantonese runtime routes, and 70,613 supported entries in the stored coverage run, for 44.12% current support across a much messier 160,057-entry dataset.

The result is not a replacement for human pronunciation recordings. When a real recording exists, that should win. But for entries where Wiktionary has IPA and no audio, generated speech is a meaningful improvement over silence.

I also updated the NGINX configuration to manage server- and browser-side caching, so the app can serve pronunciation audio without repeatedly calling back to the Wiktionary archives.