railsでGeminiを使って遊んでみた忘備録

節分に出てくる鬼。
鬼と言えば、鬼のパンツ。
鬼のパンツといえば、鬼のパンツはいいパンツ。

だがいいパンツとは誰が判定しているのか。
AIに判定してもらいましょう〜

ということで、APIを使って遊んでみたいなと、ちょっと時期過ぎてる気はしますが節分と絡めてGeminiPro使ってみました。

GeminiProを選んだ理由は無料でアクセスでき、1分あたり最大60クエリが利用可能とのことだったので、無料のうちにやってみました。2024年初頭までは無料とのことでしたが、参考にする際には公式で確認してください。

基本的には公式に書いてあるこテキストと画像入力の箇所を参考にしました。
https://ai.google.dev/tutorials/rest_quickstart

---

早速実装していきますが、conrtollerやmodelの細かいところはすっ飛ばします。
というのもPhoto DBにはimageとgemini_textの２個のカラムのみなので。

```ruby
# 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のやり取りの部分をモデルのところに書いてみました。
```ruby
# 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
```

---

１つずつ見ていきます。
```ruby
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リクエストを送信するための準備を行なっています。

```ruby
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からの応答を受け取るための手順を行なっています。
```ruby
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からのレスポンスを処理するためのコードです。
```ruby
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として　レコードを更新。

画像がうまく読み込んでくれなかったりした場合は、返ってくるテスストは存在しないので、代替テキストとして固定のメッセージを使用して更新しました。

というのも鬼の`パンツ`として判定したかったのですが、このパンツが下着という意味のパンツとして捉えられてしまい、不適切な表現として怒られてしまいました笑。

（本来の鬼のパンツが、ズボンという意味なのか、下着という意味なのかどっちかは知りませんが）
苦肉の策で`「鬼柄のズボン」`として判定をお願いしましたが、パンツという目線で判定して欲しかったですねー

---

実際に判定してもらったのはこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2346730/30dbeb58-6a4d-9468-cc23-9ff3e68874cc.png)

多くは試してませんが、見た感じGeminiの日本語のバリエーションはまだ少なそう。
もっと捻ったら面白そうな返答は出せそうですね