高精度OCRデスクトップアプリの作り方 — PaddleOCR-VLとPyInstaller凍結の実践

アプリの作り方

高精度OCRデスクトップアプリの作り方 — PaddleOCR-VLとPyInstaller凍結の実践

画面の文字を拾って翻訳するには、スクショ → OCRサイト → 翻訳サイトと渡り歩くことになる。しかも「自分のPCだけで完結する配布アプリ」にしようとすると、PyInstaller の凍結でほぼ確実にどこかが壊れる。撮る・解析する・訳すを一つの窓に収め、その凍結の壁の越え方まで、稼働中の実装そのままで公開する。

目次
  1. このガイドで作るもの
  2. 例えば、使う場面はこう流れる
  3. 設計の起点:1コードベースで GPU/CPU を吸収する
    1. なぜ PaddleOCR-VL なのか
    2. GPU版とCPU版、どちらを配るか
  4. 作り方の要点
    1. 1. 起動を速くする:重い import を関数の中へ
    2. 2. 表と数式を読みやすく出す
    3. 3. PyInstaller 凍結の最大の罠:メタデータ不足
    4. 4. 巨大モデルは「同梱」でなく「配布物へコピー」
    5. 翻訳(DeepL)の用意は5分で終わる
  5. つまずきと対策:検証は「起動」でなく「OCR完走」まで
    1. 配布はフォルダごと渡す
  6. 使用ライブラリと順守メモ
  7. まとめ

このガイドで作るもの

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手で回る。

PaddleOCR-VL Desktop のメイン画面
完成版のメイン画面。左が画像の取り込み・編集(スクリーンショット/画像を開く)、右がOCR結果(テキスト / Markdown表示 / 翻訳)。認識モードは自動認識で、DeepL翻訳も同じ画面から呼べる。

設計の起点: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 で配布物を作る人なら題材を問わず踏む罠だ。役立てば幸いだ。


👉 関連: 開発実例:PaddleOCR-VL Desktopほかのアプリの作り方

背景除去アプリの作り方 — PyQt6+ONNX(BiRefNet)でローカル完結の透過ツールを作る背景除去アプリの作り方 — PyQt6+ONNX(BiRefNet)でローカル完結の透過ツールを作る前のページ

Grokで回すXメディア生成パイプラインの作り方 — 検索から画像・音声・動画まで自動化する次のページGrokで回すXメディア生成パイプラインの作り方 — 検索から画像・音声・動画まで自動化する

ピックアップ記事

  1. New Eden Intelligence Hub の作り方 — EVE Onl…

  2. 競艇予想AIの作り方 — LightGBMで「当たる順位」を学習させる実装ガイド…

  3. 高精度OCRデスクトップアプリの作り方 — PaddleOCR-VLとPyIns…

  4. Claude Mission Control の作り方 — Tauri+Pyth…

関連記事

  1. JavaScriptでRPGを作る③|CTB戦闘システムをDOM非依存で実装する【Archipelago Saga制作】

    アプリの作り方

    JavaScriptでRPGを作る③|CTB戦闘システムをDOM非依存で実装する【Archipela…

    FFX風のCTB(チャージタイム制)戦闘を、画面(DOM)から完全に切…

  2. マイク録音テストを数値化するアプリの作り方 — ノイズフロア測定の落とし穴と、どのオーディオインターフェースでも使える手順
  3. AIアセットでゲームマップを作る — 生成スケッチの透過切り出しからレベルセレクトまで
  4. JavaScriptでRPGを作る④|ワールドマップ・シーン遷移・セーブ(localStorage)【Archipelago Saga制作】

    アプリの作り方

    JavaScriptでRPGを作る④|ワールドマップ・シーン遷移・セーブ(localStorage)…

    ステージデータから蛇行するワールドマップを描き、クリアでロックを解放し…

  5. Transcription Studio の作り方 — Whisper をローカルで走らせ、エンジンを切り替えて文字起こしする

    アプリの作り方

    Transcription Studio の作り方 — Whisper をローカルで走らせ、エンジン…

    音声/動画をドロップするとローカルの Whisper で文字起こしする…

  6. ブラウザゲームを自動テスト&デプロイ⑥|puppeteerヘッドレス検証と静的配信【Archipelago Saga制作・完結】

    アプリの作り方

    ブラウザゲームを自動テスト&デプロイ⑥|puppeteerヘッドレス検証と静的配信【Archipel…

    1万行のゲームを手で全クリアせず「動いている」を機械に証明させる。pu…

注目

AIで、ここまで作れる

AIで作った2D RPGを、ブラウザでそのまま遊べます。その「作り方=最後まで完成させる進め方」も実例つきで公開中。

▶ ゲームを遊ぶやり方を読む

PR

お名前.com 独自ドメイン取得(PR)

独自ドメイン:お名前.com(本サイトで使用・PR)

  1. VS Code 1.128 のマルチチャット — 1セッションで2方針を並走する

    AI・テック動向

    VS Code 1.128 のマルチチャット — 1セッションで2方針を並走する…
  2. 背景除去アプリを作る③|「囲まれた背景」と「アイコンの穴」— 相反する2つの難所を実測で解く【背景除去Studio制作】

    アプリの作り方

    背景除去アプリを作る③|「囲まれた背景」と「アイコンの穴」— 相反する2つの難所…
  3. OpenAI GPT-Image-1.5 — 最大4倍高速・ディテール維持の精密編集をアイキャッチ量産に活かす

    AI・テック動向

    OpenAI GPT-Image-1.5 — 最大4倍高速・ディテール維持の精密…
  4. バイブコーディングの始め方|AIツールの選び方と最初のセットアップ【入門2】

    バイブコーディング

    バイブコーディングの始め方|AIツールの選び方と最初のセットアップ【入門2】
  5. xAI が Grok の新画像モデル「Aurora」を展開 — 自己回帰型で既存パイプラインの何を検証すべきか

    AI・テック動向

    xAI が Grok の新画像モデル「Aurora」を展開 — 自己回帰型で既存…
PAGE TOP

TAG CLOUD

ドラッグで回転・クリックでそのタグの記事一覧へ