タイやカンボジアを旅するとき、現地語の短文をいくつか覚えていくと旅の質が変わります。翻訳アプリを開けば済む場面でも、自分の口から出るかどうかで相手の反応がまったく違う。

それで Anki(暗記アプリ)に会話フレーズを入れて覚えているのですが、既存の共有デッキがどうも自分に合いません。載っているのは「駅はどこですか」「これはいくらですか」といった教科書的な文で、自分が実際に使う場面と少しずつずれている。

だったら自分で作ればいい、ということで 日本語で書いた文からタイ語・クメール語のデッキを生成するツール を作りました。仕事とは何の関係もない、完全な趣味の道具です。

既存デッキが合わない、とは

たとえば私の旅だと、こういう文のほうが出番があります。

  • 辛くしないでください
  • 氷は入れないでください
  • この道でロットゥー乗り場に行けますか

「自分の旅のしかた」に紐づいた文です。食べ方の好み、移動の癖、泊まり方。これは他人が作ったデッキには入っていません。当たり前で、その人の旅ではないからです。

逆に言えば、日本語で自分の文さえ書ければ、あとは翻訳と音声を機械にやらせればいい。そう考えて作り始めました。

全体の流れ

パイプラインはこうなっています。

日本語マスター (content/phrases/*.yaml)
  ↓ translate      ローカル Ollama で翻訳候補を生成    → generated
  ↓ review         人間が TSV で確認して承認           → approved
  ↓ tts generate   ローカル MMS-TTS で音声生成         → generated
  ↓ review         人間が聞いて承認                    → approved
  ↓ build          Anki パッケージを生成               → .apkg
Anki Desktop → AnkiWeb → AnkiMobile

日本語を書く、機械が訳す、人が確認する、機械が読み上げる、人が聞く、デッキになる。それだけです。

ローカルで完結させた

このツールの方針として決めたのが、外部 API を一切呼ばないことでした。翻訳はローカルの Ollama、音声合成もローカルの MMS-TTS で動かしています。

なぜそうしたか。理由は身も蓋もなくて、趣味の道具に課金したくなかったからです。

翻訳する文は 244 文あり、しかも一度では終わりません。訳を直しては再生成し、音声を聞いては作り直す。この試行回数を「1 回いくら」で気にしながら回すのは、趣味として楽しくない。失敗し放題であることのほうが、品質より先に効いてくる場面があります。

ローカルなら、深夜に思いつきで全部作り直しても誰にも怒られません。手元のマシンが少し熱くなるだけです。

副次的な効果として、オフラインで動くのと、書いた文が外に出ないのもあります。個人的な旅のメモみたいな文が混ざるので、これは地味に安心でした。

ライセンスは確認しておくこと

ただしローカルだから何でも自由、ではありませんでした。

使っている MMS-TTS のモデルは CC-BY-NC 4.0、つまり非商用ライセンスです。生成した音声は個人的な学習に使うぶんには問題ありませんが、配布したり商用に使ったりはできません

後述するローマ字用の辞書も、元は Wiktionary なので CC BY-SA です。こちらもリポジトリには入れず、必要なら 1 コマンドで作り直せるようにしてあります。

「自分で使うだけだから」と流さず、README に明記しました。あとから「実はこのデッキ配れないの?」となるのが一番面倒なので、最初に確認しておくべき類のことだと思います。

機械は「承認」を書かない

設計でひとつだけこだわったのが、機械が approved を書けないようにすることです。

翻訳も音声も、生成された直後は generated という状態にしかなりません。人間が中身を確認して、はじめて approved に移ります。そして承認済みのものが再生成で上書きされることはありません

なぜかというと、ローカル LLM の翻訳は普通に間違えるからです。それらしいタイ語が出てくるので、確認せずに通すと、間違ったまま暗記することになります。暗記アプリで間違いを覚えるのは最悪です。せっかく覚えたものを忘れ直さなければいけない。

自動化していると「全部自動で回したい」誘惑が出てきますが、ここだけは人が通らないと先に進めない構造にしておいてよかったと思っています。

ローマ字はモデルではなく辞書から引く

カードにはタイ語の綴りと一緒に、読み方のローマ字を載せています。ここで一度やり直しました。

タイ語は声調言語です。ไม่ は mâi、ไหม は mǎi。声調記号ひとつで別の単語になります。つまり声調を間違えたローマ字は「だいたい合っている表記」ではなく、別の単語です。しかも暗記カードは、それを自信満々の顔で毎日見せてきます。

最初はこのローマ字も、翻訳と一緒にモデルに出させていました。「日本語話者が音読できる簡単な表記で」と頼んで。これがうまくいきませんでした。

モデルは一度に一かたまりずつ書くので、別のかたまりで自分が何と書いたかを見られません。結果、ครับ(丁寧語の語尾。ほぼ全文に出てきます)に 4 通りの綴りが生まれ、244 行のうち 142 行は声調記号がまったく付いていない状態になりました。

考えてみれば当然で、一貫性はデッキ全体の性質です。1 回のリクエストの内側には、それを保証する材料がありません。

なのでこの経路からモデルを外しました。いまは英語版 Wiktionary から作った辞書を引いています。Wiktionary は見出しごとに Paiboon 式のローマ字を載せているので、それをそのまま使います。単語の切り出しも同じ辞書に対して行うので、切り出せた断片は必ず読み方が分かっている断片になります。同じタイ語は必ず同じローマ字になり、作り直しても変わりません。

大事なのは、できないときは「できない」と言うようにしたことです。辞書にない語はその行に手を付けず、「引けなかった」として一覧に出します。文字を 1 つずつ読み下したりはしません。最初の実装ではそれをやってしまい、เช็คเอาท์(チェックアウト)を “ao tɔɔ” と読み上げていました。発音しない記号を、その記号の名前で読んでいたわけです。それらしい出力が返ってくるぶん、黙って間違えるより性質が悪い。

辞書を引いたら、承認済みのカードから誤りが出てきた

この作業には副産物がありました。人間が承認したはずのカードに、間違いが残っていたのです。

เขาปวดครับ という文がありました。「膝が痛いです」のつもりでした。ところが เขา は kǎo で「彼」。膝は เข่า で kào です。声調記号がひとつ落ちていて、このカードは他人の痛みについて教えていました

辞書と突き合わせて初めて見つかりました。人が目で見て承認したときには、通ってしまっていたわけです。ほかにも一人称が 1 箇所だけ ฉัน になっていたり(ほかは全部 ผม)、余計な記号が 30 個ほど紛れていたりしました。

前の節で「人が確認して初めて承認される」と書きましたが、人が見れば間違いが消える、という意味ではありませんでした。機械に承認させないことと、人の承認が正しいことは別の話です。結局、別の角度からもう一度照合するしかない。翻訳を作った経路と、読み方を引く経路を分けたことが、たまたまその照合になりました。

できないこと

正直に書いておくと、このツールには保証できないことがあります。

  • 翻訳品質は保証できません。 文字種のチェックはしていますが、それは「明らかな失敗の検出」であって品質保証ではありません
  • 発音の正しさは自動判定できません。 必ず耳で確認する必要があります
  • 対応言語はタイ語・クメール語だけ。GUI もありません

特に発音は、合成音声である以上どうしても限界があります。ネイティブ音声に差し替えられる作りにはしてあるので、いずれ現地で録らせてもらうのが理想です。

手作業が減っていく

日本語マスターは 244 文 / 13 カテゴリまで増えて、タイ語はすでに翻訳と音声の承認まで終わっています。デッキも出来上がりました。病気・マッサージ・盗難まわりの言い回しは、あとから「これは要るな」と気づいて足したものです。

最後まで手作業で残っていたのが、Anki への取り込みでした。.apkg を書き出したあと、ファイルをダブルクリックしてインポートダイアログを進める。訳をひとつ直すたびにこれをやるので、地味に効いてきます。

ここは AnkiConnect(Anki を HTTP API 越しに操作できるアドオン)で繋ぎました。ビルドから同期まで 1 コマンドで通ります。

uv run anki-deck-smith build --language th --force --import --sync

取り込み自体は Anki 自身にやらせているので、コレクションのデータベースを裏から触ることはありません。Anki が起動している最中に外から DB を書き換えるのは事故のもとなので、ここは譲れないところでした。

取り込んだあとは、期待したフレーズが全部あるか、重複していないか、音声がちゃんと入っているかを突き合わせて確認し、問題があれば失敗として終わるようにしています。インポートダイアログを経由しないぶん、目で確かめる機会がなくなるので、その代わりの検証です。

残っているのはクメール語です。タイ語で一周まわせたので、あとは同じ手順を繰り返すだけ……のはずですが、たぶん何か出てくるでしょう。

趣味の道具は、こうやって少しずつ手作業が減っていくのが楽しいところだと思っています。旅に間に合うかは、まだわかりません。

まとめ

  • 既存の共有デッキが合わないのは当たり前で、その人の旅ではないからです。日本語で自分の文さえ書ければ、あとは機械に回せます
  • ローカルで完結させた理由は品質ではなく、失敗し放題であることでした。試行回数が要る作業では、それが先に効きます
  • ローカルでもライセンスは自由になりません。音声も辞書も、配布できるかは別途確認が要ります
  • 一貫性はデッキ全体の性質なので、1 回のリクエストしか見えないモデルには保証できません。ローマ字のように「全体で揃っていること」が意味を持つ部分は、辞書のような決定的なものに任せるほうが向いています
  • そして、人が承認したから正しい、とは限りません。機械に承認させないことと、人の承認が正しいことは別の話でした。別の角度からもう一度照合する仕掛けがあると、すり抜けたものが見つかります