節分に出てくる鬼。
鬼と言えば、鬼のパンツ。
鬼のパンツといえば、鬼のパンツはいいパンツ。
だがいいパンツとは誰が判定しているのか。
AIに判定してもらいましょう〜
ということで、APIを使って遊んでみたいなと、ちょっと時期過ぎてる気はしますが節分と絡めてGeminiPro使ってみました。
GeminiProを選んだ理由は無料でアクセスでき、1分あたり最大60クエリが利用可能とのことだったので、無料のうちにやってみました。2024年初頭までは無料とのことでしたが、参考にする際には公式で確認してください。
基本的には公式に書いてあるこテキストと画像入力の箇所を参考にしました。
https://ai.google.dev/tutorials/rest_quickstart
早速実装していきますが、conrtollerやmodelの細かいところはすっ飛ばします。
というのもPhoto DBにはimageとgemini_textの2個のカラムのみなので。
# controller
def create
@photo = Photo.new(photo_params)
if @photo.save
@photo.judges_for_gemini
redirect_to judge_path(@photo)
else
render :new
end
end
今回はAPIのやり取りの部分をモデルのところに書いてみました。
# model
def judges_for_gemini
json_data = {
"contents": [
{
"parts": [
{ "text": "鬼柄のズボンが見えたら、仲良い感じみたいに褒めてください。" },
{
inline_data: {
mime_type: 'image/jpeg',
data: Base64.strict_encode64(self.image.download)
}
}
]
}
]
}
url = URI.parse("https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=#{ENV['GEMINI_API_KEY']}")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url, { 'Content-Type' => 'application/json' })
request.body = json_data.to_json
response = http.request(request)
if response.is_a?(Net::HTTPSuccess)
parsed_response = JSON.parse(response.body)
text_content = parsed_response.dig("candidates", 0, "content", "parts", 0, "text")
if text_content.present?
self.update(gemini_text: text_content)
else
self.update(gemini_text: 'いいパンツじゃない😢')
end
end
end
1つずつ見ていきます。
json_data = {
"contents": [
{
"parts": [
{ "text": "鬼柄のズボンが見えたら、仲良い感じみたいに褒めてください。" },
{
inline_data: {
mime_type: 'image/jpeg',
data: Base64.strict_encode64(self.image.download)
}
}
]
}
]
}
この部分は、ほぼドキュメント通りですね。
写真はどうやらエンコードしないといけないみたいです。
rubyのエンコードしてくれるメソッドで、Base64.encode64
というのもあるんですが、このメソッドはエンコード後の文字で60文字ごとに改行が入ってしまい、正しく値が渡りませんでした。
Base64.strict_encode64
を使うと改行は入らないので、そちらを使いましょう
この部分はGemini APIに対してHTTPリクエストを送信するための準備を行なっています。
url = URI.parse("https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=#{ENV['GEMINI_API_KEY']}")
http = Net::HTTP.new(url.host, url.port)
URIの作成
URI.parse
を使用して、Gemini APIのエンドポイントのURLを構築します。
エンドポイントにはAPIキーも含める必要があので、環境変数からAPIキーを取得し、URLのクエリパラメーターとして追加します。
HTTPオブジェクトの作成
Net::HTTP.new
を使用して、Gemini APIのホスト名とポート番号を指定して新しいHTTPオブジェクトを作成。ここでは、URIからホスト名とポート番号を取得。
この部分は、Gemini APIに対してPOSTリクエストを送信し、Gemini APIからの応答を受け取るための手順を行なっています。
request = Net::HTTP::Post.new(url, { 'Content-Type' => 'application/json' })
request.body = json_data.to_json
response = http.request(request)
POSTリクエストの作成
Net::HTTP::Post.new
を使用して、POSTメソッドを指定して新しいHTTP POSTリクエストオブジェクトを作成。
リクエストヘッダーには、Content-Typeが'application/json'であることを指定。
これは、リクエストの本文がJSON形式であることをGemini APIに伝えるため。
リクエストボディの設定
request.body
を使用して、リクエストボディにJSON形式のデータを設定。
to_json
を使用して、json_data ハッシュをJSON文字列に変換。これにより、Gemini APIに送信されるリクエストボディには、事前に作成したJSONデータが含まれる。
HTTPリクエストの送信
http.request(request)
を使用して、作成したHTTP POSTリクエストを実際に送信。
このメソッドはGemini APIにリクエストを送信し、APIからの応答を受け取る。
応答はresponse
変数に格納される。
この部分はGemini APIからのレスポンスを処理するためのコードです。
if response.is_a?(Net::HTTPSuccess)
parsed_response = JSON.parse(response.body)
text_content = parsed_response.dig("candidates", 0, "content", "parts", 0, "text")
if text_content.present?
self.update(gemini_text: text_content)
else
self.update(gemini_text: 'いいパンツじゃない😢')
end
end
成功したリクエストかどうかのチェック
response.is_a?(Net::HTTPSuccess)
を使用して、APIからのレスポンスが成功したものかどうかを確認。成功した場合のみ、処理を続行。
レスポンスの解析
JSON.parse(response.body)
を使用して、APIからのレスポンスボディをJSON形式からRubyのハッシュにパースします。これにより、APIからの応答データを扱いやすい形式に変換します。
テキストコンテンツの取得
parsed_response.dig("candidates", 0, "content", "parts", 0, "text")
では、APIからの応答データからテキストコンテンツを取得します。
この部分は、APIの応答データ構造に従って、適切な階層のキーを指定してテキストコンテンツを抽出。
Geminiテキストの更新
取得したテキストコンテンツが存在する場合は、履歴として見返せるように、APIから返ってきたテキストを、gemini_textとして レコードを更新。
画像がうまく読み込んでくれなかったりした場合は、返ってくるテスストは存在しないので、代替テキストとして固定のメッセージを使用して更新しました。
というのも鬼のパンツ
として判定したかったのですが、このパンツが下着という意味のパンツとして捉えられてしまい、不適切な表現として怒られてしまいました笑。
(本来の鬼のパンツが、ズボンという意味なのか、下着という意味なのかどっちかは知りませんが)
苦肉の策で「鬼柄のズボン」
として判定をお願いしましたが、パンツという目線で判定して欲しかったですねー
多くは試してませんが、見た感じGeminiの日本語のバリエーションはまだ少なそう。
もっと捻ったら面白そうな返答は出せそうですね