てけとーぶろぐ。

ソフトウェアの開発と、お絵かきと、雑記と。

Python + Gemini API で画面OCRツールをつくる

PCの画面に表示されている文字列を文字列としてコピーしたいが文字列としてコピーできないようになっている。 ということがたまにあるかと思います。

いい例が挙げられませんけど、例えばウィンドウのタイトルバーに書かれている文字列はコピーできません。

そんなときは目で見てタイプするわけですけど数が多かったり、文字列が長かったりすると面倒なわけで…。

そこでそれをGeminiでつまりAIで行うツールをつくろうという話です。

やることは要はスクリーンショットに対するOCR、文字認識ですね。

ツールを起動してマウスドラッグで画面内の文字認識したい範囲を指定すると文字認識がなされて結果がクリップボードに書き込まれます。 あとはメモ帳などにペーストするなりなんなり。そんなツールをつくります。

では早速つくっていきましょう。

作成の流れは以下の記事と同じです。

Python + Rye + Gemini API でAIチャットを作る - てけとーぶろぐ。

Ryeのインストール

セットアップを簡単にするためPythonのパッケージマネージャーにはRyeを使います。

Ryeをインストールしていない場合は以下の「Installation Instructions」に従ってインストールします。

rye.astral.sh

プロジェクトの作成

プロジェクトを作成します。

$ rye init screen-ocr
$ cd screen-ocr
$ rye sync
パッケージのインストール

Gemini API 用の Python SDK が含まれる google-generativeai パッケージなど必要なパッケージをインストールします。

$ rye add google-generativeai
$ rye add pillow
$ rye add pyautogui
$ rye add pyglet
$ rye add pyperclip
$ rye sync
Pythonファイルの作成

screen-ocr/main.py として以下の内容でPythonファイルを作成します。

import os

import google.generativeai as genai
import pyautogui
import pyglet
import pyperclip
from PIL import Image
from pyglet import shapes


class SimpleScreenshotSelector(pyglet.window.Window):
    def __init__(self, original_image: Image):

        # 以下のコードだとマウスカーソルが左上に持っていけない問題が発生した
        # width, height = original_image.size
        # super().__init__(width, height, fullscreen=True)

        screen = pyglet.display.get_display().get_default_screen()
        width, height = screen.width, screen.height

        super().__init__(
            width,
            height,
            "Screenshot Selector",
            fullscreen=False,
            style=pyglet.window.Window.WINDOW_STYLE_BORDERLESS,
        )

        # 何もしないとウィンドウがUbuntuのDockや画面上部のTop Barに
        # 重ならないような位置に表示されるので左上に移動する
        self.set_location(0, 0)

        self.set_mouse_cursor(self.get_system_mouse_cursor(self.CURSOR_CROSSHAIR))

        # Pillow → Pyglet変換
        image_data = (
            original_image.transpose(Image.FLIP_TOP_BOTTOM).convert("RGB").tobytes()
        )
        self.texture = pyglet.image.ImageData(width, height, "RGB", image_data)

        self.original_image = original_image
        self.start_pos = None
        self.current_pos = None
        self.is_dragging = False

        self.batch = pyglet.graphics.Batch()
        self.selection_rect = None

    def on_draw(self):
        self.clear()
        self.texture.blit(0, 0)

        if self.is_dragging and self.start_pos and self.current_pos:
            x1, y1 = self.start_pos
            x2, y2 = self.current_pos
            rect_x = min(x1, x2)
            rect_y = min(y1, y2)
            rect_w = abs(x2 - x1)
            rect_h = abs(y2 - y1)

            self.selection_rect = shapes.Rectangle(
                rect_x, rect_y, rect_w, rect_h, color=(255, 0, 0), batch=self.batch
            )
            self.selection_rect.opacity = 100
            self.batch.draw()

    def on_mouse_press(self, x, y, button, modifiers):
        if button == pyglet.window.mouse.LEFT:
            self.start_pos = (x, y)
            self.current_pos = (x, y)
            self.is_dragging = True

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        if self.is_dragging:
            self.current_pos = (x, y)

    def on_mouse_release(self, x, y, button, modifiers):
        if button == pyglet.window.mouse.LEFT:
            self.is_dragging = False
            image = self.get_selection()

            if image:
                text = perform_ocr(image)
                print("OCR Result:")
                print(text)
                pyperclip.copy(text)
                self.close()

    def on_key_press(self, symbol, modifiers):
        if symbol == pyglet.window.key.ESCAPE:
            self.close()

    def get_selection(self):
        if not self.start_pos or not self.current_pos:
            return None

        # self.start_pos, self.current_pos は
        # 左下を原点とする座標系での位置が入っている

        # x, y それぞれの最小と最大を得てboxにする
        x1, y1 = self.start_pos
        x2, y2 = self.current_pos
        box = (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))

        if box[2] - box[0] < 2 or box[3] - box[1] < 2:
            print("Selection too small.")
            return None

        # 左上を原点とする座標系に直す
        corrected_y2 = self.original_image.height - box[1] - 1
        corrected_y1 = self.original_image.height - box[3] - 1
        box = (box[0], corrected_y1, box[2], corrected_y2)

        # crop()で切り抜く画像に右下の点が含まれるように調整
        box = (box[0], box[1], box[2] + 1, box[3] + 1)

        return self.original_image.crop(box)


def perform_ocr(image):
    # Geminiへの指示文
    instruction = (
        "このスクリーンショットに写っている文字列を全て読み取って返してください。"
        "写っている文字列のみを返してください。それ以外の説明などの文言は不要です。"
    )

    # Geminiに画像と指示を渡す
    response = model.generate_content([instruction, image])

    return response.text


if __name__ == "__main__":
    GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")

    if GOOGLE_API_KEY is None:
        print("The environment variable GOOGLE_API_KEY is not set.")
        exit()

    # model = genai.GenerativeModel("gemini-2.5-flash")
    model = genai.GenerativeModel("gemini-2.5-pro")

    try:
        img = pyautogui.screenshot()
        window = SimpleScreenshotSelector(img)
        pyglet.app.run()
    except Exception as e:
        print(f"Error: {e}")
APIキーの取得

以下のサイトの「APIキーを取得する」ボタンをクリックしてAPIの使用に必要なAPIキーを取得します。

ai.google.dev

取得したら以下のようにして「GOOGLE_API_KEY」という環境変数に取得したAPIキーをセットします。 ("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"の部分は取得したAPIキー) Pythonプログラムからこの環境変数を参照します。

$ export GOOGLE_API_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
プログラムの実行
$ rye run python main.py
.env ファイルの作成

上記のようにAPIキーを環境変数にセットしているとターミナルを立ち上げるたびに環境変数にセットしなければなりません。 それは面倒なのでプログラム実行時にファイルからAPIキーを読み込むようにしてみましょう。 screen-ocr/.env を以下の内容で作成します。("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"の部分は取得したAPIキー)

GOOGLE_API_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
pyproject.toml ファイルの編集

.envファイルから環境変数に値をセットしてプログラムの実行をするような スクリプトを screen-ocr/pyproject.toml に定義します。 pyproject.tomlに以下を追記します。

[tool.rye.scripts]
run-main = { cmd = "python main.py", env-file = ".env" }
スクリプトを使ってのプログラムの実行

そしてプログラムの実行を以下のコマンドで行うようにします。

$ rye run run-main

実は自分は以前にも同じようなツールを作っています。 そのときは以下のAPIを使っていました。

ocr.space

現在は変わっているかもしれませんが当時は使っていてけっこう誤認識がある印象でした。

一方で今回のものは認識精度が高すぎて画像から読んでいないのではないかと疑いたくなってしまうほどです。 えらいこってす。