はじめに
先日、英語ゲームをリアルタイムで日本語翻訳するデスクトップアプリを GitHub に公開しました。
※まだ v0.3.0という立ち位置で試作段階ではありますが、動くようになっています。
github:https://github.com/kabamodoki/game-translator
クイックスタート:https://kabamodoki.github.io/game-translator/
技術スタックを羅列するだけでは味気ないので、どんな失敗を経て今の形になったか を備忘録として残しておこうと思います。
※本記事はベースを書いた後にAIによって記載の補填をしてもらっています。
作った経緯
自分はゲームをよくするのですが、アーリーアクセスやデモ版だと日本語対応していないゲームが多い。自分は何となくニュアンスで読めるのですが、一緒にゲームをする友人には英語が読めない人もいる。そういった人向けに翻訳ツールがあればいいなと思ったのが始まりです。
1. 市場調査
「既存ツールがあれば使えばいい」と思い探してみました。翻訳系ツール自体はそこそこ存在していましたが、自分のニーズに合うものは見当たりませんでした。
自分が重視していた要件はこの2点です。
| 要件 | 既存ツールの状況 |
|---|---|
| セットアップのハードルが低い | GitHub の README を読んでコマンドを打てる人前提のものが多い |
| 外部 API を使わない(完全ローカル) | Google Translate API や DeepL API キーが必要なものがほとんど |
ターゲットとして想定していたのは「ツールを使うことに詳しくない、GitHub も API キーも全くわからない」ユーザーです。そういった人向けのものは見つからなかったので自作することにしました。
2. 技術スタック検討
まずユーザー体験から逆算して技術を選びました。
自分がイメージした体験は 「画面のここを翻訳しておいて、とドラッグで選ぶだけで、あとは勝手に翻訳して上に表示してくれる」 というものです。
処理の流れとしてはシンプルです。
画面解析 → 翻訳 → 画面に表示
- 画面解析: OCR(文字起こし)を使えば文字を取れそう
- 翻訳: 外部 API を使わないとなると、ローカル LLM サーバーが候補に上がりました。ちょうど仕事で llama.cpp の検証をしていたので採用
- 表示: PyQt6 でオーバーレイウィンドウを作る
3. 要件定義
自分用の整理として要件を書き出しました。
- 完全オフライン翻訳(外部 API 不使用)
- プログラミング知識なしでセットアップできる
- 対象言語は英語 → 日本語のみでOK
- ゲーミング PC のスペック前提でOK(万人受けは狙わない)
最後の点は重要で、ゲームをする人はそれなりのスペックの PC を持っているという前提を置くことで、LLM の負荷についての妥協点を上げられました。
4. 実装の旅路
実装には Claude(AI)を活用 しました。コードの製造はほぼ Claude に任せ、自分は「どういう方式にするか」「この方式で本当にいいか」という方式検討役に徹しました。
「こういう動きにしたい」を伝えてコードを出してもらい、動かして確認して、うまくいかなければ方針を変えてまた伝える、という進め方です。コードを書くスピードは上がりますが、方式選択の判断は自分でやらないといけない というのが実感です。
4.1 まず動くものを作った
最初に完成したのは OCR で画面の文字を読み取り、それを LLM に翻訳させる という構成です。
OCR を使って画面から文字を取り出す仕組みを初めて触りましたが、これが思ったより難しかった。
テストに使っていたゲーム(Ys シリーズ)の吹き出しフォントとの相性が悪く、Doctor が Octor に、the が fhe になるなど誤読が頻発しました。
とはいえ 画面の変化を検知して自動翻訳してくれる という体験自体は感動しました。
4.2 OCR の精度を上げようとした(失敗)
OCR の読み取り精度が低い原因はフォントが潰れているからではないかと仮定し、OCR に渡す前に画像を拡大して渡す という対策を試みました。
結果: 改善されませんでした。
フォントの問題ではなく、ゲーム特有の装飾フォントや背景との合成が原因のようでした。
4.3 LLM の呼び出し制御を入れた
OCR の精度問題を追いかけながら、別の問題も顕在化してきました。LLM の負荷が高すぎる 問題です。
画面変化を検知するたびに LLM に投げていたため、CPU が100%に張り付く状態が続きました。
そこで呼び出しを減らす制御を追加しました。
- 前回の文字列と今回の文字列を比較し、変化率が小さければスキップ
- 文字が認識できない(空文字)なら呼ばない
CPU 張り付きは解消されましたが、それでも遅く、精度も低いままでした。
4.4 試しにスクリーンショットをそのまま LLM に投げてみた
OCR の精度問題が解決しないので、試しに画面キャプチャの画像をそのまま LLM(Vision モデル)に渡して翻訳させてみた ところ、精度が大幅に上がりました。
OCR が読み間違えていた装飾フォントも、Vision LLM はそのまま正しく読み取れるのです。
「これは OCR いらないのでは?」という結論になりました。
4.5 OCR を完全にやめた
OCR を廃止し、画素差分で画面の変化を検知 → 落ち着いたら画像ごと LLM に送る という方式に切り替えました。
毎秒送り続けると負荷が大きいため、変化検知のステートマシンを作りました。
「平均ピクセル差」ではなく「変化したピクセルの割合」で判定するのがポイントで、静的な背景の上でテキストだけ変わるようなケースにも対応できました。
また、変化が延々と続いて永遠に送信されないケースを避けるため、8秒以上変化が続いた場合は強制的に LLM に送信する タイムアウト処理も入れています。
4.6 翻訳に 30〜40 秒かかる問題
Vision LLM に切り替えたことで精度は上がりましたが、翻訳に30〜40秒かかる という問題が発生しました。これではさすがに使い物になりません。
原因を調べたところ、VRAM が足りていなかった ことがわかりました。
「いいモデルを使えば精度が上がる」という漠然とした期待から Gemma 3 12B(7GB超)を使っていたのですが、VRAM を超えると CPU にスワップアウトされて途端に激遅になります。
モデルを Gemma 3 4B(約3.6 GB) に変更したところ、翻訳時間が 3〜5 秒 まで改善しました。
| モデル | ファイルサイズ | 必要 VRAM | 翻訳時間 |
|---|---|---|---|
| Gemma 3 12B | 7.3 GB | 約 7.8 GB | 30〜40 秒 |
| Gemma 3 4B | 2.5 GB | 約 3.6 GB | 3〜5 秒 |
5. リリース
動くものができたので PyInstaller で exe 化し、GitHub の Releases に公開しました。
セットアップは setup.ps1 を右クリック → PowerShell で実行するだけで、モデルと推論エンジンが自動ダウンロードされます。Python のインストールも不要です。
GitHubでのリリース公開はこれが初めてだったので、ちょっとドキドキしました。
最終的な構成
最終アーキテクチャ
[ゲーム画面]
↓ 1秒ごとにキャプチャ(mss)
[画素差分チェック]
↓ 変化あり・安定したら
[Vision LLM(llama-server + Gemma 3 4B)]
↓ 日本語テキストを返す
[オーバーレイウィンドウ(PyQt6)]
→ ゲーム画面の上に翻訳結果を表示
技術スタック
| 役割 | 採用技術 |
|---|---|
| GUI・オーバーレイ | PyQt6 |
| スクリーンキャプチャ | mss |
| 画素差分計算 | NumPy |
| LLM 推論エンジン | llama.cpp(llama-server) |
| LLM モデル | Gemma 3 4B Q4_K_M |
| exe 化 | PyInstaller |
まとめ
当初想定していた「OCR → LLM」から、最終的には「Vision LLM に画像ごと投げる」という全然違うアーキテクチャになりました。
試行錯誤のポイントをまとめると:
- OCR はゲームフォントとの相性問題がある → Vision LLM に丸投げするほうがシンプルで精度も高い
- LLM はモデルサイズより VRAM 収まりが大事 → VRAM内に収まる4Bモデルのほうが12Bより速い
- 変化検知は「平均差分」より「変化ピクセル割合」が安定する → テキスト変化を見逃しにくい