0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GPT-5.6の見積書OCRをPythonで検算する

0
Posted at

GPT-5.6の見積書OCRをPythonで検算する

見積書を画像から読ませる仕事では、文字の読み取り精度より「合計が合っているか」のほうを先に機械で止めた方がいいです。

今朝、GPT-5.6 Sol の画像認識を扱った記事を見て、あらためてそう感じました。画像を読むモデルはかなり使いやすくなっています。けれど、見積書をスプレッドシートへ移す場面で困るのは、品名の一文字よりも 23,100円23,000円 のまま次へ流れることです。人の目は合計欄だけ見て、明細の掛け算まで戻らないことがある。ここ地味に効きます。

モデルには画像から決まったJSONを返してもらい、金額の判定だけはPythonに任せます。OCRの結果をそのまま確定データにしない、という分け方です。

参照: GPT-5.6 Sol のvision評価

返却JSONに入れる項目を絞る

まず、画像を渡す側では次の形で返すよう指定します。金額は 12000 のような文字列に寄せます。12,000円約1.2万円 を混ぜると、後段のチェックが余計に複雑になります。

{
  "document_type": "見積書",
  "issuer": "サンプル商事株式会社",
  "issue_date": "2026-08-18",
  "items": [
    {"name": "初期設定", "quantity": "1", "unit_price": "12000", "amount": "12000"}
  ],
  "subtotal": "12000",
  "tax": "1200",
  "total": "13200"
}

明細が複数行あるときは、items の要素を増やします。税込10%を決め打ちで計算しないのがポイントです。軽減税率や端数処理があるので、ここでは書類に書かれた税額を取り込み、明細合計・小計・税額・合計のつながりだけを見ます。

この流れでは、画像を見たモデルが金額を決めるわけではありません。

Pythonで明細から合計まで照合する

次のスクリプトを verify_estimate.py として保存します。標準ライブラリだけで動きます。Decimal を使っているのは、金額にfloatの丸め誤差を持ち込みたくないからです。

import json
import sys
from datetime import date
from decimal import Decimal, InvalidOperation

def yen(value, path):
    if not isinstance(value, str):
        raise ValueError(f"{path}: 文字列で返っていません")
    try:
        amount = Decimal(value)
    except InvalidOperation:
        raise ValueError(f"{path}: 金額として読めません ({value!r})")
    if amount < 0 or amount.as_tuple().exponent < 0:
        raise ValueError(f"{path}: 0以上の整数で返してください ({value!r})")
    return amount

def require(record, key, path):
    value = record.get(key)
    if value in (None, ""):
        raise ValueError(f"{path}.{key}: 値がありません")
    return value

def verify(data):
    if data.get("document_type") != "見積書":
        raise ValueError("document_type: 見積書ではありません")
    require(data, "issuer", "root")
    try:
        date.fromisoformat(require(data, "issue_date", "root"))
    except ValueError:
        raise ValueError("root.issue_date: YYYY-MM-DD形式ではありません")

    items = data.get("items")
    if not isinstance(items, list) or not items:
        raise ValueError("items: 明細がありません")

    item_total = Decimal("0")
    for index, item in enumerate(items, start=1):
        path = f"items[{index}]"
        require(item, "name", path)
        quantity = yen(require(item, "quantity", path), path + ".quantity")
        unit_price = yen(require(item, "unit_price", path), path + ".unit_price")
        amount = yen(require(item, "amount", path), path + ".amount")
        if quantity == 0:
            raise ValueError(path + ".quantity: 0件です")
        if quantity * unit_price != amount:
            raise ValueError(
                f"{path}: 数量×単価={quantity * unit_price}円、金額={amount}円です"
            )
        item_total += amount

    subtotal = yen(require(data, "subtotal", "root"), "root.subtotal")
    tax = yen(require(data, "tax", "root"), "root.tax")
    total = yen(require(data, "total", "root"), "root.total")
    if item_total != subtotal:
        raise ValueError(f"明細合計={item_total}円、税抜小計={subtotal}円です")
    if subtotal + tax != total:
        raise ValueError(f"税抜小計+消費税={subtotal + tax}円、合計={total}円です")
    return item_total, total

if __name__ == "__main__":
    try:
        with open(sys.argv[1], encoding="utf-8") as source:
            item_total, total = verify(json.load(source))
        print(f"OK: 明細 {item_total}円 / 合計 {total}")
    except (IndexError, OSError, json.JSONDecodeError, ValueError) as error:
        print(f"NG: {error}")
        sys.exit(1)

実行結果と、止まるケース

先ほどのJSONを estimate.json として保存して実行します。

python3 verify_estimate.py estimate.json

手元では、明細が 12,000円9,000円、税抜小計が 21,000円、消費税が 2,100円 のデータで次の出力になりました。

OK: 明細 21000円 / 合計 23100円

同じ明細で total だけを 23000 に変えると、終了コード1で止まります。

NG: 税抜小計+消費税=23100円、合計=23000円です

検算で拾えるのは算数と形式のズレです。画像の「8」を「3」と読んだ結果でも、明細から合計まで一貫していれば通ります。金額が大きい案件は、発行元・宛名・銀行口座の突合も別に残した方が安心です。全部を一つの判定に詰め込むより、止めたい理由ごとにチェックを分ける方が、修正の依頼も早くなります。

このまま業務へ入れるなら

取り込みの順番は、画像を読む、JSONを保存する、このスクリプトを通す、OKだけをスプレッドシートへ書く、で十分です。NGのJSONは捨てずに、元画像と同じフォルダへ残します。後から「どこが違ったか」を確認できるからです。

画像認識モデルの更新は歓迎です。ただ、書類の転記で信頼を作るのはモデル名ではなく、間違った数字を次の工程へ渡さない仕組みです。まずは見積書1枚で動かしてみると、どの項目に揺れが出るかも見えてきます。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?