見出し画像

ScanSnapでスキャンしたレシートを生成AIで仕分ける

はじめに

「レシートを読み取って仕分けする」というと、
世の中にはそのようなサービスが無数にありますよね。

しかし、スマホで撮影した画像を使う場合、どうしても品質が安定しないことがあります。
何度もやり直しをしなければならなかったり、枚数が多いと疲れてしまいます。

でも、スキャナーがあれば一定の品質が担保され、大量のスキャンも簡単に行えます。

さらに、スキャン後の仕分け作業を生成AIに任せれば、QOLを大幅に向上させることができます。

ということで、我が家で稼働している、
ScanSnapを使った生成AIレシート仕分け
について説明していきます。

必要なもの

  • ScanSnap本体(iX1600を使っています)

  • Googleアカウント(Googleドライブや各種サービスを使うために必要です)

  • OpenAIのキー(APIを使用するため、課金アカウントが必要です)

処理の流れ

  1. ScanSnapでレシートをスキャンする
    →連携機能でGoogle ドライブにレシート画像が保存される

  2. Google Apps Scriptを実行する
    →レシート画像をOpenAI APIで仕分け
    →結果がGoogle スプレッドシートに出力される

事前準備

OpenAIのキーの取得

こちらを参考にして取得しておいてください。
https://qiita.com/kurata04/items/a10bdc44cc0d1e62dad3

Google ドライブで作成しておくもの

  • フォルダ
    親となるプロジェクト用と仕分け完了用の2つ必要になります。
    まず、プロジェクトフォルダを作成し、その下に仕分け完了フォルダを作成してください。
    作成後、それぞれのフォルダのIDを取得しておきます。
    フォルダを開いたときのURLの*****の部分がIDになります。
    https://drive.google.com/drive/folders/*****
    ※後述のGoogle スプレッドシート、Google Apps Scriptもすべて、プロジェクトフォルダ配下に作成してください。

  • Google スプレッドシート
    仕分け結果の出力用に1つ必要になります。

    作成後、「receipts」と「log」という名前の2つの内部シートを作成してください。
    receipts」シートには仕分け結果、「log」シートにはエラーログなどが出力されます。
    こちらもIDを取得しておきます。
    こちらも開いたときのURLの*****の部分がIDになります。
    https://docs.google.com/spreadsheets/d/*****/

  • Google Apps Script(以下、GAS
    メインの処理用に1つ必要になります。
    Google ドライブのトップから「新規」→「その他」→「Google Apps Script」で作成できます。
    https://script.google.com/home/startからでも作成できます。
    内部に記述するコードや、設定は後述します。

すべてを作成した後のプロジェクトフォルダは以下のようになります。
(フォルダ名、ファイル名等は自由に設定してください)

作成後の状態

プロジェクトフォルダ:ocr-ojisan-image-folder
仕分け完了フォルダ:done
スプレッドシート:ocr-ojisan-sheet
GAS:Ocr-ojisan

ScanSnapの設定

ScanSnap Homeアプリを起動し、「クラウドに送る」の設定を行います。
Google ドライブとの連携方法はマニュアル公式ヘルプ等を参照してください。
保存先」のフォルダは先ほど作成したプロジェクトフォルダを選択します。
原稿種判別はせず、e文書対応とOCR精度向上のため、画質は最高設定にしています。

詳細設定」の各設定は以下になります。

タイトル

タイトルはファイル名のことで、
デフォルト(日付のみ)では、
1日に2回以上使うときにファイル名が重複してしまうので、
時分秒を加えています。

yyyy-MM-dd-HH-mm-ss

ファイル形式
スキャン
ファイルサイズ

以上が事前準備となります。

GASのセッティング

それではメイン処理を担うGASのセッティングをやっていきましょう。
まず、メニューより「プロジェクトの設定」を開きます。

以下の「スクリプトプロパティ」を設定します。

  • DONE_FOLDER_ID
    仕分け完了後フォルダのID

  • OPENAI_KEY
    OpenAIのキー

  • OPENAI_MODEL
    「gpt-4o-2024-08-06」をセットします(2024年8月時点で安価かつ高性能なGPT-4oのモデルです)

  • PROJECT_FOLDER_ID
    プロジェクトフォルダのID

  • SPREADSHEET_ID
    Google スプレットシートのID

GASコード

次にメニューの「エディタ」を開きます。

以下をコピぺします。

const USER_PROPERTIES = PropertiesService.getScriptProperties();
function getProperty(key) {
    return USER_PROPERTIES.getProperty(key);
}

const PROMPT = `
添付画像を解析し、日付、金額、勘定科目の情報を抽出してください。
## Instructions
- 日付はYYYY-MM-DDフォーマットであること
- 日付の年が2桁の場合は西暦とみなすこと
- 金額は通貨単位をつけずに数値のみであること
- 勘定科目は明細から推測すること
- 結果は以下のフォーマットのJSONのみを返すこと
{
  "date": string,
  "amount": number,
  "account_title": string
}
`;

// メイン処理
function main() {
    try {
        // 画像フォルダを取得
        const folder = DriveApp.getFolderById(getProperty('PROJECT_FOLDER_ID'));
        const files = folder.getFiles();
        // doneフォルダを取得
        const doneFolder = DriveApp.getFolderById(getProperty('DONE_FOLDER_ID'));

        while (files.hasNext()) {
            const file = files.next();
            let result;

            try {
                result = postImage(file);
            } catch (e) {
                log('API call failed for file: ' + file.getName() + ', error: ' + e);
                continue; // スキップして次のファイルを処理
            }

            // 処理済みの画像を移動
            file.moveTo(doneFolder);

            // 新しいURLを取得
            const imageUrl = file.getUrl();

            // スプレッドシートに書き込む
            const sheet = SpreadsheetApp.openById(getProperty("SPREADSHEET_ID")).getSheetByName("receipts");
            if (sheet) {
                const lastRow = sheet.getLastRow();
                var newRow = lastRow + 1;
                var values = [result.date, result.amount, result.account_title, imageUrl];
                for (var j = 0; j < values.length; j++) {
                    sheet.getRange(newRow, j + 1).setValue(values[j]);
                }
            } else {
                log('Sheet not found: receipts');
            }
        }
    } catch (e) {
        log(e);
    }
}

// OpenAI APIに画像をPOST
function postImage(file) {

    const headers = {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + getProperty('OPENAI_KEY')
    };

    const imageBlob = file.getBlob();
    const base64Image = Utilities.base64Encode(imageBlob.getBytes());

    let messages = [];
    messages.push({
        'role': 'user',
        'content': [
            {'type': 'text', 'text': PROMPT},
            {'type': 'image_url', 'image_url': {'url': `data:image/jpeg;base64,${base64Image}`}}
        ]
    });

    const payload = {
        "model": getProperty('OPENAI_MODEL'),
        "temperature": 0,
        "messages": messages
    };

    const options = {
        "method": "post",
        "headers": headers,
        "payload": JSON.stringify(payload)
    };

    const response = UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', options);
    const data = JSON.parse(response.getContentText());

    const content = data.choices[0].message.content;
    const json = content.match(/```json\n([\s\S]*?)\n```/)[1];
    return JSON.parse(json);
}

// ログシートを返す
function getLogSheet() {
    if (getLogSheet.memoSheet) return getLogSheet.memoSheet;

    const ss = SpreadsheetApp.openById(getProperty("SPREADSHEET_ID"));
    getLogSheet.memoSheet = ss.getSheetByName("log");
    return getLogSheet.memoSheet;
}

// ログ書き込み
function log(msg) {
    const sheet = getLogSheet();
    const now = new Date();
    sheet.appendRow([now, msg]);
}

PROMPTにセットしている文字列が生成AIに対する指示となっており、
仕分け処理を調整するときは、こちらを修正します。

const PROMPT = `
添付画像を解析し、日付、金額、勘定科目の情報を抽出してください。
## Instructions
- 日付はYYYY-MM-DDフォーマットであること
- 日付の年が2桁の場合は西暦とみなすこと
- 金額は通貨単位をつけずに数値のみであること
- 勘定科目は明細から推測すること
- 結果は以下のフォーマットのJSONのみを返すこと
{
"date": string,
"amount": number,
"account_title": string
}
`;

テスト

それではテストしてみましょう。
以下のレシート画像をダウンロードして、プロジェクトフォルダに入れてください。

以下のようになっていればOKです。

GASのエディタ画面の上部のプルダウンから「main」を選び、「▷実行」します。
※初回は権限の許可確認が出てきますが、すべてOKにしてください。

しばらく待ちます。
エディタ画面の下部の「実行ログ」に「実行完了」が表示されると仕分け処理は終了です。

Google スプレッドシートの「receipts」シートを確認します。

仕分けできていますね!
左から
日付、金額、勘定科目、レシート画像URL
となっています。

レシート画像URLのセルをマウスオーバーすると、プレビューが表示されます。
クリックすると別タブで拡大画像が表示されます。

プロジェクトフォルダに戻り、画像が仕分け完了フォルダに移動していれば、テスト完了です。
お疲れ様でした🤗

精度はどうなの?

ScanSnapに実装されているOCRよりは精度は高いと感じます。
ただ、完璧ではありませんので、チェックは必要です。

私はもともと、この仕分け処理を1日に1回実行する、
というトリガーを設定して自動で実行していましたが、
結局、チェックに一定の時間を取られるので、
レシート処理をする、というまとまった時間の中で一緒にする方が効率が良いと判断して、
手動で実行するようになりました。

普段は自動処理をして一気にチェックしたい、
という方はトリガーの設定をしてみてください。
GASのメニューの「トリガー」から、様々なタイミングでのスケジュール実行の設定が可能となっています。

読み取り、仕分けに失敗するパターン

読み取り精度は、基本的にはレシートの印字濃度や汚れといった状態に依存しますが、
生成AI起因によって仕分けに失敗する特殊なパターンも存在します。

例えば、
日付の年が2桁の場合、和暦と解釈する
というものがあります。
「24-08-01」を平成24年→2012年と変換するパターンです。
読み取ってはいるが、生成AIであるが故に勝手解釈をしてしまう、ということでしょうか。
プロンプトでもそうしないよう指示していますが、たまに起こります。

以下、実際のレシート画像と併せて失敗パターンを見ていきましょう。

  • 文字が欠けたレシート
    合計の文字が読み取れず、お釣りの方を正としたようです。

文字が欠けているレシート
文字が欠けているレシートの読み取り結果
  • 薄くなったレシート
    この状態のレシートの日付は目視でも厳しいレベルです😓
    料金を読み取ったのは優秀(?)

文字の色が抜けているレシート
文字の色が抜けているレシートの読み取り結果
  • 手書きレシート
    手書きは全般的にまだまだ弱いです。これは致し方ないところです…

手書きレシート
手書きレシートの読み取り結果
  • 手書きレシート(成功例)
    手書きでも成功することはあります。
    書き手の文体とAIモデルの得手不得手が関係していそうです。

手書きレシート(成功例)
手書きレシート(成功例)の読み取り結果

コストはどうなの?

基本的に気にするのはOpenAI APIのコストです。https://openai.com/api/pricing/

画像の大きさに依存するようで、
使用モデルの「gpt-4o-2024-08-06」では
150px * 150px = $0.000638
とのことですが、
正直、よく分かりません(笑)

こちらの画面でコストをチェックしてしていますが
個人的には、安いと思ってます。
https://platform.openai.com/organization/usage

以下は16枚ほどのレシート画像を処理したときのコストです。

途中でモデルを変更したため、GPT-4oとminiが混在しています

今後の課題

チェックが必要、勘定科目が一定していない、など、
まだまだ自動化への課題は残っていますが、
レシートの仕分けという、あまり人生に有益ではない作業に使う時間を最小限にして、
少しでもQOLを上げていきたいですね。

それでは皆さま、良いスキャンライフを!👍

謎のエラー

テスト時に遭遇しましたが、画像のサイズが20MB未満であっても、以下のエラーが出ることがあるようです。


いいなと思ったら応援しよう!