以前、テスターから「バグってます」とだけ書かれたスクリーンショットが送られてきて、スタックトレースもログも何もなく、画像一枚だけ——そのデバッグに2時間近く費やしたことがある。それがきっかけで、Vision LLMを使ったUIエラー画像の自動解析を調べ始めた。
Vision LLMは、もう猫や犬を認識するだけのものではない。「この画像のUIエラーを列挙してJSONで返して」と自然言語で指示すれば、必要な情報をそのまま返してくれる。たとえばMoondream 2は約2GBと軽量ながら、エラーメッセージ、HTTPステータスコード、さらには各ボタンのdisabled/enabled状態まで読み取れる。
UIエラー画像解析のアプローチ比較
主なアプローチは3つある——それぞれのメリット・デメリットを正直に説明するので、自分に合ったものを選んでほしい。
アプローチ1:従来のOCR(Tesseract、EasyOCR)
OCRで画像からテキストを抽出し、そのテキストをパースする方法。高速で完全オフライン動作、APIコストもゼロ。
- メリット:軽量、無料、強力なGPU不要
- デメリット:テキストを読み取るだけでコンテキストを理解できない。「これは無効化されたボタン」「これはエラーメッセージ」「これは通常のラベル」といった区別ができない。画質が低かったり珍しいフォントだったりすると精度が落ちる。
アプローチ2:クラウドVision API(Google Vision、AWS Rekognition、Azure Computer Vision)
クラウドプロバイダーのAPIを呼び出して画像を解析する方法。純粋なOCRよりも精度が高く、テキスト・オブジェクト・ラベルを検出できる。
- メリット:高精度、モデルのセットアップ不要
- デメリット:呼び出しごとにコストが発生(Google Visionは約$1.5/1000画像)、画像をクラウドに送信するため機密データに問題あり、UIのコンテキスト理解はまだ不十分
アプローチ3:Vision LLM(Moondream、LLaVA、GPT-4 Vision)
画像理解能力を持つ大規模言語モデルを使う方法。英語で要件を説明するだけで、モデルが構造化された分析結果を返してくれる。
- メリット:UIのコンテキストを深く理解できる——エラーメッセージ、ボタンの状態、フォームバリデーション、モーダルダイアログを区別可能。構造化されたJSONを返せる。ローカル実行も可能(Moondream、LLaVA)。
- デメリット:大型モデルはより多くのRAM/VRAMが必要。CPUでローカル実行する場合、純粋なOCRより処理が遅い。
Vision LLMを選ぶべき場面
画質の良い画像から単純にテキストを抽出したいだけならOCRで十分。しかしUIエラーのスクリーンショットには、それ以上のことが求められる:
- エラーの種類の特定:バリデーションエラー、ネットワークエラー、権限エラー、クラッシュ
- エラーが発生しているコンポーネントの特定:どのフォームフィールド、どのボタン、どのAPIエンドポイントか
- エラーコード、HTTPステータス、スタックトレースが画像に含まれていれば抽出
- コンテキストの説明:ユーザーがどの画面にいて、エラー発生前に何をしていたか
これはテキストを読み取るだけでなくコンテキストを理解する問題であり、Vision LLMはその点で圧倒的に優れている。
モデル選択:Moondream vs LLaVA vs GPT-4 Vision
| モデル | ローカル実行 | 必要RAM | 精度 | 速度(CPU) |
|---|---|---|---|---|
| Moondream 2 | ✅ | ~2GB | まずまず良好 | 最速 |
| LLaVA 7B | ✅ | ~8GB | 良好 | 普通 |
| LLaVA 13B | ✅ | ~16GB | 非常に良好 | 低速 |
| GPT-4 Vision | ❌(APIのみ) | N/A | 最高 | 高速(API) |
機密データを扱う本番環境では:Moondream 2が良い出発点——軽量で通常のCPUでも動作し、UIスクリーンショットの解析には十分な精度を持つ。GPUがある場合や高い精度が必要な場合はLLaVA 7Bを選ぼう。
実装ガイド:MoondreamでUIエラー画像を解析する
ステップ1:環境セットアップ
# virtualenvを作成
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# 依存パッケージをインストール
pip install transformers torch pillow einops
GPUがない場合は、軽量なCPU専用のtorchをインストール:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
ステップ2:基本的なエラー画像解析スクリプト
from transformers import AutoModelForCausalLM, AutoTokenizer
from PIL import Image
import json
# モデルを読み込む(初回は約2GBダウンロードされる)
model_id = "vikhyatk/moondream2"
revision = "2025-01-09" # 破壊的変更を避けるため特定のリビジョンを使用
tokenizer = AutoTokenizer.from_pretrained(model_id, revision=revision)
model = AutoModelForCausalLM.from_pretrained(
model_id,
trust_remote_code=True,
revision=revision
)
def analyze_error_screenshot(image_path: str) -> dict:
"""UIエラーのスクリーンショットを解析し、構造化されたdictを返す。"""
image = Image.open(image_path).convert("RGB")
enc_image = model.encode_image(image)
# Vision LLMは英語プロンプトの方が精度が高い
prompt = """Analyze this UI error screenshot. Extract:
1. Error type (validation/network/permission/crash/other)
2. Error message text (exact if visible)
3. HTTP status code or error code if visible
4. Which UI component has the error (button/form field/page/modal)
5. Any stack trace or technical details visible
Respond in JSON format:
{"error_type": "", "error_message": "", "error_code": "", "component": "", "details": ""}"""
result = model.answer_question(enc_image, prompt, tokenizer)
# レスポンスからJSONをパース
try:
# レスポンス内のJSONを探す(モデルが余分なテキストを付加することがある)
start = result.find("{")
end = result.rfind("}") + 1
if start != -1 and end > start:
return json.loads(result[start:end])
except json.JSONDecodeError:
pass
# フォールバック:JSONパース失敗時は生テキストを返す
return {"raw_analysis": result}
# テスト実行
if __name__ == "__main__":
result = analyze_error_screenshot("error_screenshot.png")
print(json.dumps(result, indent=2, ensure_ascii=False))
ステップ3:OllamaでLLaVAを使う(オプション)
Ollamaがすでに動いている環境なら、ローカルAPIからLLaVAを使う方が手動でモデルを読み込むよりはるかに簡単:
# LLaVAモデルをプル
ollama pull llava:7b
import requests
import base64
import json
from pathlib import Path
def analyze_with_ollama(image_path: str, model: str = "llava:7b") -> dict:
"""Ollama APIを使ってエラー画像を解析する。"""
# 画像をbase64エンコード
image_data = base64.b64encode(Path(image_path).read_bytes()).decode("utf-8")
prompt = """Look at this UI error screenshot carefully.
Extract the following information and return ONLY valid JSON:
{
"error_type": "validation|network|permission|crash|unknown",
"error_message": "exact error text visible in the image",
"error_code": "HTTP status or error code if visible, else null",
"affected_component": "which UI element has the error",
"suggested_cause": "brief technical cause based on what you see"
}"""
response = requests.post(
"http://localhost:11434/api/generate",
json={
"model": model,
"prompt": prompt,
"images": [image_data],
"stream": False,
"format": "json" # OllamaにJSON出力を強制する
},
timeout=60
)
response.raise_for_status()
return response.json()["response"]
# 使用例
result = analyze_with_ollama("ui_error.png")
print(result)
ステップ4:エラー画像のバッチ処理パイプラインを構築する
テスターが20〜30枚の画像をまとめて送ってくることは珍しくない。以下のスクリプトは一括処理してJSONレポートを出力する:
import os
import json
from pathlib import Path
from datetime import datetime
def process_error_screenshots_folder(folder_path: str, output_file: str = "bug_report.json"):
"""フォルダ内のすべてのエラー画像を処理し、JSONレポートを出力する。"""
folder = Path(folder_path)
image_extensions = {".png", ".jpg", ".jpeg", ".webp"}
results = []
screenshots = [f for f in folder.iterdir() if f.suffix.lower() in image_extensions]
print(f"{len(screenshots)}枚の画像が見つかりました。解析を開始します...")
for idx, img_path in enumerate(screenshots, 1):
print(f"[{idx}/{len(screenshots)}] 解析中: {img_path.name}")
try:
analysis = analyze_with_ollama(str(img_path))
if isinstance(analysis, str):
analysis = json.loads(analysis)
results.append({
"file": img_path.name,
"analyzed_at": datetime.now().isoformat(),
**analysis
})
except Exception as e:
results.append({
"file": img_path.name,
"error": f"解析に失敗しました: {e}"
})
# レポートを出力
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\n完了!レポートを保存しました: {output_file}")
# 簡易集計
error_types = {}
for r in results:
et = r.get("error_type", "unknown")
error_types[et] = error_types.get(et, 0) + 1
print("\nエラー種別の集計:")
for et, count in sorted(error_types.items(), key=lambda x: -x[1]):
print(f" {et}: {count}件")
# 実行
process_error_screenshots_folder("./screenshots", "./bug_reports/sprint_42.json")
実務から得た実践的なヒント
約2ヶ月間、テスターから送られてきた数百枚のスクリーンショットを実際に処理してわかった——ドキュメントには書いていないことをまとめる:
- プロンプトは英語の方が精度が高い — Vision LLMは主に英語データで訓練されている。UIが日本語の画像でも、プロンプトを英語にするだけで出力精度が約20〜30%向上する。
- 常にJSON出力を要求する — モデルに自由形式のテキストを返させて手動でパースしようとしない。Ollamaなら
"format": "json"を使う。Moondreamならプロンプト内にJSON構造を埋め込む。 - 送信前に画像をリサイズする — 4K Retinaディスプレイの画像はモデルの理解精度を上げないのに処理時間が3〜4倍かかる。幅1280pxにリサイズすれば十分——50枚で試したところ、処理時間が約8分から約2分に短縮された。
- モデルはメモリにキャッシュする — 複数の画像を処理する場合は、モデルを一度だけ読み込んで使い回す。Moondreamを毎回リロードすると起動に4〜5秒かかり、積み重なると無視できないロスになる。
# 解析前に画像をリサイズする
from PIL import Image
def preprocess_screenshot(image_path: str, max_width: int = 1280) -> Image.Image:
img = Image.open(image_path).convert("RGB")
if img.width > max_width:
ratio = max_width / img.width
new_size = (max_width, int(img.height * ratio))
img = img.resize(new_size, Image.LANCZOS)
return img
実際のワークフローへの統合
デモ後にチームメンバーからよく聞かれた質問:「どこに組み込めばいいの?」実際に試して問題なく動いた場所を紹介する:
- Jira/Linear webhook:テスターがチケットに画像をアップロードした際、自動的に解析を実行して「Error Type」「Error Code」フィールドを埋める
- Slack bot:テスターが #bugs チャンネルに画像を送ると、botが解析サマリーを自動返信する
- CI/CDパイプライン:E2Eテスト実行後にスクリーンショットの失敗があれば、自動解析してテストレポートに添付する
- Playwright/Cypress:
onTestFailedフックでVision LLMを呼び出し、レポート内でエラーを自動的に説明させる
このアプローチで一番気に入っているのは、モデルの精度そのものではなく——チーム内のコミュニケーション形式が変わったことだ。「バグってます」の代わりに、「チェックアウト画面でSubmitボタンをクリックした際に /api/orders エンドポイントで HTTP 403 Forbidden が発生」という情報が届くようになった。それだけで、追加情報の確認にかかる時間が1スプリントあたり少なくとも30分は節約できている。
さらに発展させるなら:Vision LLMとRAGを組み合わせて、過去に対処した類似エラーに基づいて修正方法を自動提案する仕組みも作れる——ただし、それはまた別の話。

