はじめに
コンソールの入力からでも相当便利で強力なChatGPTですが、もし音声でレスポンスが返ってくるなら更に愛着が湧くはず。
音声で出力を試みる方法は色々あるが、Discord上で動作するBOTの開発経験があったのでDiscordを選択して挑戦。
構成
- nodejs 16.18.1
- DiscordJS v14
- Docker (voicevox用HTTPサーバに使用)
- ChatGPT(GPT3.5 turbo)
作ったもの
丸番号については以下の通り。
① ローカル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);
参考にさせていただいた情報
この場をお借りして御礼申し上げます。
-
VOICEVOX
ボイスサンプル選んだり、あの手この手考えてる時間が一番楽しかった。 -
VOICEVOX/voicevox_engine (github)
Dockerイメージを使わせていただきました。ありがとうございます。 - @discordjs/voiceを使用して音声を再生する
-
Node.jsからVOICEVOXを使ってみる。
execで愚直にvoicevoxの音声合成をしていた最中、この記事に出会いました。ありがとうございます! -
VoiceConnection stuck in signalling state about a minute after creating it
DiscordJS v14だと、player.play(resource)しても再生がうまく行かない事象が発生しているらしい。
本開発でも”this.configureNetworking();”を強引に突っ込んで解決。修正待ちかなあ。
これからやりたいこと
- Looking Glassを活用して3Dっぽく
- ローカルではなくどこかにデプロイ
- ChatGPTが429吐くとどうしようもないのでエラーのハンドリング
その他
- 当初はスラッシュコマンドでメッセージを送る予定だったが、送信するメッセージ量が多いとDiscordJSがエラー吐くので断念した
- コードもChatGPTに書かせたりしながら、設計->完成までかなり短時間でできたことがとても良かった
- メッセージだけでなく音声でレスポンスがあるとやっぱり違う