Kristopher Baker iOS roots · Product systems · AI-assisted workflows
← Aoede

shipped · 2026.07.02 · 3 min read

Two kinds of wrong furigana

Furigana is the small kana Aoede prints over kanji so you know how to read them, and mine were wrong in visible ways. The trouble is that "wrong" turned out to be two completely different problems, and I could not fix either until I could tell them apart. So before touching the reader I built a harness, FuriganaQA, that runs Aoede's real furigana pipeline over a corpus of Aozora books and diffs every reading against the ruby the authors themselves marked up. Each mismatch gets sorted into one of two piles: my bug to fix, or a reading no analyzer was ever going to guess.

The first pile is the generator. Aoede reads kanji with OpenJTalk, and it was making avoidable mistakes. Long vowels came out spelled the way they sound instead of the way they are written (方 showed ほお instead of ほう) because the reading was pulled from the phonetic field; switching to the orthographic reading fixed the spelling while leaving the spoken audio untouched. Placement was off too: a run like 木の葉 grouped its kana into one blob over the whole word instead of こ over 木 and は over 葉. That is now mono-ruby by default, each reading centered over its own kanji, merging two readings into a group only when they would actually collide. And I added a dictionary tier: the JmdictFurigana dataset gives per-kanji placement and a validator that catches impossible readings and repairs them from JMdict, so 這入る resolves to はいる and 行衛 to ゆくえ.

The second pile is the interesting one, because it is not fixable by any amount of cleverness. Japanese is full of readings the author simply assigns: 地球 printed with ほし ("star") over it to mean Earth, 硝子 read がらす, 本気 read マジ. No analyzer will ever produce those, because they live in the author's intent, not in the characters. The only correct source for them is the author, so Aoede now preserves the ruby that ships in the book. It reads furigana straight out of Aozora's 《》 markup and EPUB's <ruby> tags, renders it over the kanji with precedence over anything OpenJTalk would guess, and, in the last phase, speaks it: 本気《マジ》 shows 本気 on the page but says マジ out loud, and 硝子《がらす》 is voiced がらす.

What I like about this one is that the QA harness did not just measure the fix, it defined the work. Its whole job is to draw the line between "the generator should have gotten this" and "the author meant something the generator can't know," and that line is exactly the two piles above. The reading you see over a word in Aoede now comes from the author when they said so, from a dictionary when there is a definitive answer, and from OpenJTalk only when nobody else has a better one.