虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

GPT4o の画像解析でレシートを読み取る bot を作ってみた

こんにちは。虎の穴ラボ エンジニアのS.Aです。

本記事は虎の穴ラボ2024年夏の連載ブログ 9日目の記事です。
前回はT.Hさんによる「【Phaser3 + Whisper + OpenAI API】ブラウザゲームを音声認識で動かしてみた」でした。
次回はA.Mさんによる「Gemini APIを触ってみる」が投稿予定です。

今回は ChatGPT の最新モデル GPT4o の画像解析を使って、レシートの内容をスプレッドシートに書き出してくれる bot を作ってみました。 その過程で GPT4o の API の使い方などの知見を得たので備忘も兼ねてご紹介しようと思います!

bot を作ろうとしたきっかけ

最初に今回の bot を作ろうと思い立ったのは、Web 版のChatGPTで GPT4o の画像解析を試してみた時でした。
正確に内容読み取ってくれないだろう...と思いながらスマホで撮影したレシートの写真を渡してみたところ、 正確にレシートの内容を読み取ってくれました。スゴイ!

いままではこの手の処理を実現するには OCR を使う必要がありましたが、 直接GPT4o に画像を投げるだけでデータを取れるのは凄くありがたみを感じます。

構成

ということで、さっそく構成を考えてみました。

  • LINE bot を作って写真を受け取る
  • LINE bot の処理は AWS Lambda 上で動かす
  • ChatGPT API に画像を渡して内容を json データに変換してもらう
  • json データを Google スプレッドシートに Stein を使って保存する

LINE bot を選んだ理由は、スマホで写真撮影して送信〜という流れがスムーズにできるからです。
LINE bot の処理は、慣れ親しんだ(かは微妙ですが) AWS Lambda で JavaScript を使って書くことにしました。
最終的に読み取ったデータは、後々データ整形しやすいように Google スプレッドシートに格納することにしました。
Stein は、Google スプレッドシートに対して読み込み/書き込みができる Web API を立ててくれる便利なサービスです。

レシートの写真をスマホでパシャっととって bot に送れば 家計簿シートに自動で商品名や価格などの情報が格納されるイメージです。

GPT4o を API で使うにはどうする?

料金

さきほど使った Web 版は GPT PLUS という月額20ドルのプランでブラウザ上から利用できますが、
Web API で ChatGPT を利用する場合は、PLUS とは別体系で、従量課金になっています。

最初に Oepn AI のページでトークンを購入しておけば、
リクエストが呼ばれるごとにトークンが消費される形で利用できます。
1リクエストあたりの価格は公式ページから確認できます。

LINE から画像を送信してみたところ、960 x 1706px となったので、 LINE 経由で画像を処理する場合は 1105 token = 約0.88円(記事執筆時点のレート)のようでした。

ちなみに、LINE のアプリの設定で送信する画像をより高画質にもでき、その場合画像サイズは 1536 x 2730 px になりましたが、 GPT4o は画像の高さ/幅がどちらも 768px を超えていると短辺が 768px になるようリサイズされる仕様だったため、 文字の検出精度や必要トークン数は変わらなそうでした。

リクエスト方法

まず、Open API のアカウントを作ってダッシュボードから API キーを生成します。

あとは、Authorization ヘッダに発行したキーを含めてリクエストを送るだけでOKです。 画像を解析したい場合は、image_url という項目にデータを入れればOKです。 image_url という名前ですが、base64 エンコードした画像も扱えました。

実装したコードはこんな感じです。

const analyzeImageByGPT = async (base64Image) => {
  const messages = [
    {role: "system", content: SYSTEM_PROMPT},
    {role: "user", content: [
        {type: "image_url", image_url: {url: `data:image/jpeg;base64,${base64Image}`}}
      ]
    }
  ];
  try {
    const response = await axios.post("https://api.openai.com/v1/chat/completions", {
        model: "gpt-4o",
        messages: messages
    }, {
        headers: {
            'Authorization': `Bearer ${GPT_API_KEY}`,
            'Content-Type': 'application/json'
        }
    });
    return response.data.choices[0].message.content;
  } catch (error) {
      console.error(`画像解析のエラー: ${JSON.stringify(error)}`);
      return null;
  }
}

レスポンスの内容を json 形式に固定する response_format: {type: "json_object"} というオプションもありましたが、 システムプロンプトで指定するだけでも問題なく json で返してくれました。 ただ、json で返してねと書くだけだと、前後に「画像から読み取ったデータは以下の通りです」といったような枕詞がついてしまうことがあるので、「結果は json のみで」とお願いするようにしています。

今回使用したプロンプトはこんな感じです。

このシステムはユーザからレシート画像を受け取り、必要な情報をjson形式で提供します。
レシート画像が与えられた場合、店名、日付、購入商品、金額の情報を読み取って以下のような形式で返してください。
結果はjsonのみだけを返し、前後にいかなる文言も絶対に含めないでください。
{
    "success": true,
    "store_name": "ファミリーマート XXXX店",
    "store_category": "コンビニ",
    "purchase_date": "2024-05-09",
    "items": [
        {
            "item_name": "コカコーラ 350ml",
            "item_kind": "飲み物",
            "item_category": "食品"
            "item_quantity": 1,
            "item_price": 85,
        }
    ],
    "total_price": 85
}
なお、各項目の説明は以下の通りです。
success: 読み取りが成功した場合は true
store_name: 店舗名
store_type: コンビニ、スーパーなど店の分類
purchase_date: 購入した日付
items: 商品情報の配列
item_name: 商品名
item_kind: タバコ、飲み物など商品の小分類
item_category: 食品、嗜好品など商品の大分類
item_quantity: 購入点数
item_price: 商品の単価
total_price: 合計金額
もし与えられた画像がレシート画像でない場合など、情報が得られない場合は、{ "success": false } というjsonを返してください

スプレッドシートへの書き込み

次に、GPT4o で得たデータをスプレッドシートに格納する処理を作ります。 まず Google ドライブにレシートの情報を保存するスプレッドシートを作って、 Steinで作成したスプレッドシートのリンクを貼ればすぐに API で読み書きできるようになりました。

スプレッドシートの一行目をヘッダーとしてカラム名を書き、 それに合わせたデータを POST すればシートに行が追加されます。 今回はレシートから読み取った商品について、 店名、店カテゴリ、購入日、商品名、商品種別、購入数、価格、商品カテゴリ を格納することにしたので、 スプレッドシートを以下のようにしました。

あとは Stein で生成された URL にデータを POST すれば格納されます。 ソースはこんな感じです。

const saveDataToSheet = async (data) => {
  if(!data.success) return null;

  // GPTで得たデータをスプレッドシートの行データの形に整形
  const records = [];
  for(const item of data.items) {
    const record = {
      store_name: data.store_name,
      store_category: data.store_category,
      purchase_date: data.purchase_date,
      item_name: item.item_name,
      item_kind: item.item_kind,
      item_category: item.item_category,
      item_quantity: item.item_quantity,
      item_price: item.item_price,
    }
    records.push(record);
  }

  // ユーザ認証の設定をしている場合は必要
  const auth = Buffer.from(`${STEIN_USER}:${STEIN_PASS}`).toString('base64');
  try {
    await axios.post(`${STEIN_URL}/${SHEET_NAME}`, records, {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Basic ${auth}`
        }
    });
  } catch (error) {
    console.error(`データ保存でエラーが発生しました。: ${JSON.stringify(error)}`);
  }
}

LINE bot 本体の実装

GPT4o の画像解析と Google スプレッドシートへの書き込みのロジックができたので、
残るは LINE bot として画像が送信されたら画像を読み取って GPT4o / Stein にリクエストする処理を書くだけです。

LINE bot を作るには、まず AWS Lambda で関数を作成し、トリガーとして API Gateway を設定しておきます。
その後、LINE Developersにアクセスしてチャンネルを作成し、
Messaging API設定 > Webhook 設定で Lambda の API エンドポイントを指定すればOKです。

ちなみに AWS Lambda はデフォルトの設定だと3秒でタイムアウトするようになっていて、
今回の処理だとタイムアウトしてしまうので、設定を1分に引き上げました。

LINE bot に送った画像を読み込むには、飛んできたリクエストからさらに画像取得APIを別途呼び出す必要があったので、 そのように書いています。
実装はこんな感じになりました。

exports.handler = async (req) => {
  const body = JSON.parse(req.body);
  for(const event of body.events){
    if(event.message.type==="image") {
      try{
        const image = await getImageContent(event.message.id);
        const imageBase64 = Buffer.from(image, "binary").toString("base64");

        const receiptJson = await analyzeImageByGPT(imageBase64);
        const receipt = JSON.parse(receiptJson);
        await saveDataToSheet(receipt);
        await replyMessage(event.replyToken, "レシートを受け取りました!");
        return {
          statusCode: 200,
          body: receiptJson
        }
      }catch(error){
        await replyMessage(event.replyToken, "エラーが発生しました");
        return {
          statusCode: 500,
          body: JSON.stringify(error)
        }
      }
    }
  }
};

// LINE で送られた画像を取得する処理
const getImageContent = async (messageId) => {
  const url = `https://api-data.line.me/v2/bot/message/${messageId}/content`;
  try {
      const response = await axios.get(url, {
          responseType: 'arraybuffer',
          headers: {
              'Authorization': `Bearer ${LINE_ACCESS_TOKEN}`
          }
      });
      return response.data;
  } catch (error) {
      console.error(`LINE画像取得でエラーが発生しました: ${JSON.stringify(error)}`);
      return null;
  }
};

// LINE でメッセージを返信する処理
const replyMessage = async (replyToken, text) => {
    const url = "https://api.line.me/v2/bot/message/reply";
    const response = {
      replyToken: replyToken,
      messages: [{type: "text", text: text}]
    };
    try {
        await axios.post(url, response, {
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${LINE_ACCESS_TOKEN}`
            }
        });
    } catch (error) {
        console.error(`LINEメッセージでエラーが発生しました: ${JSON.stringify(error)}`);
    }
};

これで完成です!

結果

ということで、実際に LINE bot を追加してレシート画像を送ってみました。
果たしてちゃんと読み取ってくれるのか....

そしてデータ格納先のスプレッドシートを見ると...

ちゃんと格納されていました!
商品名や値段もうまくレシートから読み取れています。
商品分類はGPTにお任せで書いてもらったのですが、このあたりもほぼほぼ正確ですね。
ただレシート中のバタースコッチの分類がキャンディになっていましたが、本当は菓子パンだったので、そこだけ間違ってました。 (このあたりは人間でも分類失敗しそうなので仕方なさそうです)

他にもいくつかレシートを試してみたところ...
一部、半角カタカナで文字が小さいレシートで読み取りに失敗して商品名が間違うこともありましたが、
いい感じで読み取ることができました。例えば「ヤサイセイカツ」が「サイゼリヤ」になってたり...(見えなくもない...)

ともあれ、当初にやりたいことは無事達成できて嬉しい限りです!!

まとめ

というわけで、今回は GPT4o をつかってレシートを読み取る bot についてご紹介しました。
作ってみた感想としては、

  • GPT4o OCRまでできてスゴイ!
  • Stein + Google スプレッドシート扱いやすくて便利
  • LINE bot で写真とって GPT4o に投げる構成、アイデア次第で他にも色々できそう

でした! 最後までお読みいただきありがとうございます!

採用情報

虎の穴ラボでは一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp