🎨

DALL·E 3による画像生成をFunction Calling経由で実行する(ChatGPT API)

2023/12/12に公開

はじめに

DALL・E 3による画像生成をFunction Calling経由で実行するメモです。

環境

  • node 18.14.2
  • typescript 5.0.2
  • openai 4.16.1

OpenAIクライアントの初期化

openai.ts
import OpenAI from "openai";

export const openAi = new OpenAI({
  apiKey: process.env["OPEN_AI_API_KEY"] ?? "",
});

OpenAIクライアントを初期化して export しておきます。

DALL·E 3で画像生成

DALL·E 3で画像を生成する関数を書きます。

genImageByDalle.ts
// Function名として定数を定義
const GEN_IMAGE_BY_DALLE = "genImageByDalle" as const;

export const genImageByDalle = async ({
  genImagePrompt,
  responseMessage,
  messages,
  size,
}: {
  // 画像生成プロンプト
  genImagePrompt: string;
  // 画像生成後にユーザに返信するメッセージ
  responseMessage: string;
  // ユーザのメッセージ(必要なら過去の会話も含める)
  messages: any[];
  // 生成する画像のサイズ
  size: string;
}): Promise<{ file?: Buffer; responseMessage: string }> => {
  let generated: ImagesResponse | undefined;
  try {
    generated = await openAi.images.generate({
      model: "dall-e-3",
      prompt: genImagePrompt,
      size: size as any,
      quality: "standard",
      n: 1,
    });

    // 画像はURLで与えられるので、fetchでダウンロードしてBufferに変換する
    // @see https://qiita.com/miyabisun/items/6eb693d04c6eec838fd6
    const url = generated?.data[0]!.url!;
    const res = await fetch(url);
    const arrayBuffer = await res.arrayBuffer();
    const file = Buffer.from(arrayBuffer);

    return {
      file,
      responseMessage,
    };
  } catch (e: any) {
    // エラーハンドリング
    // OpenAI側の安全システムにより画像生成指示が公序良俗に反すると判断された場合などエラーが発生するので、その内容をユーザにわかりやすく伝える
    const completion = await openAi.chat.completions.create({
      model: "gpt-4",
      messages: [
        ...messages,
        {
          role: "function",
          content: `画像生成にあたり「${e.message}」のエラーが発生しました。ユーザにわかりやすくエラーを説明してください。`,
          name: GEN_IMAGE_BY_DALLE,
        },
      ],
    });
    return {
      responseMessage: completion.choices[0]?.message.content ?? "",
    };
  }
};

いまのところ、画像はOpenAIよりURLで与えられる仕様のため、何かと取り回しを良くするにはダウンロードしてあげる必要があります。Node.jsなので fetch でダウンロードして Buffer に変換しています。

あと、エラーハンドリングにひと工夫を入れています。ユーザにあまりそういうつもりがない場合でも、OpenAI側の安全システムにより画像生成指示が不適切(公序良俗に反する?)と判断される場合があります。その場合、一律に「予期せぬエラー」とか「リクエストエラー」としてしまうとユーザは何がだめだったのかわからないままになってしまいます。そのため、GPT-4にエラーを渡してユーザにわかりやすく伝えるようにしています。

ただ、セキュリティの観点から一般的にサーバ内部で発生したエラーをユーザに返すべきではないので、

  • OpenAI安全システムに抵触した場合だけユーザに説明させる
  • エラーメッセージの全てではなく一部だけ抜粋してGPT-4に渡す
  • OpenAIのエラーコードと固定メッセージの対照表を定義しておいて決まった説明だけを返す(GPTを介在させない)

など、Productionとして稼働させる場合にはもうひとひねり入れると良いと思います。

Function Callingに組み込む

chat.ts
export const chat = async (
  userMessage: string,
  // 画像の出力処理を引数で注入する
  outputImageCallback: (file: Buffer, fileName: string) => Promise<void>
) => {
  const completion = await openAi.chat.completions.create({
    model: "gpt-4",
    messages: [
      {
        role: "user",
        content: userMessage,
      },
    ],
    functions: [
      {
        name: GEN_IMAGE_BY_DALLE,
        description: "DALL・E3を使って画像を生成する",
        parameters: {
          type: "object",
          properties: {
            genImagePrompt: {
              type: "string",
              description: "画像生成するためのプロンプト",
            },
            fileName: {
              type: "string",
              description: "生成するPNG画像のファイル名",
            },
            size: {
              type: "string",
              // 256と512は未対応エラーになる
              enum: ["1024x1024", "1792x1024", "1024x1792"],
              description:
                "生成する画像のサイズ。ユーザの要望に最も近いものを選択する。特に要望がない場合は「1024x1024」とする",
            },
            responseMessage: {
              type: "string",
              description: "画像生成後にユーザに返信するメッセージ",
            },
          },
          required: ["genImagePrompt", "fileName", "size", "responseMessage"],
        },
      },
    ],
    function_call: "auto",
  });

  if (completion.choices[0]!.finish_reason !== "function_call") {
    // Function Callingでない場合(通常の会話)
    // 早期リターンして終了
    return completion.choices[0]?.message.content ?? "";
  }

  // Function Callingの場合
  const message = completion.choices[0]!.message;
  if (message.function_call?.name === GEN_IMAGE_BY_DALLE) {
    // DALL·E 3で画像生成する場合
    const args = JSON.parse(message.function_call!.arguments);
    const { file, responseMessage } = await genImageByDalle({
      genImagePrompt: args.genImagePrompt,
      responseMessage: args.responseMessage,
      messages: [
        {
          role: "user",
          content: userMessage,
        },
      ],
      size: args.size,
    });
    if (file != null) {
      await outputImageCallback(file, args.fileName);
    }
    return responseMessage;
  } else {
    // ここには来ない想定
    throw new Error("不正なタイプのFunctionCallingです。");
  }
};

画像を出力する処理は outputImageCallback 関数として注入するようにしています(DI)。関数内にベタ書きしても動作はするのですが、SlackボットやLINEボットとして組み込む場合など、いちいちデプロイしないと動作確認ができなくなってしまいます。

DIしておくと、ローカルで動作確認する際にファイルや標準出力に書き出すようにしやすいです。

実行

ではローカルへのファイル書き出しで実行します。

main.ts
const main = async () => {
  const userMessage = "ゴリラの画像を生成してください。";
  const chatGptMessage = await chat(userMessage, async (file, fileName) => {
    fs.writeFileSync(fileName, file);
  });
  console.log(chatGptMessage);
}

main();
実行
node -r esbuild-register main.ts

実行すると、生成されたPNG画像が手元に出力されます(ファイル名はChatGPTが考えてくれます)。

Slack Botに組み込むデモ

上記のFunction Callingの仕組みをSlackボットに入れ込むとこんな感じになります。

Slack Botでやってみた

通常のテキストの会話を振った場合にはテキスト返答をし、画像生成の指示を出した場合にはFunction Callingが発動して画像を返してくれるようになります。

おわりに

以上、参考になれば幸いです。

参考

Discussion