初めに
沢山の家計簿アプリが存在しますが,自分にマッチしたアプリってなかなか見つからないですよね...
自分で作ってしまえばよくね!ってことで根本的な機能を考えてみました!
非常にシンプルでレシートを認識して,支出ごとにカテゴライズします.
アプリ概要
カメラでレシートを撮影し,Google Cloud Runに送信.Cloud Runではレシート画像からCloud Vision APIにてテキスト認識を行い,Geminiにテキストを渡して支出ごとにカテゴライズします.
デザイナー画面
機能 | 内容 |
---|---|
カメラ | ボタン,写真撮影 |
認識 | ボタン,テキスト認識を行う |
保存 | ボタン,結果をTinyDBに保存する |
表示 | ボタン,TinyDBに保存されているデータを表示する |
image1 | 撮影した画像を表示する |
Label1 | ラベル,認識結果もしくはTinyDBに保存したデータの表示 |
Camera1 | 写真撮影 |
Web1 | CloudRunへのアクセス |
KIO4_Base641 | 画像をbase64にエンコードする |
TinyDB1 | CloudRunからの結果の保存場所 |
blocks画面
変数
CloudRunURL
CloudRunにアクセスするための1URL
ボタン
認識
画像をbase64に変換しCloudCunへ送信.レスポンスはLabel1に表示.
表示
TinyDBに保存した今までのCloudRunの出力をLabel1に表示.
when Camera1. After picture
when KIO4_Base64. After Image Base64
画像をBase64にエンコードして,Cloudrunに送信.
when Web1. GotText
Cloud Runからのレスポンス.
ResponseContentから必要な情報のみを表示.
Google Cloud Runのコード
Cloud Vision APIを使用できるようにし,Cloud Runにコードを記述します.
生成AIに書いてもらいました~~!AIってほんとに優秀ですね.
main.py
import base64
import json
import os
from googleapiclient import discovery
from flask import Flask, request, jsonify
import functions_framework
import requests
app = Flask(__name__)
# Google Cloud Vision API のエンドポイント
VISION_API_ENDPOINT = "https://vision.googleapis.com/v1/images:annotate"
# Gemini API のエンドポイントとAPIキー
GEMINI_API_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"
GEMINI_API_KEY = "XXXXXXXXXXXXXXXXXXXXXX" #ご自身のAPIKeyに書き換える
@app.route('/extract_text', methods=['POST']) # POSTメソッドに変更
def extract_text():
"""Base64エンコードされた画像を取得し、テキストを抽出し、Gemini APIでカテゴリ分けし、JSONで返す"""
try:
# リクエストからBase64エンコードされた画像データを取得
data = request.get_json()
base64_image = data.get('image')
if not base64_image:
return jsonify({"error": "imageデータがありません"}), 400
# Google Cloud Vision API のクライアントを作成
service = discovery.build(
'vision', 'v1', credentials=None,
cache_discovery=False, # キャッシュを無効にする
static_discovery=False # 静的検出を無効にする
)
# リクエストの本文(JSON)を作成
request_body_vision = {
"requests": [
{
"image": {
"content": base64_image
},
"features": [
{
"type": "TEXT_DETECTION"
}
]
}
]
}
# Google Cloud Vision API にリクエストを送信
response_vision = service.images().annotate(body=request_body_vision).execute()
# Vision API のレスポンスを処理
extracted_text = ""
if response_vision.get('responses'):
text_annotations = response_vision["responses"][0].get("textAnnotations", [])
# 全てのテキストを結合
extracted_text = "\n".join([annotation.get('description', '') for annotation in text_annotations])
else:
return jsonify({"error": f"Vision APIリクエストエラー: {response_vision.get('error')}"}), 500
# Gemini API へのリクエストを作成
headers_gemini = {
'Content-Type': 'application/json'
}
payload_gemini = {
"contents": [{
"parts": [
{"text": f"""これから家計簿を作成します。
レシートテキストを渡すのでそれをカテゴライズし、カテゴリーごとの金額を出力してください。カテゴライズできない購入品に関しては「その他」に分類してください。
また、購入日と店舗名、決済方法も出力してください。
### レシートテキスト
{extracted_text}
"""}
]
}],
"generationConfig": {
"response_mime_type": "application/json",
"response_schema": {
"type": "ARRAY",
"items": {
"type": "OBJECT",
"required":["購入日", "店舗名", "決済方法"],
"properties": {
"購入日": {
"type":"STRING",
"description":"購入日を出力"
},
"店舗名": {
"type":"STRING",
"description":"店舗名を出力"
},
"決済方法": {
"type":"STRING",
"description":"決済方法を出力"
},
"食費": {
"type":"INTEGER",
"description":"食費の合計を数値で出力"
},
"交通費": {
"type":"INTEGER",
"description":"交通費の合計を数値で出力"
},
"日用品": {
"type":"INTEGER",
"description":"日用品の合計を数値で出力"
},
"衣服": {
"type":"INTEGER",
"description":"衣服の合計を数値で出力"
},
"コンビニ": {
"type":"INTEGER",
"description":"コンビニの合計を数値で出力"
},
"その他": {
"type":"INTEGER",
"description":"分類できない支出の合計を数値で出力"
}
}
}
}
}
}
# Gemini API にリクエストを送信
response_gemini = requests.post(
f"{GEMINI_API_ENDPOINT}?key={GEMINI_API_KEY}",
headers=headers_gemini,
json=payload_gemini
)
response_gemini.raise_for_status() # エラーレスポンスの場合は例外を発生させる
gemini_results = response_gemini.json()
# Gemini API のレスポンスを処理
if gemini_results.get('candidates') and gemini_results['candidates'][0].get('content') and gemini_results['candidates'][0]['content'].get('parts'):
gemini_response_text = gemini_results['candidates'][0]['content']['parts'][0].get('text')
try:
categorized_data = json.loads(gemini_response_text)
return jsonify({"extracted_text": extracted_text, "categorized_data": categorized_data})
except json.JSONDecodeError:
return jsonify({"error": f"Gemini APIのレスポンスをJSONとしてデコードできませんでした: {gemini_response_text}"}), 500
else:
return jsonify({"error": f"Gemini APIからの有効なレスポンスが見つかりませんでした: {gemini_results}"}), 500
except Exception as e:
return jsonify({"error": str(e)}), 500
@functions_framework.http
def main(request):
"""HTTPリクエストを処理する"""
return extract_text()
requirements.txt
functions-framework==3.*
requests
google-api-python-client
実行結果
終わりに
カテゴライズできたので,表にまとめられるようにしたいですね!