2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

手書き帳票OCRをGeminiとChatGPTで比較、Geminiが圧勝 ― Flash-Lite vs GPT-5.4 nano、ほぼ同コストで精度53pt差【2026年7月】

2
Posted at

生成AIがマルチモーダルを扱えるようになり、紙の帳票のデジタル化が一気に進みそうな気配があります。現場に紙の帳票は、まだまだたくさん残っています。作業日報、点検表など。これらをGASとGeminiを使ってOCRする方法については、下記の記事で紹介しました。

今回は 「結局どのモデルが、安くて、正確なのか?」 を調査してみます

日本語の手書き作業日報を1枚用意し、最新の"最安帯"モデル3つに同じ条件で読ませて、精度・速度・コストを横並びにしてみました。使ったのは次の3モデルです(料金は100万トークンあたり・2026年7月時点)。

モデル 入力 / 出力 位置づけ
Gemini 3.1 Flash-Lite $0.25 / $1.50 Googleの高効率・低遅延モデル
GPT-5.4 nano $0.20 / $1.25 OpenAIが「コスト重視ならこれ」と推奨する最安
GPT-5.4 mini $0.75 / $4.50 nanoの一段上(約3倍)

Gemini に関しては、Flash-Liteで十分なOCR精度を発揮することを確認済ですので、Flashモデルは評価していません。
GPT−5.4 nano と Gemini Flash-Lite はほぼ同じ価格帯、GPT-5.4 mini はその一段上です。「ほぼ同じ値段ならどちらが賢いか」「一段課金したらどれだけ変わるか」を評価します。

テストに使った帳票

読ませたのは、建設現場の作業日報を模した手書きの1枚です(記載内容はすべて架空)。OCRの実力差が出るように、"難所"を仕込みました。

  • 日本式天気記号:天候欄は「晴」と書かず、○(快晴)/○に縦棒(晴れ)/◎(くもり)/●(雨)/結晶マーク(雪)で手描きしました。記号を天気の意味に変換できるかを見ます
  • 天気の移り変わり(気象庁の区切り表記) :1日ぶんは「晴れ/くもり」のように書いています。気象庁の表記では 「/」=「のち」「|」=「時々・一時」 です(気象庁FAQ)。つまり「晴れ/くもり」は 晴れのちくもり。区切り記号まで解釈できるかを見ます
  • 手書きの訂正跡:1マスはわざと二重線で訂正してあります(実際の帳票では日常茶飯事)。訂正跡をどう扱うかでモデル差が出ます
  • 氷点下+小数-1.2(マイナス符号が脱落しないか)、6.0(末尾ゼロを正しく理解できるか)
  • 英数字混じりの専門用語D13(鉄筋径、手書きではDBに酷似)、スランプ18生コン 24-18-20
  • 単位のゆらぎ:本/枚/m³/袋
  • ○囲みマーク:進捗欄の「順調・要注意・遅延」のどれを丸で囲んだか

テストに使った手書きの作業日報(内容はすべて架空)。天候欄は日本式天気記号で手描きした

3モデルを同じ条件で叩くコード

公平に比べるため、同じ画像・同じプロンプト・同じJSONスキーマ(構造化出力) で3モデルを呼びます。APIは OpenRouter に一本化して、1つのコードで3モデルを切り替えられるようにしました。

天候欄は「日本式天気記号を天気の名称に変換して」と指示し、responseSchema で表構造をそのまま受け取ります。

import base64, json, os, time
from pathlib import Path
from openai import OpenAI  # OpenRouter は OpenAI 互換

MODELS = [
    {"id": "google/gemini-3.1-flash-lite", "label": "Gemini 3.1 Flash-Lite", "in": 0.25, "out": 1.50},
    {"id": "openai/gpt-5.4-nano",          "label": "GPT-5.4 nano",          "in": 0.20, "out": 1.25},
    {"id": "openai/gpt-5.4-mini",          "label": "GPT-5.4 mini",          "in": 0.75, "out": 4.50},
]

# 各行のスキーマ。天候は「記号→名称」に変換させる
ROW_PROPS = {
    "日付": {"type": "string"},
    "天候": {"type": "string", "description": "日本式天気記号を晴れ/快晴/くもり/雨/雪等の名称に変換"},
    "気温": {"type": "number"},
    "作業内容": {"type": "string"},
    "作業人数": {"type": "integer"},
    "使用機材資材": {"type": "string"},
    "数量": {"type": "string"},
    "進捗": {"type": "string", "enum": ["順調", "要注意", "遅延"]},
}
# OpenAI の strict モードは全 object に additionalProperties:false と
# 全プロパティの required を要求する(Gemini は寛容)。両対応で false を明記する
SCHEMA = {
    "type": "object", "additionalProperties": False,
    "properties": {
        "header": {
            "type": "object", "additionalProperties": False,
            "properties": {k: {"type": "string"} for k in
                           ["記入日", "記入者", "現場名", "工種", "元請担当"]},
            "required": ["記入日", "記入者", "現場名", "工種", "元請担当"],
        },
        "rows": {"type": "array", "items": {
            "type": "object", "additionalProperties": False,
            "properties": ROW_PROPS, "required": list(ROW_PROPS)}},
        "備考": {"type": "string"},
    },
    "required": ["header", "rows", "備考"],
}

PROMPT = ("これは建設現場の手書き作業日報です。表の各行と基本情報・備考欄を読み取り、"
          "指定のJSONスキーマで返してください。\n"
          "天候欄は天気記号を天気の名称に変換してください:"
          "○=快晴/○に縦棒=晴れ/◎=くもり/●=雨/結晶マーク=雪。\n"
          "記号と記号の間の区切りは天気の移り変わりを表します。"
          "『/』は『のち』、『|』は『時々(一時)』を意味します。"
          "例えば「晴れの記号/くもりの記号」は『晴れのちくもり』です。\n"
          "手書きに二重線などの訂正がある場合は、訂正後の記入を採用してください。")

def image_data_url(path):
    b64 = base64.b64encode(Path(path).read_bytes()).decode()
    return f"data:image/jpeg;base64,{b64}"

def run_model(client, model, data_url):
    t0 = time.time()
    resp = client.chat.completions.create(
        model=model["id"],
        messages=[{"role": "user", "content": [
            {"type": "text", "text": PROMPT},
            {"type": "image_url", "image_url": {"url": data_url}},
        ]}],
        response_format={"type": "json_schema",
                         "json_schema": {"name": "work_report", "schema": SCHEMA, "strict": True}},
        temperature=0,
    )
    u = resp.usage
    cost = (u.prompt_tokens * model["in"] + u.completion_tokens * model["out"]) / 1_000_000
    return {"label": model["label"], "parsed": json.loads(resp.choices[0].message.content),
            "sec": round(time.time() - t0, 1), "in_tok": u.prompt_tokens,
            "out_tok": u.completion_tokens, "usd": cost}

# 実行:3モデルを順に叩いて速度・コストを表示し、結果をJSONに保存
if __name__ == "__main__":
    client = OpenAI(base_url="https://openrouter.ai/api/v1",
                    api_key=os.environ["OPENROUTER_API_KEY"])
    data_url = image_data_url("work-report-filled.jpg")
    results = []
    for m in MODELS:
        r = run_model(client, m, data_url)
        results.append(r)
        print(f'{r["label"]:<22} {r["sec"]}s  ${r["usd"]:.4f}')
    # 採点スクリプトへ受け渡し
    json.dump(results, open("results.json", "w", encoding="utf-8"),
              ensure_ascii=False, indent=2)

ポイント: OpenAIの構造化出力(strictモード)は、スキーマ内のすべてのオブジェクトに additionalProperties: false と、全プロパティの required 記載を求めます(記載がない場合 400 Invalid schema になります)。

採点:正解データと突き合わせる

正解をJSONで用意し、フィールド単位で突き合わせます。全半角・空白・日付書式のゆらぎは受け入れます。

import json, unicodedata

def norm(v):
    s = unicodedata.normalize("NFKC", str(v))
    s = s.replace("", "/").replace("", "/").replace("", "")  # 日付書式を吸収
    return s.replace(" ", "").replace("", "").replace(" ", "").replace("-", "").lower()

def eq(a, b):
    try:
        return abs(float(a) - float(b)) < 1e-6   # 8.2 と "8.2"、6.0 と 6 を一致扱い
    except (ValueError, TypeError):
        return norm(a) == norm(b)

# 正解データ(帳票の中身を手で用意したJSON)。スキーマのキー名との対応表
GT = json.load(open("ground-truth.json", encoding="utf-8"))
HEADER = {"記入日": "記入日", "記入者": "記入者", "現場名": "現場名",
          "工種": "工種", "元請・担当": "元請担当"}
ROW = {"日付": "日付", "天候": "天候", "気温": "気温", "作業内容": "作業内容",
       "作業人数": "作業人数", "使用機材・資材": "使用機材資材",
       "数量": "数量", "進捗": "進捗"}

def score(parsed):
    ok = sum(eq(GT["header"][g], parsed["header"].get(m, "")) for g, m in HEADER.items())
    total = len(HEADER)
    for gt_row, p_row in zip(GT["rows"], parsed["rows"]):
        ok += sum(eq(gt_row[g], p_row.get(m, "")) for g, m in ROW.items())
        total += len(ROW)
    return ok, total

# 実行:比較スクリプトが保存したJSONを読み、採点して総合精度を表示
if __name__ == "__main__":
    results = json.load(open("results.json", encoding="utf-8"))
    for r in results:
        ok, total = score(r["parsed"])
        print(f'{r["label"]:<22} {ok}/{total} 正解  {round(100 * ok / total)}%')

結果

モデル ヘッダ 天候 全項目 総合 実コスト
Gemini 3.1 Flash-Lite 5/5 6/6 48/48 100% 5.0 $0.0015
GPT-5.4 nano 1/5 3/6 24/48 47% 6.3 $0.0014
GPT-5.4 mini 5/5 5/6 45/48 94% 5.1 $0.0051

タスクの内容によるかもしれませんが、かなり圧倒的な結果になりました。

コスト×精度の散布図。Gemini 3.1 Flash-Lite が最安帯かつ最高精度で左上に位置する

1. 最安帯の勝者は Gemini 3.1 Flash-Lite(満点)

nano とほぼ同じコスト($0.0015)で、精度は 100%。ヘッダも本文も、後述の「移り変わり記号」も、二重線で訂正されたセルまで、すべて正しく読みました。既にGeminiモデルのOCR精度の高さは認識していましたが、今回のタスクでも非常に優秀でした。

2. GPT-5.4 nano は"それっぽい嘘"を返した

同じ最安帯ですが 47%。天候欄では、快晴(○)もくもり(◎)も両方「晴れ」と誤読——日本式天気記号を判読できませんでした。さらに注意したいのは、読めない箇所を空欄にせず、もっともらしい別の言葉で埋めてくる傾向です。

  • 現場名「東町マンション新築工事」→ 「東側マンション新築工事」
  • 記入者「山田太郎」→ 「よだまち部」

出力のJSONは整っていて一見それらしいので、目視チェックなしに信頼すると危険です。モデルを選ぶときはタスクに合わせて精度も確認するべき、という良い実例になりました。

3. GPT-5.4 mini は正確(94%)だが割高

94%と高精度ですが、コストは Flash-Lite の 約3.4倍。この用途(日本語手書き帳票)に限れば、mini を選ぶ理由が見当たらないという結果になりました。

外したのは48項目中3つでした。ひとつは二重線で訂正された天候欄のセルで、最終的な記入は「晴れ」なのに mini は 「晴れ時々くもり」 と読みました(Gemini Flash-Lite は同じセルを正しく「晴れ」と読解)。残る2つは手書き特有の一字違いで、 「異形鉄筋」→「黒板鉄筋」 、そして 「現場閉所」→「現場開所」 。とくに後者は"閉"と"開"が入れ替わって意味が逆転してしまいます。

4. プロンプトで「区切り記号」の説明が必要

実は最初、区切り記号の意味を説明せずに投げたところ、3モデルとも「晴れ/くもり」を「晴れ」止まりで読み落としました。プロンプトに「『/』=のち、『|』=時々」と一文足したところ、Gemini は「晴れのちくもり」まで正しく取れるようになりました。同じモデル・同じ画像でも、ドメイン知識が重要——プロンプト設計の重要性の例になりました。

結論

  • 日本語の手書き帳票を安く読むなら、まず Gemini 3.1 Flash-Lite がおすすめ。最安帯で高い精度を発揮します。
  • 「コスト」だけで モデルを選択しない。ほぼ同じ価格でもタスクによって得意・不得意があります。
  • GPT-5.4 mini でも、この用途では Flash-Lite を上回れなかった。モデルは"高い=正確"とは限りません。ベンダーにこだわらず、タスクごとにテストすることがおすすめです。

モデルは数か月で入れ替わります。この記事の数字も"2026年7月時点のスナップショット"として、現場のデータで測り直すことが必要だと思います。


なお、読み取った結果を実際にGAS(Google Apps Script)だけでスプレッドシートへ自動入力する方法は、冒頭でも紹介した記事で解説しています。あわせてご覧ください。

私たちPROMPT-X(プロンプトX)は、こうした「現場の困りごとをAIとデータで解く」開発を、地方拠点から全国のプロジェクトで手がけています。未経験からの参加も歓迎しています。

  • 地方から、全国レベルの開発に挑戦しませんか? → 採用情報
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?