YomiTokuの解析結果をOpenAI APIで検証する

[YomiToku](https://github.com/kotaro-kinoshita/yomitoku)は日本語に特化して学習したAI OCRです。Pythonパッケージとして公開されており、インストールすればCLIで簡単にOCR解析が可能です。APIもありますので、Pythonからの操作も可能です。

YomiTokuのOCR精度は十分に高いですが、自動化する際には異なるOCR処理と組み合わせることで、検証が行えます。今回はOpenAIのAPIを使って、YomiTokuのOCR結果を検証する流れを解説します。

## 今回検証する帳票

今回は、以下のような帳票を対象としています。

![template_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4009607/dbc81270-3923-482f-a89c-0ca8180d36c7.png)

## YomiTokuのインストール

YomiTokuのインストールは、 `pip` コマンドで行います。

```bash
pip install yomitoku
```

### APIを利用する

今回は自動処理なので、YomiTokuのAPIを利用します。CLIも提供しているので、解析結果だけが欲しい場合には、そちらを利用してください。

```bash
yomitoku -o /path/to/results -f md /path/to/dir
```

#### インポートする

まずは、YomiTokuをインポートします。

```python
from yomitoku import DocumentAnalyzer
from yomitoku.data.functions import load_pdf
```

#### DocumentAnalyzerを初期化する

DocumentAnalyzerを初期化します。このとき、GPUを使う場合は `device="cuda"` を指定します。

```python
analyzer = DocumentAnalyzer(visualize=True, device="cuda")
```

#### ドキュメントを読み込む

ドキュメントを読み込みます。PDFファイルを読み込む場合は、 `load_pdf` を使います。

```python
imgs = load_pdf("/path/to/invoice.pdf")
```

今回利用したドキュメントは1ページしかないので、 `imgs` には1つの要素が入ります。複数ある場合にはループ処理を行ってください。今回は1ページ目を取り出しています。

```python
img = imgs[0]
```

#### OCRを実行する

OCR処理を行う `analyzer` は非同期処理なので、 `await` を使います。

```python
results, ocr_vis, layout_vis = await analyzer.run(img=img)
```

#### 結果を出力する

解析結果を取得します。

```python
yomitoku_results = results.to_json("", img=img)
```

この時、 `json` にOCR処理の結果が入ります。以下はその一部です。たとえば検証として、請求金額の合計を使います。明細やフッターなども取得できていますが、今回は省略しています。

```json
{
    :,
    "paragraphs": [
        :
        {
            "box": [
                449,
                500,
                707,
                542
            ],
            "contents": "¥2,855,600-",
            "direction": "horizontal",
            "order": 5,
            "role": null
        },
        :
    ],
    :
}
```

## OpenAIのAPIを利用する

次に、OpenAIのAPIを利用して処理をします。OpenAIのAPIは、APIキーを取得して利用します。APIキーは、OpenAIのサイトから取得できます。なお、OpenAIでは画像ファイルのみ対象なので、PDFは画像（PNGなど）にしてから処理します。

```python
import argparse
import os
import sys
import json
import openai
import base64
from typing import Optional, Dict, Any

def main():
    """メイン関数"""
    
    # OpenAI APIクライアントの初期化
    client = initialize_openai_client(os.environ.get("OPENAI_API_KEY"))
    
    # ファイルの内容に対して、解析を行う
    openai_results = analyse_file(client, "/path/to/invoice.png")
    
    if openai_results:
        print("\n=== ファイル解析結果 ===")
        print(json.dumps(openai_results, indent=2, ensure_ascii=False))
        print("=== 解析完了 ===")
    else:
        print("ファイルの解析に失敗しました。")

if __name__ == "__main__":
    main()
```

OpenAIのクライアントを初期化する関数です。キーは環境変数で設定しておくと便利です。

```python
def initialize_openai_client(api_key: str) -> openai.OpenAI:
    """OpenAI APIクライアントを初期化する関数"""
    return openai.OpenAI(api_key=api_key)
```

`analyse_file` 関数は、ファイルを読み込んでOpenAIに送信し、結果を取得する関数です。

```python
def analyse_file(client: openai.OpenAI, file_path: str) -> Dict[str, Any]:
    """OpenAIを使って、ファイルを解析する
    
    Args:
        client: OpenAI APIクライアント
        file_path: 解析対象のファイルパス
        
    Returns:
        Dict[str, Any]: ヘッダー、明細、フッターを含むJSON形式のデータ
    """
    try:
        # ファイルが存在するか確認
        if not os.path.exists(file_path):
            print(f"エラー: ファイル '{file_path}' が見つかりません。", file=sys.stderr)
            return None
        
        # ファイル名から拡張子を取得
        _, file_extension = os.path.splitext(file_path)
        file_extension = file_extension.lower()
        
        # ファイル解析のためのプロンプト
        prompt = """
        このファイルを解析して、以下の3つの部分に分けてください：
        1. ヘッダー部分
        2. 明細部分
        3. フッター部分
        
        それぞれの部分について、構造と内容を詳細に説明してください。
        結果はJSON形式で返してください。
        """
        
        # ファイルの種類に応じた処理
        image_content = None
        
        if file_extension in ['.jpg', '.jpeg', '.png']:
            # 画像ファイルを直接読み込む
            with open(file_path, 'rb') as file:
                image_content = file.read()
                mime_type = f"image/{file_extension[1:]}"
        else:
            print(f"サポートされていないファイル形式です: {file_extension}", file=sys.stderr)
            return None
        
        if not image_content:
            print("画像データの取得に失敗しました。", file=sys.stderr)
            return None
        
        # OpenAI APIを使用してファイルを解析
        print("OpenAI APIを使用してファイルを解析しています...")
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {
                    "role": "system",
                    "content": "あなたはファイル解析の専門家です。アップロードされたファイルを解析し、ヘッダー、明細、フッターに分けて情報を抽出してください。"
                },
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": f"data:{mime_type};base64,{base64.b64encode(image_content).decode('utf-8')}"
                            }
                        }
                    ]
                }
            ],
            response_format={"type": "json_object"}
        )
        
        # 解析結果を取得
        raw_content = response.choices[0].message.content
        
        try:
            analysis_result = json.loads(raw_content)
            
            # 結果を整形
            result = {
                "header": analysis_result.get("ヘッダー部分", analysis_result.get("header", {})),
                "details": analysis_result.get("明細部分", analysis_result.get("details", {})),
                "footer": analysis_result.get("フッター部分", analysis_result.get("footer", {}))
            }
            
            # 結果が空の場合は、元のレスポンスをそのまま返す
            if all(not v for v in result.values()):
                print("解析結果が空のため、元のレスポンスを使用します。")
                openai_results = analysis_result
        except json.JSONDecodeError as e:
            print(f"JSONの解析に失敗しました: {e}")
            return None
        
        return openai_results
        
    except Exception as e:
        print(f"エラー: ファイルの解析中に問題が発生しました: {e}", file=sys.stderr)
        return None
```

結果として、以下のようなJSON形式のデータが得られます。なお、この結果を確実に受け取るには、プロンプトにて明示的に指定する必要があります。

```json
{
  "header": {
    "title": "請求書",
    "recipient_info": {
      "name": "合名会社小田部石材商会 東京支社 御中",
      "address": "〒919-5359 福井県南条群日野2-4-4",
      "department": "営業部",
      "contact_person": "担当者：北岡 崇典 様"
    },
    "issuer_info": {
      "name": "株式会社ナカヤマ",
      "address": {
        "postal_code": "〒456-0029",
        "prefecture_area": "愛知県日井市胡瓜町",
        "office": "4-1-10 東城ビレ1F"
      },
      "phone": [
        "0531-50-0067",
        "0531-50-0058"
      ],
      "email": "sato_415@example.org"
    },
    "invoice_info": {
      "number": "123456-123",
      "date": "令和13年12月21日"
    },
    "seal": "株式会社ナカヤマ"
  },
  "details": {
    "request_statement": "下記の通りご請求申し上げます。",
    "total_amount_due": "¥2,855,600",
    "items": [
      {
        "no": 1,
        "name": "ノートパソコン",
        "quantity": "5台",
        "unit_price": "120,000",
        "amount": "600,000"
      },
      :
      {
        "no": 9,
        "name": "モニター",
        "quantity": "4台",
        "unit_price": "30,000",
        "amount": "120,000"
      }
    ],
    "sub_total": "¥2,596,000",
    "tax": {
      "rate": "10%",
      "amount": "¥259,600"
    },
    "grand_total": "¥2,855,600"
  },
  "footer": {
    "notes": "備考欄：",
    "bank_info": {
      "bank_name": "あおぞら銀行",
      "branch": "夕立支店",
      "account_type": "普通口座",
      "account_number": "No 0123456",
      "account_holder": "カ）ﾅｶﾔﾏ"
    },
    "payment_due_date": "お支払い期限：令和13年12月31日",
    "payment_note": "（※お振込み手数料は御社ご負担にてお願い致します）"
  }
}
```

この結果から、 `results["details"]["grand_total"]` に請求金額が入っています。これを使って、YomiTokuの結果と比較します。

```python
# 正規表現で、数値でないものは除去
yomitoku_total = re.sub(r"[^0-9]", "", yomitoku_results["paragraphs"][5]["contents"]) # 2855600
openai_total = re.sub(r"[^0-9]", "", openai_results["details"]["grand_total"]) # 2855600
assert int(yomitoku_total) == int(openai_total)
```

このようにして、YomiTokuの結果とOpenAI APIによる結果を検証できます。

## 検証のメリット・デメリット

OpenAIのAPIは、有料です。また、ネットワークを使うので、結果が返ってくるのに時間を要する場合があります。そのため、常時検証するのではなく、定期的にテストするなどでも十分でしょう。

YomiTokuは商用では有償ですが、実行回数などによる課金は発生しません。また、データを自社内に留めて利用できますので、セキュリティ的にも安心して利用できます。さらに、同様の帳票であれば同じように結果が得られるので、システム自動化しやすいのがメリットです。

こうした複数のAIによる検証により、信頼性や精度の向上を図れるでしょう。AIによる業務フロー自動化を行う際には、こうした検証・テスト工程をフローに含めることをお勧めします。

## ライセンス

YomiTokuのライセンスは[コモンズ証 \- 表示 \- 非営利 \- 継承 4\.0 国際 \- Creative Commons](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.ja)です。非商用での個人利用、研究目的においては、ご自由に利用できます。商用目的での利用に関しては、商用ライセンスが必要です。

## まとめ

YomiTokuの精度を検証する際には、他のOCR処理が可能なAIと比較してみると良いでしょう。OpenAI APIは有名なLLMであり、検証に用いるのにちょうど良いかと思います。

YomiTokuは日本語に特化しており、日本語のOCR処理には最適です。また、Pythonパッケージとして提供されているため、簡単に利用できます。ぜひ、AI OCRを使って業務効率化を実現してください。

[kotaro\-kinoshita/yomitoku: Yomitoku is an AI\-powered document image analysis package designed specifically for the Japanese language\.](https://github.com/kotaro-kinoshita/yomitoku?tab=readme-ov-file)