画面の文字を拾って翻訳するには、スクショ → OCRサイト → 翻訳サイトと渡り歩くことになる。しかも「自分のPCだけで完結する配布アプリ」にしようとすると、PyInstaller の凍結でほぼ確実にどこかが壊れる。撮る・解析する・訳すを一つの窓に収め、その凍結の壁の越え方まで、稼働中の実装そのままで公開する。
目次
このガイドで作るもの
PaddleOCR-VL-1.5 を使った Windows 向けの高精度 OCR デスクトップアプリ(PySide6 製)。スクリーンショット撮影 → 編集 → 解析 → 翻訳までを一つの窓で完結させ、表や数式も崩さずに表示する。NVIDIA GPU 機でも非搭載機でも動くよう、1つのコードベースから GPU版/CPU版の2エディションを配布できる形にする。
構成はシンプルに分けてある。OCR推論は core/ocr_engine.py、結果表示は ui/ocr_result_widget.py、翻訳は utils/translator.py、数式描画は utils/math_render.py、配布は build_exe.spec / build_exe.py。役割ごとにファイルを分けると、後で「どこを直すか」が一目で分かる。
例えば、使う場面はこう流れる
操作の流れを1つ通しで。海外ゲームの英語ウィンドウを読みたい場面だ。①ホットキーでスクリーンショットを撮り、読みたい範囲をドラッグで切り取る。②切り取った画像がアプリに入り、解析ボタンでOCRが走る。③表や数式も崩れずにテキスト化され、④そのままDeepLで日本語訳が横に並ぶ。ここまでで、ブラウザもOCRサイトも一度も開いていない。ゲームに限らず、PDFのスクショ・エラーダイアログ・紙の資料を写した写真でも、同じ4手で回る。

設計の起点:1コードベースで GPU/CPU を吸収する
配布先のPCに GPU があるとは限らない。そこでコードは分岐させず、起動時にデバイスを自動判定する。要は paddle.is_compiled_with_cuda() 一発で、GPU版ホイールが入っていれば GPU、CPU版なら自動で CPU に落ちる。
# core/ocr_engine.py — 起動時に一度だけ、正しいデバイスで初期化する
import paddle
# CPUビルド(Intel Arc/非NVIDIA機)はCPU専用wheelを積むので、
# CUDA が使えなければ自動で CPU にフォールバックする
use_gpu = USE_GPU and paddle.is_compiled_with_cuda()
device_str = 'gpu' if use_gpu else 'cpu'
paddle.device.set_device(device_str)
print(f"[OCREngine] Preparing PaddleX on {device_str}...")
# モデルは配布フォルダ内の絶対パスから読み込む
official = MODELS_DIR / "official_models"
config["SubModules"]["LayoutDetection"]["model_dir"] = str((official / "PP-DocLayoutV3").resolve())
config["SubModules"]["VLRecognition"]["model_dir"] = str((official / "PaddleOCR-VL-1.5").resolve())
self._pipeline = paddlex.create_pipeline(pipeline=temp_yaml_path)
コードは共通のまま、ビルド設定(spec/venv)だけを GPU用・CPU用に分ける。機能差のない2エディションを、1つの実装から保守できる。
なぜ PaddleOCR-VL なのか
OCRエンジンは他にもあるが、PaddleOCR-VL-1.5 を選んだ決め手は「VL=視覚言語モデル」であることだ。従来型のOCRが「文字の形を1つずつ拾う」のに対し、VL系はレイアウト検出(PP-DocLayoutV3)と組み合わせて文書の構造ごと理解する。表はセルの並びを保った表として、数式は数式として出てくる。ゲーム画面や技術文書のような「文字がレイアウトの中に埋まっている」素材ほど、この差が効く。重みが Apache-2.0 でオフライン配布できることも、ローカル完結という目的に合っていた。
GPU版とCPU版、どちらを配るか
目安は単純で、NVIDIA のGPUが載った機なら GPU版、それ以外は CPU版。CPU版でも1枚の解析は待てる速度で動くので、「動くかどうか」の心配は要らない。差が出るのは連続処理の快適さだ。配布時は相手の環境が分からないことも多いので、迷ったら CPU版を渡しておけば確実に動く。
作り方の要点
1. 起動を速くする:重い import を関数の中へ
OCRアプリは起動が遅いと使われない。犯人はたいてい「モジュール先頭の重い import」だ。翻訳ライブラリ deepl は依存(requests/ssl 等)込みで cold start に約1.5秒かかる。これをモジュール scope に置かず、使う直前まで遅延 import する。
# utils/translator.py — deepl は cold import に ~1.5s。使う直前で import する
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import deepl # 型チェック専用(実行時は読み込まない)
class Translator:
def initialize(self) -> bool:
if not self._api_key:
self._init_error = "APIキーが設定されていません"
return False
import deepl # 初期化する瞬間に初めて import
try:
self._client = deepl.DeepLClient(self._api_key)
self._client.get_usage() # 疎通確認
self._is_initialized = True
return True
except deepl.AuthorizationException:
self._init_error = "APIキーが無効です"
return False
数式描画の matplotlib も同じで、数式を含む結果が来たときだけ import する。「重い import は関数の中へ」。地味だが起動体感に最も効く。
2. 表と数式を読みやすく出す
OCR結果は素のテキストのままだと表が崩れ、数式は記号の羅列になる。表は markdown-it-py で描画する。PaddleOCR-VL は生の <table> を吐くことがあるので html=True で素通しし、GFM の表・打ち消しを有効にする。
# ui/ocr_result_widget.py — 生 <table> を活かしつつ GFM 表を描画
from markdown_it import MarkdownIt
def render_markdown(text: str) -> str:
md = (
MarkdownIt("commonmark", {"html": True, "breaks": True})
.enable("table")
.enable("strikethrough")
)
return md.render(text)
数式は matplotlib の mathtext で LaTeX → PNG に変換する。LaTeX のインストールもネットワークも要らない。mathtext が解釈できない記法(\begin{align} 等)は None を返し、ソース表示へフォールバックさせるのが安全だ。
# utils/math_render.py — LaTeX数式を matplotlib mathtext で PNG 化
def latex_to_png(latex: str, *, color="#e8e8e8", fontsize=16.0, dpi=160):
snippet = latex.strip().replace("\n", " ")
if not snippet:
return None
try:
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg
fig = Figure(figsize=(0.01, 0.01))
FigureCanvasAgg(fig)
fig.text(0, 0, f"${snippet}$", fontsize=fontsize, color=color)
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=dpi, transparent=True,
bbox_inches="tight", pad_inches=0.06)
return buf.getvalue()
except Exception:
return None # mathtext非対応の記法 → Noneでソース表示にフォールバック
3. PyInstaller 凍結の最大の罠:メタデータ不足
ここが本題だ。PaddleOCR(paddlex)系は、凍結 exe の中で依存パッケージのメタデータ(importlib.metadata)を読もうとする。これが欠けると paddlex 内のガード(例:Jinja2 の有無判定)が誤作動し、NameError で落ちる。しかもこの経路はOCRを実行した瞬間に初めて走るので、起動確認だけでは絶対に気づけない。
対策は spec で、インストール済み全パッケージの dist-info を copy_metadata で同梱すること。ただし torch / tensorflow 等の除外フレームワークは飛ばす(同梱すると paddlex がそれらを import する経路に入ってしまう)。
# build_exe.spec — 全パッケージのメタデータを同梱(凍結時の NameError 対策)
from PyInstaller.utils.hooks import copy_metadata
import importlib.metadata as ilmd
skip = ('torch', 'tensorflow', 'keras', 'nvidia')
for dist in ilmd.distributions():
name = (dist.metadata['Name'] or '').strip()
if not name or name.lower().replace('-', '_').startswith(skip):
continue
datas += copy_metadata(name)
# torch は明示除外。paddle は torch 不要で、torch の CUDA/cuDNN DLL を積むと
# Windows が誤って torch 側を掴み WinError 127 で落ちる
excludes = ['torch', 'torchvision', 'torchaudio', 'tensorflow', 'keras']
4. 巨大モデルは「同梱」でなく「配布物へコピー」
OCRモデルは数百MB級。PyInstaller のバンドルに押し込むのではなく、ビルド成功後に models/ を配布フォルダへコピーする「オフラインモデル配布」にした。アプリは配布フォルダ内の絶対パスからモデルを読む。PaddleOCR-VL の重みは Apache-2.0 なので同梱配布は可能だが、その際は重みのライセンス文・帰属表示(NOTICE 等)を配布物に同梱する。
# build_exe.py — ビルド成功後に dist へモデルをまるごとコピー
result = subprocess.run([sys.executable, "-m", "PyInstaller",
"--clean", "--noconfirm", str(spec_file)])
if result.returncode == 0 and MODELS_DIR.exists():
shutil.copytree(MODELS_DIR, dist_dir / "models") # 数百MBのモデルを同梱
GPU版では、CUDA/cuDNN を nvidia/<pkg>/bin/ のレイアウトを保ったまま同梱しないと paddle が DLL を解決できない点も要注意だ。
翻訳(DeepL)の用意は5分で終わる
翻訳機能は DeepL API を使う。DeepL のサイトでアカウントを作り、APIキーを発行してアプリの設定欄に貼るだけだ。無料プランにも月あたりの翻訳量の枠があり、個人のスクショ翻訳の範囲なら無料枠で十分回る。キーが無い状態でもOCR自体は動くように、翻訳は「あれば使う」オプション扱いにしてある(initialize() が失敗を握りつぶさず、エラーメッセージだけ返して他の機能を止めない作り)。
つまずきと対策:検証は「起動」でなく「OCR完走」まで
凍結アプリで最も危険なのは、「起動した=動く」と判断してしまうことだ。前述のメタデータ NameError のように、実際にOCRを走らせて初めて踏む経路がある。配布前の確認は必ず、画像を入れてOCRを最後まで完走させ、翻訳・表・数式まで表示させること。起動スモークだけで「動作確認済み」とは言わない。
配布はフォルダごと渡す
ビルドが済むと、配布物は「exe+models/+関連DLL」が入った1つのフォルダになる。渡すときはフォルダごと zip にして渡す。exe 単体を抜き出して送ると、隣にあるはずのモデルが見つからず起動後の解析で落ちる。受け取った側の手順は「zip を展開して exe をダブルクリック」だけで、インストーラも管理者権限も要らない。モデルを更新したいときも、models/ の中身を差し替えれば exe はそのままでいい。アプリ本体とモデルの寿命を分離できるのも、「同梱でなくコピー」方式の利点だ。
使用ライブラリと順守メモ
- PaddleOCR / PaddleX … Apache License 2.0。
- PySide6(Qt for Python)… LGPLv3 等。配布形態に応じてライセンス条件を満たすこと。
- markdown-it-py … MIT/matplotlib … matplotlib ライセンス(BSD系)。
- DeepL 翻訳は別途 APIキー(無料/有料プラン)の取得と DeepL 利用規約の順守が必要。
- OCRモデルの重み(PaddleOCR-VL-1.5)… Apache License 2.0(再配布可)。同梱して配布する場合は、重みのライセンス文・帰属表示(NOTICE 等)を配布物に含めること。
掲載しているコードは、本アプリの稼働実装からの抜粋だ。
まとめ
このOCRアプリの肝は4つ。① GPU/CPU を1コードベースで吸収、② 重い import は関数の中へ、③ 凍結はメタデータで落ちる(copy_metadata + 検証は OCR 完走まで)、④ 巨大モデルは配布物へコピー。特に③は、PyInstaller で配布物を作る人なら題材を問わず踏む罠だ。役立てば幸いだ。
この制作で使ったもの PR
🖥 サーバ/ドメイン
🤖 使ったAIツール(公式)
📚 技術書(Amazon)
👉 関連: 開発実例:PaddleOCR-VL Desktop ・ ほかのアプリの作り方



