スマホやPCの中に、写真やスクショが山のようにたまっていませんか?😅 「これ何の画像だっけ」「レシートの合計だけ知りたい」「種類ごとにフォルダ分けしたい」——そんな作業を、ネット接続なしで動くローカルAIに手伝ってもらうようにしてみました。今回は新しいアプリを作るのではなく、以前作ったローカルAIエージェント画像機能を“実用レベル”に強化します。

🖼 この記事でわかること
・写真をドラッグ&ドロップで複数まとめて渡す仕組み
・各画像を説明 → タグ付け → 文字起こし(OCR的)まで一括処理
提案→確認→コピーの安全なやり方でタグ別フォルダに整理(元ファイルは触らない)
・スクショ→画面内容の説明、そしてすべてローカル完結(写真が外に出ない)
再現できるよう、実装のキモになるコード(D&D/画像をローカルLLMに渡す/安全なコピー整理)も掲載

土台:p935 ローカルAIエージェント。画像を理解する頭脳は Ministral 3(vision対応)。実装はMac、コードはMac/Windows共通(前回p941で同一コードのWindows動作を確認済み)。

作るもの:写真をD&Dで説明・タグ・整理

スポンサーリンク

完成形はこう動きます。複数の画像をまとめてエージェントにドラッグ&ドロップすると、1枚ずつ「これは何か(説明)」「関連するタグ」「写っている文字(OCR的)」を答え、さらに「タグごとにこう仕分けます」という整理案まで出してくれます。下は、レシート・会議メモ・セールのポスター・売上グラフの4枚を渡して一括処理した結果です。

4枚の画像を一括処理し、各画像の説明・タグ・OCR文字・仕分け案が並んだ画面
4枚を一括処理した結果。各画像に「説明・タグ・仕分け先・写っている文字」が付く。レシートは品目や合計まで文字起こしできている。全部ローカル=写真は外に出ない

なぜ“エージェントに足す”のか

p935のエージェントは、実はもともと画像を理解できます(Ministral 3のvision)。さらに「ファイルを読む・書く」「コマンドを実行する」という道具も持っています。つまり「画像を見て、名前を変えて、フォルダに振り分ける」ための部品はほぼ揃っているんですよね。

足りなかったのは“使い勝手”だけ。今回はそこを埋めます。独立アプリは作らず、エージェントに次を追加しました。

  • ドラッグ&ドロップ(複数画像)で手軽に渡せるUI
  • 各画像を説明・タグ・OCRする一括処理ボタン
  • 提案→確認→コピーで安全に整理する仕組み+CSV書き出し
  • スクショ→画面説明(既存のスクショ道具+vision)

ドラッグ&ドロップで複数画像を渡す

これまでは「画像添付」ボタンで1枚ずつでした。今回はエージェントの画面に、写真を何枚でもドラッグ&ドロップできます。ドロップすると「◯枚 受け取りました」と表示され、一括処理の準備完了です。画像を入れると画像を理解できるMinistral 3へ自動で切り替わるようにもしました(コード特化モデルを使っていても安心)。

画像をドラッグ&ドロップして4枚受け取った状態のエージェント画面。下部に画像ツールの行
画像をD&Dすると「4枚 受け取りました」。下の行に「一括処理」「整理を実行(コピー)」「画面を説明」を追加した。1枚だけならチャットに添付して質問もできる

実装はPySide6の標準的なドロップ受け口だけ。3ステップで、ファイル選択(複数可)と同じ入口に流し込みます(Mac/Windows共通)。

# ① ウィンドウでドロップを受け付ける(__init__ の中で一度だけ)
self.setAcceptDrops(True)

# ② ドラッグ中:画像ファイルが含まれていれば受け入れる
def dragEnterEvent(self, e):
    urls = e.mimeData().urls()
    if e.mimeData().hasUrls() and any(is_image(u.toLocalFile()) for u in urls):
        e.acceptProposedAction()

# ③ ドロップされたら画像パスを集めて処理へ渡す
def dropEvent(self, e):
    paths = [u.toLocalFile() for u in e.mimeData().urls()
             if is_image(u.toLocalFile())]
    if paths:
        self._add_images(paths)   # ← 「画像添付」ボタン(複数選択)も同じ入口に通す

is_image() は拡張子で画像かどうかを見るだけの小さな関数です。受け取り後の処理(表示更新・vision対応モデルへの自動切替)は _add_images() に集約しています。

一括処理:説明・タグ・文字起こし(OCR)

「一括処理」を押すと、各画像に対してローカルのvisionモデルが1回ずつ推論し、次の3つを返します。

  • 説明:画像の内容を1文で(例:「カフェのレシートで、4品目の飲食代と合計金額が記載されている」)
  • タグ:短い日本語タグを3〜5個(例:レシート/カフェ/金額)
  • 文字(OCR的):画像に写っている文字をそのまま書き出す

肝は「画像をローカルのvisionモデルに渡す」ところ。画像をbase64にして、テキストと一緒に1つのメッセージにするだけです。あとは1画像=1回の推論で、説明・タグ・文字をまとめて答えてもらいます。

# 「テキスト+画像」を1つのメッセージにする(vision対応モデルへ渡す形)
def image_content(text, image_path):
    b64 = base64.b64encode(open(image_path, "rb").read()).decode()
    return [{"type": "text", "text": text},
            {"type": "image_url",
             "image_url": {"url": f"data:image/png;base64,{b64}"}}]

# 1回の推論で「説明・タグ・文字」をまとめて答えてもらう
prompt = (
    "この画像について、次の形式で日本語で答えてください。\n"
    "説明: 内容を1文で\n"
    "タグ: カンマ区切りで3〜5個\n"
    "文字: 画像に写っている文字をそのまま(無ければ「なし」)"
)
msg = llm.chat_message(
    [{"role": "user", "content": image_content(prompt, path)}],
    max_tokens=500, temperature=0.2)
# msg["content"] を「説明:/タグ:/文字:」の行ごとに分解して使う

特に文字起こしが便利で、レシートの品目や合計、会議メモの箇条書きなどをテキストに落とせます。結果は画面に表示するだけでなく、CSVにも書き出すので、あとで表計算ソフトで見返せます。

⚙ 実装メモ:1画像あたり1回の推論で「説明・タグ・文字」をまとめてもらいます。最初はJSONで受け取ろうとしましたが、画像内の文字に改行が含まれるとJSONが壊れるため、「説明:/タグ:/文字:」のラベル形式で受けるようにして安定しました。

整理:提案→確認→コピー(安全)

タグが付いたら、いよいよ整理です。ここは安全第一にしました。AIが「この画像は『レシート』フォルダに『01_レシート.png』という名前で」という整理案を先に提示し、あなたが確認してから実行します。しかも実行は必ずコピー——元の写真は移動も変更もしません

整理を実行してタグ別フォルダにコピーで仕分けした後の画面
「整理を実行(コピー)」を押すと、確認のうえタグ別フォルダへコピー。元ファイルはそのまま残る安全設計

実行すると、こんな形でコピーされます(元の4枚はそのまま)。

output/organized_20260709_.../
├─ レシート/    01_レシート.png
├─ 会議メモ/    02_会議メモ.png
├─ セール/      03_セール.png
└─ 売上グラフ/  04_売上グラフ.png

実行部分はこれだけ。ポイントは shutil.copy2(コピー)を使い、元ファイルには一切触らないことです。移動(move)や上書きは使いません。

import os, shutil, datetime

def apply_plan(plan, out_dir):
    """整理案どおりに仕分ける。★必ずコピー(元ファイルは触らない)。"""
    stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    root = os.path.join(out_dir, f"organized_{stamp}")
    for r in plan:                 # r = {"path":元, "folder":タグ, "new_name":新名, ...}
        dst_dir = os.path.join(root, r["folder"])
        os.makedirs(dst_dir, exist_ok=True)
        shutil.copy2(r["path"], os.path.join(dst_dir, r["new_name"]))  # ← コピーのみ
    return root

「AIにファイルを勝手に動かされるのは怖い」という不安に応えて、提案を見て納得してから・コピーで、という流れにしています。

画面を説明(スクショ→vision)

おまけで、「今の画面」を撮って説明させることもできます。エージェントは元々スクショを撮る道具を持っているので、それと画像理解をつなぐだけ。エラー画面の意味を聞いたり、UIの内容をメモに落としたり、といった使い方ができます。これもローカルで完結します。

つまずき・気づき

  • 画像機能はvision対応モデルが必須:コード特化モデル(Qwen2.5-Coder)はテキスト専用なので、画像を入れたら自動でMinistral 3へ切替るようにしました。切替は「今のモデルを解放→次を読み込む」方式(前回のモデル切替をそのまま活用)。
  • JSONは画像内テキストで壊れる:前述のとおり、OCRの複数行でJSON解析が失敗。ラベル形式に変えて解決。
  • 日本語OCRの精度:今回のくっきりした画像ではかなり正確でしたが、手書き・低解像度・複雑な背景では崩れることがあります。過信せず「下書き」として使うのが安全です。
  • 枚数と速度:1枚ずつ推論するので、枚数が増えるほど時間がかかります。大量の写真は小分けにするのがおすすめ。
  • 画面撮影のOS差(Windows):後述のWindows動作確認で、「画面を説明」だけWindowsでは追加ライブラリ(Pillow)が必要と判明しました。原因は画面撮影に使っていた pyautogui がWindowsでPillowに依存するため(Macは screencapture を使うので不要)。PySide標準の画面撮影に切り替えて、追加依存なしで両OS共通にしました。

Mac/Windows 両方で動いた

このエージェントは前回と同じくMac/Windows同一コード。今回の画像機能も、Windows(RTX3060)でドラッグ&ドロップ・一括処理・整理コピー・画面説明の4つすべて動作を確認しました。下はWindowsで整理を実行した画面です(パスが C:\…\output\organized_… になっているのがWindowsの証拠)。

Windowsで同じ画像機能が動作し、Windowsのパスにコピー整理された画面
Windows(RTX3060)でも同じコードで動作。一括処理→整理コピーがWindowsのパスに実行されている。反映はファイルを上書きコピーしただけ(コード改変ゼロ)

唯一のOS差が、前章で触れた「画面を説明」の画面撮影。ここだけPySide標準の撮影に統一して、Pillowなしで両OSとも動くようにしました。標準的なドラッグ&ドロップ(PySide)や、パスの扱い(os.path)はそのまま両OSで動きます👍

まとめ

ローカルAIエージェントの画像機能を、実際に使えるレベルまで強化できました。ポイントを整理します。

  • 写真をドラッグ&ドロップで複数まとめて渡せる
  • 説明・タグ・OCRを一括処理し、CSVにも書き出し
  • 整理は提案→確認→コピーの安全設計(元ファイルは触らない)
  • スクショ→説明も可能。すべてローカル完結=写真が外に出ない
  • 画像はvision対応のMinistral 3で。Mac/Windows共通のコード

クラウドの画像AIは強力ですが、家族の写真やレシート・書類を外に送りたくない場面は多いはず。手元で完結する“画像整理の相棒”は、そういうときにこそ頼りになります😊 たまった写真の整理、AIに下ごしらえを任せてみると、思ったより気持ちよく片づきますよ。

それでは、今回はここまで。最後までありがとうございました😊