processingでOpenAI APIを叩く

こんにちは、うだいです。
簡単な 2D グラフィックを表現する際に、最近はもっぱら`TypeScript`上で`p5.js`を使うことが多く、長らく`processing`を使っていませんでした。

しかし今回、OpenAI の API を始めとした生成 AI の講座をサークル内で行うことが決まり、`processing`から OpenAI の API を叩いて生成、表示までを行なってみるサンプルコードを制作しました。

**今回のコードでは日本語を含むリクエストボディを送信することができない**（レスポンスは処理できる）という大きな大きな課題がありますが、`processing`から REST API を叩く第一歩として参考にしていただければと思います。

![スクリーンショット 2024-05-29 2.22.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449776/1a84d3b8-b038-782f-c941-b574adbb6fb2.png)

## 準備: processing から REST API を叩く

まず、`processing`から REST API を叩くためには、`http-request`というライブラリを使います。
processing の「スケッチ」→「ライブラリをインポート」→「ライブラリを追加」から、`http-request`を検索してインストールしてください。

## processig から DALLE-E 3 の API を叩く

今回は、OpenAI の DALL-E 3 という 画像生成 AI の API を叩きます。

画像生成には時間がかかるため、非同期処理を使うことで画像生成中でも processing のフリーズしないようにしています。
非同期処理には Java の`Thread`を使っており、`fetchDalle`メソッドを別スレッドで実行しています。
また、グローバル変数として`loadedImage`を持つことで、別スレッドで読み込んだ画像をメインスレッドで描画することができます。
API キーについては環境変数から取得しています。事前に OpenAI の API キーを取得し、`OPENAI_API_KEY`という名前で環境変数に設定してください。

注意事項として、現状では日本語を含むマルチバイト文字列をリクエストボディに含めると全て文字化けし、API には無意味な文字列が送信されてしまうため、英語のみのリクエストボディを送信しています。

```java:コード全文
import http.requests.*;

// ライブラリからhttp.requestsをインストールする必要あり
Thread asyncThread;
String API_KEY;
String imageUrl;
PImage loadedImage;
boolean imageLoaded = false;

void setup() {
  API_KEY = System.getenv("OPENAI_API_KEY");
  // 非同期タスクを実行するスレッドを作成
  asyncThread = new Thread(new Runnable() {
    public void run() {
      fetchDalle();
    }
  });

  // スレッドを開始
  asyncThread.start();

  size(512, 512);
  fill(0);
  textSize(16);
  PFont font = createFont("Meiryo", 50);
  textFont(font);
  imageUrl = "Loading...";
}

void draw() {
  background(255);
  if (imageLoaded) {
    // 画像が読み込まれた場合、画像を描画
    image(loadedImage, 0, 0, width, height);
  } else {
    // 画像が読み込まれていない場合、テキストを描画
    text(imageUrl, 10, height / 2);
  }
}

void fetchDalle() {
  String url = "https://api.openai.com/v1/images/generations";
  // リクエストボディを作成
  JSONObject requestBody = new JSONObject();
  requestBody.setString("model", "dall-e-3");
  requestBody.setString("prompt", "a white siamese cat");
  requestBody.setInt("n", 1);
  requestBody.setString("size", "1024x1024");
  String requestBodyString = requestBody.toString();

  // HTTPリクエストを送信
  PostRequest request = new PostRequest(url);
  request.addHeader("Authorization", "Bearer " + API_KEY);
  request.addHeader("Content-Type", "application/json");
  request.addData(requestBodyString);
  request.send();

  // レスポンスを取得
  String responseString = request.getContent();

  println(responseString);

  JSONObject response = parseJSONObject(responseString);

  // data[0].urlを取得
  JSONArray data = response.getJSONArray("data");
  JSONObject firstData = data.getJSONObject(0);
  imageUrl = firstData.getString("url");

  // 画像を読み込む
  loadedImage = loadImage(imageUrl, "png");

  // 画像が読み込まれたかチェック
  if (loadedImage != null) {
    imageLoaded = true;
  } else {
    imageUrl = "Failed to load image.";
    println("Failed to load image.");
  }
}
```

## processig から ChatGPT の API を叩く

次に、OpenAI の ChatGPT の API を叩いてみます。
これもほとんど同じですが、リクエストボディに配列が含まれるため、少々冗長な記述を行う必要があります。
また、同じくリクエストの日本語については文字化けを起こしてしまうため、リクエストは英語、レスポンスは日本語で行います。

```java:コード全文
// ライブラリからhttp.requestsをインストールする必要あり
// 日本語リクエストがうまく処理されない
import http.requests.*;
import java.nio.charset.StandardCharsets;

Thread asyncThread;
String API_KEY;
String text;

void setup() {
  API_KEY = System.getenv("OPENAI_API_KEY");
  
  
  // 非同期タスクを実行するスレッドを作成
  asyncThread = new Thread(new Runnable() {
    public void run() {
      fetchGPT();
    }
  });
  
  // スレッドを開始
  asyncThread.start();
  
  size(800, 512);
  fill(0);
  textSize(16);
  PFont font = createFont("Meiryo", 50);
  textFont(font);
  text = "Loading...";
}

void draw() {
  background(255);
  text(text, 10, height / 2); // 表示するテキスト, x座標, y座標
}

void fetchGPT() {
  String url = "https://api.openai.com/v1/chat/completions";
  JSONArray messages = new JSONArray();
  // 最初のメッセージオブジェクトを作成
  JSONObject message1 = new JSONObject();
  message1.setString("role", "system");
  message1.setString("content", "answer in Japanese.");
  messages.append(message1);
  
  // 二つ目のメッセージオブジェクトを作成
  JSONObject message2 = new JSONObject();
  message2.setString("role", "user");
  message2.setString("content", "Hello! Please tell me about the history of Japan.");
  messages.append(message2);
  
  // リクエストボディを作成
  JSONObject requestBody = new JSONObject();
  requestBody.setString("model", "gpt-4o");
  requestBody.setJSONArray("messages", messages);
  requestBody.setInt("max_tokens", 150);
  requestBody.setFloat("temperature", 0.5);
  String requestBodyString = requestBody.toString();
  
  // HTTPリクエストを送信
  PostRequest request = new PostRequest(url);
  request.addHeader("Authorization", "Bearer " + API_KEY);
  request.addHeader("Content-Type", "application/json");
  request.addData(requestBodyString);
  request.send();
  
  // レスポンスを取得
  String responseString = request.getContent();
  byte[] bytes = responseString.getBytes(StandardCharsets.ISO_8859_1);
  responseString = new String(bytes, StandardCharsets.UTF_8);
  
  println(responseString);
  
  JSONObject response = parseJSONObject(responseString);
  
  // choices[0].message.contentを取得
  JSONArray choices = response.getJSONArray("choices");
  JSONObject firstChoice = choices.getJSONObject(0);
  String content = firstChoice.getJSONObject("message").getString("content");
  
  text = content;
}
```

## 課題

今回のコードでは無事(?)に OpenAI の API を叩くことができましたが、日本語の扱いについてはまだ課題が残っています。
`java.nio.charset.StandardCharsets`を使用しましたが、Macではそれでも文字化けが治らず、Windowsであってもレスポンスの表示はできましたがリクエストの日本語化はできませんでした。
この問題を解決するためには、`http.requests`ライブラリのソースコードを改変するか、別のライブラリを使用する必要があります。

## まとめ

今回は`processing`から OpenAI の API を叩くサンプルコードを紹介しました。
`processing`は非常に有用なツールであり、事例こそほとんどないものの生成 AI との通信も行うことが確認できました。
今後は日本語の扱いについても改善していけたらと思います。