TypeChatとCloud Vision APIを使用したレシート解析
はじめに
過去に家計簿系のアプリの個人開発をしている中で、レシートの読み取り機能を実装した際に試したことがあったので、簡単にまとめてみたいと思います。
処理の流れ
処理の流れは画像のままですが、以下のようになります。
- ユーザーがレシートの画像をアップロード
- 1の画像をCloud Vision APIで画像解析
- 2で取得したデータをTypeChatに渡す
- JSONとして出力
OCR系のOSSなどのライブラリは何個かありましたが、今回はCloud Vision APIを選択しました。そこまで深い理由はないです。なるべく早く検証してみたかったという気持ちが強かったです。
TypeChatについて
TypeChatは、microsoftのOSSであり、型の力を活用してAIとのやりとりをより正確かつ安全に行うためのライブラリです。従来のチャットAIでは、自然言語の解釈に曖昧さがつきものですが、TypeChatでは型を定義することで、出力されるデータの構造を保証し、開発者が扱いやすくすることができます。
特徴
- TypeScriptの型を使ってAIの出力を制約する
- 型の制約を利用してAIの出力を制御できるため、意図しないデータの生成を防ぎやすくなる
実装
実装に使用したコードは以下です。
事前に必要な情報としては、以下になります。
OPENAI_API_KEY=xxxxx
OPENAI_MODEL=xxxxx
GOOGLE_APPLICATION_CREDENTIALS=xxxxx
主要な箇所に絞って解説していきます。
CloudVisionClientの作成
import { ImageAnnotatorClient } from '@google-cloud/vision';
import fs from 'fs';
export class CloudVisionClient {
private readonly client: ImageAnnotatorClient;
constructor() {
this.client = new ImageAnnotatorClient();
}
async fetchImageToText(path: string): Promise<string | undefined> {
const request = {
image: {
content: fs.readFileSync(path),
},
imageContext: {
languageHints: ['ja'],
},
};
const [result] = await this.client.textDetection(request);
const detections = result.textAnnotations;
const description = detections?.[0].description;
return description ?? undefined;
}
}
これは単純にCloud Vision APIと通信をするためのものです。特に珍しくないですが、imageContext
のlanguageHints
には日本語を指定しています。
interfaceの定義
TypeChatでinterfaceを定義する理由は、AIの出力を型で制約し、意図しないデータを防ぐためです。型を指定することによって、構造化されたJSONが手に入り、処理の一貫性を保てます。また、開発者が期待するフォーマットに従ったデータを受け取れるのが嬉しいポイントです。
export interface Product {
price: number;
name: string;
count: number;
category: 'food' | 'drink' | 'snack' | 'other';
}
export interface Products {
products: Product[];
}
今回自分のケースでは、スーパーで購入した商品などが対象となるので以下のようにしています。
- price...値段
- name...商品名
- count...個数
- category...カテゴリ(カテゴリ分けできない場合は、otherに分類されます)
メイン部分の実装
import * as fs from 'fs';
import * as path from 'path';
import * as typechat from 'typechat';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { Products } from './interface.js';
import { CloudVisionClient } from 'cloud-vision-client.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.join(__dirname, '../.env') });
const cloudVisionClient = new CloudVisionClient();
const model = typechat.createLanguageModel(process.env);
const schema = fs.readFileSync(path.join(__dirname, 'interface.ts'), 'utf8');
const translator = typechat.createJsonTranslator<Products>(
model,
schema,
'Products'
);
const getOcrResult = async () => {
const result = await cloudVisionClient.fetchImageToText(
`${__dirname}/images/image.jpg`
);
return result;
};
const text = await getOcrResult();
const response = await translator.translate(text as string);
console.log(JSON.stringify(response.data, null, 2));
このコードの中ではtypechatを使用した箇所がポイントです。
const model = typechat.createLanguageModel(process.env);
createLanguageModelにはどのモデルを使用するかを指定します。(OpenAI or Azure OpenAIのみが対象です。)
以下該当のコードになります。
環境変数がOpenAIの環境変数の場合(OPENAI_API_KEY
)は、createOpenAILanguageModelという関数が呼ばれ、Azure OpenAIの場合(AZURE_OPENAI_API_KEY
)は、createAzureOpenAILanguageModelという関数が呼ばれます。
その後、スキーマを指定します。
const schema = fs.readFileSync(path.join(__dirname, 'interface.ts'), 'utf8');
先ほど定義したinterfaceが対象となります。
const translator = typechat.createJsonTranslator<Products>(
model,
schema,
'Products'
);
createJsonTranslator
に先ほど指定した、model, schemaを渡します。
createJsonTranslator
は、自然言語のテキストを指定した型のJSONに変換するTypeChatの関数です。指定したTypeScriptスキーマのtypeNameに対応するJSONを生成し、バリデーションを行う TypeChatJsonTranslator<T>
を返却します。
const response = await translator.translate(text as string);
translate
は、自然言語のテキスト(request)を指定した型のJSONに変換するメソッドです。変換結果がスキーマに適合しない場合、attemptRepair
がtrueならエラー情報を基に再試行します(参考)。
実際に試す
この画像が対象です。(完全に酒のあてです)
出力されたJSON
{
"products": [
{
"price": 3,
"name": "レジ袋L (バイオマス 30%",
"count": 1,
"category": "other"
},
{
"price": 128,
"name": "かっぱえびせん",
"count": 1,
"category": "snack"
},
{
"price": 148,
"name": "サントリー ジムビーム",
"count": 1,
"category": "drink"
},
{
"price": 140,
"name": "キリン淡麗グリーンラ",
"count": 1,
"category": "drink"
},
{
"price": 105,
"name": "サントリーこだわり酒場",
"count": 1,
"category": "drink"
}
]
}
まとめ
商品名やcatgoryなどちゃんと意図した通りになっています。分類しづらい商品や画質の違いなども試してみたいと思います。今回は検証という形でやってみましたが、レシート解析や家計簿アプリへの応用がしやすいと感じました。
Discussion