React × Gemini × LangChainで作る画像読み取りくん
まず初めに
Gemini×LangChain入門で学んだ知識を生かして、ちょっとした作品を作りましょう。まだ読んでいない方は先にこちらを読んでいただきたいです。
前提知識
- React
- TypeScript
- TailwindCSS
- Gemini
- LangChain
今回作成するもの
入れた画像とテキストに対して、AIが答えてくれる画像よみとりくん
を作ります。本当はgifで掲載したかったのですが、うまくいかなかったので、画像でごめんなさい。
gifデモ
(追記:載せれたんですが、なんかバグりました。)
画像デモ
環境構築
React+ TS +ViteとTailwindの環境構築は、以下の本の環境構築部分をご参照ください。
Gemini + Langchain
必要なライブラリをインストールしていきましょう。
$ npm install @google/generative-ai
$ npm install @langchain/google-genai
APIKeyの取得
こちらのAPIKeyの取得を参照ください。
APIKeyの設定
ルートディレクトリに.env
をファイルを作成しましょう。下記のAPIKeyを設定しましょう。
VITE_GOOGLE_API_KEY = "<取得したAPIKey>"
コーディング
まず、ローディングUIを作りましょう。components
ディレクトリをsrc
の直下に作成し、その中にLoading.tsx
を作成しましょう。
const Loading = () => {
return(
<div className="flex justify-center">
<div className="h-6 w-6 animate-spin border-2 border-sky-400 rounded-full border-t-transparent"></div>
</div>
)
}
export default Loading
public
ディレクトリに下記の画像をno-image.png
という名前をつけて入れといてください。
次にApp.tsx
に以下を貼り付けましょう。
import { ChangeEvent, FormEvent, useState } from "react"
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { HumanMessage } from "@langchain/core/messages";
import Loading from "./components/Loading";
const App = () => {
const [file, setFile] = useState<File | null>(null)
const [text, setText] = useState<string>('')
const [preview, setPreview] = useState<string>('/no-image.png');
const [result, setResult] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (file && text) {
setIsLoading(true)
setResult('')
const vision = new ChatGoogleGenerativeAI({
apiKey: import.meta.env.VITE_GOOGLE_API_KEY,
modelName: "gemini-pro-vision",
maxOutputTokens: 2048,
});
const reader = new FileReader();
reader.onloadend = async () => {
const input2 = [
new HumanMessage({
content: [
{
type: "text",
text: `${text}`,
},
{
type: "image_url",
image_url: reader.result as string,
},
],
}),
];
const res2 = await vision.stream(input2);
for await (const chunk of res2) {
const chunkText = chunk.content;
console.log(chunkText);
setResult(prevResult => prevResult + chunkText);
}
setIsLoading(false)
setText('')
};
reader.readAsDataURL(file);
} else {
alert('入力に漏れがあります。')
}
}
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFile(e.target.files[0])
setPreview(URL.createObjectURL(e.target.files[0]))
}
}
return (
<div className="container mx-auto">
<div className="grid grid-cols-8 items-center mt-[70px]">
<div className="col-start-3 col-span-4">
<div className="bg-yellow-300 p-4 rounded">
<h1 className="text-3xl font-bold mb-2">画像読み取りくん🤨</h1>
<form onSubmit={handleSubmit}>
<div className="mb-2">
<img src={preview} alt="" onClick={() => document.getElementById('file-input')?.click()} className="cursor-pointer" />
<input type="file" id="file-input" onChange={handleImageChange} className="hidden" />
</div>
<div className="mb-2">
{result ? <p>{result}</p> : (isLoading ? <Loading /> : <></>)}
</div>
<div className="mb-2">
<input type="text" className="w-full border border-black rounded p-2 text-2xl focus:outline-0" value={text} onChange={(e) => setText(e.target.value)} disabled={isLoading} />
</div>
<button className="bg-blue-500 text-white py-1 px-3 rounded" disabled={isLoading}>送信</button>
</form>
</div>
</div>
</div>
</div>
)
}
export default App
Gemini × Langchain入門を読んでいただけた方はほぼほぼこのコードに問題はないかと思います。少し、詰まるであろうと思う点を下記にまとめておきます。
Viteでの環境変数の扱い
create-react-app
コマンドで構築したプロジェクトであれば、下記の記述で、フロントから.env
の環境変数にアクセスすることができます。
process.env.VITE_GOOGLE_API_KEY
しかし、Viteを用いた構築の場合、記述が変わります。
import.meta.env.VITE_GOOGLE_API_KEY
また、環境変数を設定する際は必ず頭にVITE
という文字列をつけてください。こうしないと正常にフロントからアクセスすることはできません。
フロントでのbase64へのエンコード
Gemini × Langchain入門ではサーバーサイドでのデモ実装であり、fs
というライブラリを用いてのエンコードを行いました。
fs
はサーバー側のみで動作するライブラリであり、今回のフロントで使うとうまくいかなくなってしまいます。そこで、Filereader
というWebAPIを用いて実装しています。
記述は至って簡単です。
readAsDataURL
というメソッドでfileをエンコードし、それが完了したら、onloaded
の中身が実行されるという形です。
const reader = new FileReader();
reader.onloadend = () => {
//base64にエンコード後に実行したいもの
};
reader.readAsDataURL(file);
streamを理解する
出力部分に以下の記述を行いました。
const res2 = await vision.stream(input2);
for await (const chunk of res2) {
const chunkText = chunk.content;
console.log(chunkText);
setResult(prevResult => prevResult + chunkText);
}
これは、データを小さな塊として受け取り、少しずつ出力しています。この記述を行うことで、まとめて全て出力されないため、最初の出力が早くなり、ユーザー体験が向上します。
ストリーミング配信のストリーミングとはこのことを指します。
まとめ
まずは、ここまでお疲れ様でした。
今回は、Gemini × Langchain入門で学んだ知識を使って、簡単な作品を作ってみました。もし、今度ご自身で作品を作る機会があれば、今回学んだことを生かしていただければと思います。
他にもWeb制作に関する情報や疑問をわかりやすくまとめて記事を書いているので、そちらも読んでいただけたら嬉しいです。
Discussion