👨‍💻

#107 OpenAIのRealtime APIをつかってみる 

に公開

OpenAIのRealtime APIをつかってみる

OpenAIからRealtime API (Beta版)がリリースされたので、実際に触ってみようと思います。

公式ドキュメントに音声の送信方法が記載されていますが、受信方法は記載されていなかったので今回は音声の受信と、受信した音声をDiscordを経由して再生してみようと思います

作成方針

今回は、ユーザがDiscordのチャット欄に「join」の入力すると、BOTがVCに入室して話し始める※。というものを作成したいと思います

OpenAIのRealtimAPIを主軸に触っていこうと思いますので、Discord周りの実装で手抜きがありますがご容赦ください

※BOTを操作する場合はスラッシュコマンドの使用を推奨されていると思いますが、実装しなければいけない量が増えてしまうため省略しました

Discord周りの実装

まずOpenAIのRealtimeAPIを試すためにはアウトプットの手段が必要なので、先にDiscord周りの実装をしていきます

内容としては、DiscordのBOTの起動と、「join」というメッセージを受け取った時にVCに接続する機能を作成しました。

TypeScript
import { Client, GatewayIntentBits, Message } from "discord.js";
import { createAudioPlayer, createAudioResource, DiscordGatewayAdapterCreator, joinVoiceChannel, NoSubscriberBehavior, StreamType } from '@discordjs/voice';
import 'dotenv/config'
import { RealtimeClient } from '@openai/realtime-api-beta';
import { ItemType } from "@openai/realtime-api-beta/dist/lib/client.js";
import { PassThrough } from "stream";
import wav from 'wav';

const discordClient = new Client({
    intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildIntegrations,
        GatewayIntentBits.GuildVoiceStates,
        GatewayIntentBits.MessageContent,
        GatewayIntentBits.GuildMessages
    ]
});

async function init() {
    // Discordへ接続
    await discordClient.login(process.env.DISCORD_TOKEN);
};

discordClient.on('messageCreate', (message) => {
    console.log(message.content);
    const channel = message.channel;
    if (!channel.isVoiceBased()) {
        return;
    }

    if (message.content != 'join') {
        return;
    }

    //VCのコネクション
    const connection = joinVoiceChannel({
        adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator,
        channelId: channel.id,
        guildId: channel.guild.id,

    });
});

OpenAI Realtime API周りの実装

init関数にRealtimeAPIの接続処理を追加します

TypeScript
async function init() {
    // Realtime APIへ接続
    await openaiClient.connect();

    // Discordへ接続
    await discordClient.login(process.env.DISCORD_TOKEN);
};

Discordのメッセージ受信部分にRealtime APIから受け取るデータを再生するための処理を追加します

一番下のopenaiClient.sendUserMessageContent()以外はDiscordで音声を流すためのコードで、ストリームの内容をVCに再生するように設定しています。

  • openaiClient.sendUserMessageContent(): この関数はRealtimeAPI側にメッセージの内容を渡すための関数です
TypeScript
discordClient.on('messageCreate', (message) => {

    //......

    // プレイヤーを作成してVCのコネクションと紐づける
    const player = createAudioPlayer({
        behaviors: {
            // 聞いている人がいなくても音声を中継してくれるように設定
            noSubscriber: NoSubscriberBehavior.Play,
        },
    });
    connection.subscribe(player);

    let stream = new PassThrough();
    stream.on("error", (err) => {
        console.log(err)
    })
    wavWriter.pipe(stream);
    // 音声リソースを作成
    const resource = createAudioResource(stream, {
        inputType: StreamType.Arbitrary,
    },);

    player.play(resource);

    openaiClient.sendUserMessageContent([{ type: 'input_text', text: `こんにちは!自己紹介してください` }]);
});

Realtime APIの受信部分を作成する

先ほどのコードに以下のコードを追記します。

openaiClient.on('conversation.updated')で随時RealtimeAPIからの応答を受信して、ストリームに音声データを流します

  • wavWriter: PCM形式の音声データをWAV形式に変換します

    • RealtimeAPIから返却される音声データはサンプルレートが24kHz, ビット深度は16bit, モノラル形式なのでそれに合わせて設定します。
  • openaiClient.on(conversation.updated, () => {}): ここがRealtimeAPIの受信部分です。分割されたオーディオのデータや文字列が送られてくるので、オーディオを受信した場合のみ処理を行っています。

    • ※実装時に詰まったポイントとして、audioBuffer = audio.bufferとするとうまく再生できませんでした。調査してみたところ、audioはInt16Arrayの配列であり、ArrayBufferに変換するときにバイト列は処理系に依存するようです。

      今回は、DetaViewを挟むことで解決しましたが依然としてエンディアンの問題が残っているので実装の際は注意が必要です



      参考情報: 音声の形式はPCM形式でリトルエンディアン。主流な処理系はx86系かARM系でどちらもリトルエンディアン
TypeScript
const openaiClient = new RealtimeClient({ apiKey: process.env.OPEN_AI_TOKEN });

const wavWriter = new wav.Writer({
    sampleRate: 24000,
    channels: 1,
    bitDepth: 16,
});

openaiClient.on('conversation.updated', async (event: any) => {
    const audio = event.delta?.audio;
    if (audio && event.item.role && event.item.role == 'assistant') {
        console.log(event);
        try {
            const view = new DataView(audio.buffer);
            const audioBuffer = Buffer.from(view.buffer);

            wavWriter.write(audioBuffer);
        } catch (error) {
            console.error(error);
        }
    }
});

これで実装完了です!

まとめ

今回は新しく発表されたRealtimeAPIに触ってみました。

音声の受信時のフォーマットが公式ドキュメントにあまり詳しく記載されていなかったのですが、今回の記事の内容でDiscordへ音声を送信するところまで完成しました。

今回の記事では一方向性的なものになってしまいましたが、音声の送信も組み合わせることでリアルタイムに会話することも可能だと思います。ぜひ挑戦してみてください

Discussion