[コード公開] GASとOpenAI Visionでレシート管理を半自動化してみた。
こんにちは、note AI creativeの田中です。
OpenAIのAPIには画像を解析できる「Vision」機能があることをご存知でしょうか。
https://platform.openai.com/docs/guides/vision
先日発表されたGPT-3.5-Turboより60%安い「GPT-4o mini」でもこのVision機能が使えるということで、GASと組み合わせて「レシート画像を読み取って自動でスプレッドシートに追記する仕組み」を作ってみました。
全体の流れ
レシート画像をGoogleドライブにアップロード(手動)
Google App Script(以下、GAS)が画像をドライブから取得
GASからOpenAIのAPI(Vision)にリクエストし、画像から店名・金額・日付を取得
OpenAIのレスポンスをもとに結果をスプレッドシートに書き込み
事前準備
レシートアップロード用のフォルダを2つ作成する
「レシートをアップロードする先のフォルダ」と「スプシ記載済レシート画像を保管しておく用のフォルダ」の2つ作成しておきます。
自分の場合は、「領収書」フォルダとその配下に「シート記載済」フォルダを作っています。
同じレシート画像をGASで何度も処理してしまわないよう、処理が終わったらシート記載済フォルダに移動させます。
スプレッドシート作成、ヘッダー準備
結果を書き込むようのスプレッドシートを事前に作成しておきます。シートには「date」「shop」「price」「支払いした人」のヘッダーを用意しておきます。
iPhoneで撮影した画像がJPEGで保存されるよう設定
iPhoneの「高効率」のカメラ設定で保存されるHEIF形式は、OpenAIのAPIが処理できないため、JPEGで保存されるように事前設定が必要です。
iPhoneの「設定」>「カメラ」>「フォーマット」で「互換性優先」を選択します。これで、撮影した画像はJPEGで保存されるようになります。
GASを実装
スプレッドシート上の「拡張機能」>「App Script」からGASのエディターを起動します。
OpenAIのAPIキーを「プロジェクトの設定」>「スクリプトプロパティ」で「OPENAI_API_KEY」で入れておきます。
以下に実際のコードを記載しますが、一部の変数は自身で設定が必要です。
sourceFolderId:レシート画像を保存するフォルダのID
destinationFolderId:シートに記載済のレシートファイルを移動させるフォルダのID
sheetName:シート名(例:「シート1」など)
payerName:名前(例:「けんぞう」など)
spreadsheetId:スプレッドシートのID
ちなみに、スプレッドシートやフォルダーのIDはURLの中に含まれる一意の識別子になります。↓の黒塗りされているところ。
function main() {
var sourceFolderId = '<ここにレシートを保存するフォルダーIDを記載>';
var destinationFolderId = '<ここに移動先のフォルダーIDを記載>';
var sheetName = '<ここにシート名>';
var payerName = '<ここに支払いした人の名前>';
processFiles(sourceFolderId, destinationFolderId, sheetName, payerName);
}
function processFiles(sourceFolderId, destinationFolderId, sheetName, payerName) {
var spreadsheetId = '<ここにスプレッドシートIDを記載>';
var sourceFolder = DriveApp.getFolderById(sourceFolderId);
var destinationFolder = DriveApp.getFolderById(destinationFolderId);
var files = sourceFolder.getFilesByType('image/jpeg');
while (files.hasNext()) {
var file = files.next();
var base64Image = convertToBase64(file);
// OpenAIのAPIにリクエスト
var response = callOpenAIAPI(base64Image);
// choices[0].message.content のみを取り出してJSONをパース
var content = response.choices[0].message.content;
var jsonData = JSON.parse(content);
// スプレッドシートに書き込む
writeToSpreadsheet(spreadsheetId, sheetName, jsonData, payerName);
// ファイル名をリネーム
var newFileName = jsonData.date.replace(/\//g, '-') + ' ' + jsonData.shop + '.jpeg';
file.setName(newFileName);
// ファイルを別フォルダに移動
moveFileToFolder(file, destinationFolder);
}
}
function moveFileToFolder(file, destinationFolder) {
destinationFolder.addFile(file);
var sourceFolder = file.getParents().next();
sourceFolder.removeFile(file);
}
function writeToSpreadsheet(spreadsheetId, sheetName, jsonData, payerName) {
var sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
// ヘッダー行を取得
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
// 各ヘッダーに対応する列を特定
var shopCol = headers.indexOf('shop') + 1;
var dateCol = headers.indexOf('date') + 1;
var priceCol = headers.indexOf('price') + 1;
var payerCol = headers.indexOf('支払いした人') + 1;
// 新しい行を追加してデータを書き込む
var lastRow = sheet.getLastRow();
sheet.getRange(lastRow + 1, shopCol).setValue(jsonData.shop);
sheet.getRange(lastRow + 1, dateCol).setValue(jsonData.date);
sheet.getRange(lastRow + 1, priceCol).setValue(jsonData.price);
sheet.getRange(lastRow + 1, payerCol).setValue(payerName);
}
function getPromptText() {
return `
画像は領収書の写真です。
画像を解析して、店名・日付・金額を以下のJSONフォーマットで出力して下さい。
# JSONフォーマット
{ "shop": "店名(String)", "date": "日付(YYYY/mm/dd)", "price": "金額(Integer)" }
# 出力例
{ "shop":"松屋", "date":"2023/02/12", "price": 1000 }
`;
}
function convertToBase64(file) {
var blob = file.getBlob();
var base64Data = Utilities.base64Encode(blob.getBytes());
return base64Data;
}
function callOpenAIAPI(base64Image) {
var scriptProperties = PropertiesService.getScriptProperties();
var apiKey = scriptProperties.getProperty('OPENAI_API_KEY');
var url = 'https://api.openai.com/v1/chat/completions';
var payload = {
model: "gpt-4o-mini",
messages: [
{
role: "user",
content: [
{
type: "text",
text: getPromptText()
},
{
type: "image_url",
image_url: {
url: "data:image/jpeg;base64," + base64Image
}
}
]
}
],
max_tokens: 300,
response_format: { "type": "json_object" }
};
var options = {
method: 'post',
contentType: 'application/json',
headers: {
Authorization: 'Bearer ' + apiKey
},
payload: JSON.stringify(payload)
};
var response = UrlFetchApp.fetch(url, options);
return JSON.parse(response.getContentText());
}
テスト実行してみる
上記の変数を全てセットしたうえで、領収書のフォルダにレシート画像をアップロードしてテスト実行してみます。
GASエディターで「main」を選択して実行をクリックすると実行できます。
成功すれば、シート上で以下のように入力された状態になるはずです。
ファイルはリネームされた状態で、処理済のフォルダーに移動しているはずです。
スケジュール実行する
定期的にGASが起動するように設定します。この設定によって、「Googleドライブに画像さえあげておけば、あとはGASが自動でスプシに転記してくれる」状態を作れます。
GASエディター上で、画面右側の「トリガー」>「トリガーを追加」から以下のように設定します。
複数人のレシート管理に対応
レシートをアップロードするフォルダを別にすれば、複数人のレシート管理に対応できます。
function main() {
var sourceFolderIdA = '<Aさんのレシートを保存するフォルダーIDを記載>';
var destinationFolderIdA = '<Aさんの移動先のフォルダーIDを記載>';
var sheetName = '<ここにシート名>';
var payerName = '<ここに支払いした人の名前>';
processFiles(sourceFolderIdA, destinationFolderIdA, sheetName, payerName);
var sourceFolderIdB = '<Bさんのレシートを保存するフォルダーIDを記載>';
var destinationFolderIdB = '<Bさんの移動先のフォルダーIDを記載>';
processFiles(sourceFolderIdB, destinationFolderIdB, sheetName, payerName);
}
▼noteの技術記事が読みたい方はこちら