7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DiscordでChatGPTのレスポンスを喋らせてみた

Last updated at Posted at 2023-03-16

はじめに

コンソールの入力からでも相当便利で強力なChatGPTですが、もし音声でレスポンスが返ってくるなら更に愛着が湧くはず。
音声で出力を試みる方法は色々あるが、Discord上で動作するBOTの開発経験があったのでDiscordを選択して挑戦。

構成

  • nodejs 16.18.1
  • DiscordJS v14
  • Docker (voicevox用HTTPサーバに使用)
  • ChatGPT(GPT3.5 turbo)

作ったもの

以下のような構成で開発。
system.png

丸番号については以下の通り。
① ローカルPC上で、作成したJSおよびvoicevoxの構成が入ったDockerを起動しておく
② Discordで話しかけたいメッセージを指定の形式で送信する
③ JSが指定の形式で送信されたメッセージを拾う
④ ChatGPTへリクエスト送信。レスポンスを受信する
⑤ ChatGPTのレスポンスで受け取った文章を音声化するためにvoicevoxのAPIへリクエスト
⑥ ②と同じサーバのボイスチャンネル上でボイス再生

コード

  • DiscordJSやVoicevoxの使用方法については省略します
  • 各種キーは各々取得して環境変数なりなんなりで補完してください
  • 余計な宣言が入ってると思われるので各自で加工してください
  • Discord上で、「!talk <メッセージ内容>」を受け取った想定の処理です
index.js
const TOKEN = "<Discord BOT のトークンを入れる>";
const CLIENT_ID = "<Discord BOT のクライアントIDを入れる>";
const { 
  Discord,
  REST, 
  Routes, 
  Client, 
  Partials,
  GatewayIntentBits,
  ModalBuilder, 
  ActionRowBuilder,
  TextInputStyle,
  TextInputBuilder,
  VoiceChannel
} = require('discord.js');
const { 
  joinVoiceChannel,
  entersState,
  VoiceConnectionStatus,
  createAudioResource,
  StreamType,
  createAudioPlayer,
  AudioPlayerStatus,
  NoSubscriberBehavior,
  generateDependencyReport
} = require("@discordjs/voice");
console.log(generateDependencyReport());
const client = new Client({
  restRequestTimeout: 60000, 
  intents: [
    GatewayIntentBits.Guilds, 
    GatewayIntentBits.GuildVoiceStates, 
    GatewayIntentBits.MessageContent,
    GatewayIntentBits.GuildMessages
  ], 
  partials: [Partials.Channel]});
const { exec } = require('child_process');
const { default: axios } = require("axios");
const fs = require("fs");

const rest = new REST({ version: '10' }).setToken(TOKEN);
const rpc = axios.create({ baseURL: "http://127.0.0.1:50021", proxy: false });

(async () => {
  try {
    console.log('Started refreshing applications.');
  } catch (error) {
    console.error(error);
  }
})();

//スタート
client.on('ready', () => {
  console.log(`Logged in as ${client.user.tag}!`);
});

//返答
client.on('messageCreate', async message => {
    if (message.author.bot) {
        return;
    }
    let restext = "";
    if (message.content.includes('!talk')) {
        //受信メッセージ加工("!talk "を消す)
        let text = message.content.slice(6);
        console.log(text);
        await fetchResponse(text).then(
        response_text => {
          restext = response_text;
          exec("rm audio.wav");
          exec("rm text.txt");
          return new Promise((resolve, reject) => {
            exec('echo "' + response_text + '" > text.txt', (err, stdout, stderr) => {
              if (err) {
                console.error(err);
                reject(err);
              } else {
                console.log("text output OK");
                resolve();
              }
            });
          });
        }).then(() => {
          //voicevox関係の処理
          console.log("genaudio");
          return genAudio(restext,"audio.wav");
        }).then(() => {
          //メッセージ送信
          console.log("then");
          const channel = message.member.voice.channel;
          if (!channel) return message.reply('ボイスチャンネルに接続していません。');
          playAudio(channel);
          message.reply(restext);
      });
    }
});

async function fetchResponse(text) {
  //ChatGPTの処理
  const base_url = "<ChatGPT APIのURL>";
  const api_key = "<ChatGPTのAPIキー>";

  const data = {
    model: "gpt-3.5-turbo",
    messages: [ { role: "system", content: "<BOTの設定などを記載するとよい>"},
                { role: "user", content: text }],
    top_p: 0.8
  };

  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${api_key}`
  };

  const response_text = "";
  try {
    const response = await axios.post(base_url, data, { headers });
    const result = response.data;
    const response_text = result.choices[0].message.content;
    return response_text;
  } catch (error) {
    console.error(error);
    const response_text = "なんかエラー!";
  }
}

async function genAudio(text, filepath) {
  /* まずtextを渡してsynthesis宛のパラメータを生成する、textはURLに付けるのでencodeURIで変換しておく。*/
  const audio_query = await rpc.post('audio_query?text=' + encodeURI(text) + '&speaker=30');

  //audio_queryで受け取った結果がaudio_query.dataに入っている。
  //このデータをメソッド:synthesisに渡すことで音声データを作ってもらえる
  //audio_query.dataはObjectで、synthesisに送る為にはstringで送る必要があるのでJSON.stringifyでstringに変換する
  const synthesis = await rpc.post("synthesis?speaker=30", JSON.stringify(audio_query.data), {
    responseType: 'arraybuffer',
    headers: {
      "accept": "audio/wav",
      "Content-Type": "application/json"
    }
  });

  //受け取った後、Bufferに変換して書き出す
  fs.writeFileSync(filepath, new Buffer.from(synthesis.data), 'binary');
}

async function playAudio(channel) {
  console.log("Audio play!");
  const connection = joinVoiceChannel({
    channelId: channel.id,
    guildId: channel.guild.id,
    adapterCreator: channel.guild.voiceAdapterCreator,
  });
  const resource = createAudioResource("audio.wav",
    {
      inputType: StreamType.Arbitrary,
    });
  const player = createAudioPlayer({
    behaviors: {
      noSubscriber: NoSubscriberBehavior.Pause,
    },
  });
  player.play(resource);
  connection.subscribe(player);
  //音声発信が終わってもボイチャに居続けたいのであえて処理無し
  //connection.destroy();  
}

client.login(TOKEN);

参考にさせていただいた情報

この場をお借りして御礼申し上げます。

これからやりたいこと

  • Looking Glassを活用して3Dっぽく
  • ローカルではなくどこかにデプロイ
  • ChatGPTが429吐くとどうしようもないのでエラーのハンドリング

その他

  • 当初はスラッシュコマンドでメッセージを送る予定だったが、送信するメッセージ量が多いとDiscordJSがエラー吐くので断念した
  • コードもChatGPTに書かせたりしながら、設計->完成までかなり短時間でできたことがとても良かった
  • メッセージだけでなく音声でレスポンスがあるとやっぱり違う
7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?