ひとまず動作するようになった
This commit is contained in:
parent
1022ab1462
commit
af131ee53d
203
bot.js
203
bot.js
@ -1,10 +1,11 @@
|
|||||||
const sodium = require('libsodium-wrappers');
|
const sodium = require('libsodium-wrappers');
|
||||||
const { Client, GatewayIntentBits } = require('discord.js');
|
const { Client, GatewayIntentBits } = require('discord.js');
|
||||||
const { joinVoiceChannel, getVoiceConnection, VoiceConnectionStatus, EndBehaviorType } = require('@discordjs/voice');
|
const { joinVoiceChannel, getVoiceConnection, VoiceConnectionStatus, EndBehaviorType, createAudioPlayer, createAudioResource, AudioPlayerStatus } = require('@discordjs/voice');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { OpusEncoder } = require('@discordjs/opus');
|
const { OpusEncoder } = require('@discordjs/opus');
|
||||||
const { exec } = require('child_process');
|
const { spawn } = require('child_process'); // execからspawnに変更
|
||||||
const OpenAI = require('openai');
|
const OpenAI = require('openai');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
// OpenAI APIキーの確認
|
// OpenAI APIキーの確認
|
||||||
const apiKey = process.env.OPENAI_API_KEY || 'your-openai-api-key';
|
const apiKey = process.env.OPENAI_API_KEY || 'your-openai-api-key';
|
||||||
@ -13,10 +14,7 @@ if (!apiKey || apiKey === 'your-openai-api-key') {
|
|||||||
process.exit(1); // エラーコード1で終了
|
process.exit(1); // エラーコード1で終了
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAI APIクライアントの作成
|
const openai = new OpenAI({ apiKey });
|
||||||
const openai = new OpenAI({
|
|
||||||
apiKey: apiKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
sodium.ready.then(() => {
|
sodium.ready.then(() => {
|
||||||
console.log('Sodium library loaded!');
|
console.log('Sodium library loaded!');
|
||||||
@ -36,7 +34,7 @@ client.once('ready', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.on('messageCreate', async message => {
|
client.on('messageCreate', async message => {
|
||||||
if (message.content === '!join') {
|
if (message.content === '!cs_join') {
|
||||||
if (message.member.voice.channel) {
|
if (message.member.voice.channel) {
|
||||||
const connection = joinVoiceChannel({
|
const connection = joinVoiceChannel({
|
||||||
channelId: message.member.voice.channel.id,
|
channelId: message.member.voice.channel.id,
|
||||||
@ -55,10 +53,17 @@ client.on('messageCreate', async message => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.content === '!leave') {
|
if (message.content === '!cs_leave') {
|
||||||
const connection = getVoiceConnection(message.guild.id);
|
const connection = getVoiceConnection(message.guild.id);
|
||||||
if (connection) {
|
if (connection) {
|
||||||
connection.destroy();
|
connection.destroy(); // ボイスチャンネルから切断
|
||||||
|
|
||||||
|
// 進行中のFFmpegなどのプロセスを強制終了
|
||||||
|
if (ffmpeg) {
|
||||||
|
ffmpeg.kill('SIGKILL');
|
||||||
|
console.log('FFmpeg process killed on leave');
|
||||||
|
}
|
||||||
|
|
||||||
message.reply('ボイスチャンネルを離れました。');
|
message.reply('ボイスチャンネルを離れました。');
|
||||||
} else {
|
} else {
|
||||||
message.reply('ボイスチャンネルに接続していません。');
|
message.reply('ボイスチャンネルに接続していません。');
|
||||||
@ -96,7 +101,6 @@ function startRecording(connection, voiceChannel) {
|
|||||||
try {
|
try {
|
||||||
const pcmData = opusDecoder.decode(chunk);
|
const pcmData = opusDecoder.decode(chunk);
|
||||||
writableStream.write(pcmData);
|
writableStream.write(pcmData);
|
||||||
console.log(`Received ${chunk.length} bytes of audio data from user ${user.id}`);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error decoding audio chunk:', err);
|
console.error('Error decoding audio chunk:', err);
|
||||||
}
|
}
|
||||||
@ -125,7 +129,7 @@ function startRecording(connection, voiceChannel) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stats.size > 0) {
|
if (stats.size > 0) {
|
||||||
convertToWavAndTranscribe(filePath); // バックグラウンドで変換と文字起こしを実行
|
convertToWavAndTranscribe(filePath, connection); // バックグラウンドで変換と文字起こしを実行
|
||||||
} else {
|
} else {
|
||||||
console.log(`File ${filePath} is empty, skipping conversion.`);
|
console.log(`File ${filePath} is empty, skipping conversion.`);
|
||||||
fs.unlinkSync(filePath); // 空のファイルを削除
|
fs.unlinkSync(filePath); // 空のファイルを削除
|
||||||
@ -135,43 +139,56 @@ function startRecording(connection, voiceChannel) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertToWavAndTranscribe(pcmPath) {
|
// PCMファイルをWAVファイルに変換し、文字起こしを実行
|
||||||
|
function convertToWavAndTranscribe(pcmPath, connection) {
|
||||||
const wavPath = pcmPath.replace('.pcm', '.wav');
|
const wavPath = pcmPath.replace('.pcm', '.wav');
|
||||||
console.log(`Converting ${pcmPath} to ${wavPath} and transcribing in the background`);
|
console.log(`Converting ${pcmPath} to ${wavPath} and transcribing in the background`);
|
||||||
|
|
||||||
const ffmpeg = exec(`ffmpeg -f s16le -ar 48000 -ac 2 -i ${pcmPath} ${wavPath}`, (error, stdout, stderr) => {
|
const ffmpeg = spawn('ffmpeg', ['-f', 's16le', '-ar', '48000', '-ac', '2', '-i', pcmPath, wavPath]);
|
||||||
if (error) {
|
|
||||||
console.error(`Error during conversion: ${error.message}`);
|
// プロセスがフリーズした場合、タイムアウトして強制終了する
|
||||||
return;
|
const ffmpegTimeout = setTimeout(() => {
|
||||||
}
|
ffmpeg.kill('SIGKILL'); // 強制終了
|
||||||
if (stderr) {
|
console.error('FFmpeg process timed out and was killed');
|
||||||
console.error(`FFmpeg stderr: ${stderr}`);
|
}, 10000); // 10秒でタイムアウト
|
||||||
return;
|
|
||||||
}
|
ffmpeg.stdout.on('data', (data) => {
|
||||||
|
console.log(`FFmpeg stdout: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpeg.stderr.on('data', (data) => {
|
||||||
|
console.error(`FFmpeg stderr: ${data}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
ffmpeg.on('close', (code) => {
|
ffmpeg.on('close', (code) => {
|
||||||
|
clearTimeout(ffmpegTimeout); // 正常に終了したらタイムアウトをクリア
|
||||||
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
console.log(`Successfully converted ${pcmPath} to ${wavPath}`);
|
console.log(`Successfully converted ${pcmPath} to ${wavPath}`);
|
||||||
fs.unlinkSync(pcmPath); // PCMファイルを削除
|
fs.unlinkSync(pcmPath); // PCMファイルを削除
|
||||||
|
|
||||||
// Whisper APIを使って文字起こしを実行
|
// Whisper APIを使って文字起こしを実行
|
||||||
transcribeAudio(wavPath);
|
transcribeAudio(wavPath, connection);
|
||||||
} else {
|
} else {
|
||||||
console.error(`FFmpeg exited with code ${code}, conversion failed.`);
|
console.error(`FFmpeg exited with code ${code}, conversion failed.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ffmpeg.on('error', (err) => {
|
||||||
|
console.error('FFmpeg process error:', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whisper APIを使って音声ファイルを文字起こしする関数
|
// Whisper APIを使って音声ファイルを文字起こしする関数
|
||||||
async function transcribeAudio(filePath) {
|
async function transcribeAudio(filePath, connection) {
|
||||||
try {
|
try {
|
||||||
console.log(`Transcribing file: ${filePath}`);
|
console.log(`Transcribing file: ${filePath}`);
|
||||||
|
|
||||||
const response = await openai.audio.transcriptions.create({
|
const response = await openai.audio.transcriptions.create({
|
||||||
file: fs.createReadStream(filePath),
|
file: fs.createReadStream(filePath),
|
||||||
model: 'whisper-1',
|
model: 'whisper-1',
|
||||||
response_format: 'json' // レスポンス形式をJSONに指定
|
response_format: 'json', // レスポンス形式をJSONに指定
|
||||||
|
language: 'ja' // 言語を日本語に固定
|
||||||
});
|
});
|
||||||
|
|
||||||
// 文字起こし結果を取得
|
// 文字起こし結果を取得
|
||||||
@ -179,13 +196,39 @@ async function transcribeAudio(filePath) {
|
|||||||
const transcription = response.text;
|
const transcription = response.text;
|
||||||
console.log('Transcription:', transcription);
|
console.log('Transcription:', transcription);
|
||||||
|
|
||||||
|
// ChatGPTに反応すべきか判断させる
|
||||||
|
const shouldReact = await shouldReactToTranscription(transcription);
|
||||||
|
if (!shouldReact) {
|
||||||
|
console.log('ChatGPT decided not to react to this transcription.');
|
||||||
|
// 文字起こし完了後、WAVファイルを削除
|
||||||
|
fs.unlink(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`Error deleting recording file: ${err}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Deleted recording file: ${filePath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ChatGPTを使用して返答を生成
|
// ChatGPTを使用して返答を生成
|
||||||
const reply = await generateChatResponse(transcription);
|
const reply = await generateChatResponse(transcription);
|
||||||
console.log('ChatGPT response:', reply);
|
console.log('ChatGPT response:', reply);
|
||||||
|
|
||||||
// 必要に応じて、返信内容をメッセージとして送信
|
// 合成音声を生成して再生
|
||||||
// 例えば、メッセージを元のチャンネルに送信するなど
|
const voiceFilePath = await generateVoiceFromText(reply);
|
||||||
// message.channel.send(reply); // メッセージを送信する場合
|
if (voiceFilePath) {
|
||||||
|
playVoiceInChannel(connection, voiceFilePath); // 音声を再生
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文字起こし完了後、WAVファイルを削除
|
||||||
|
fs.unlink(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`Error deleting recording file: ${err}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Deleted recording file: ${filePath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Error: Transcription response is missing "text" field.');
|
console.error('Error: Transcription response is missing "text" field.');
|
||||||
@ -195,19 +238,47 @@ async function transcribeAudio(filePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChatGPTに反応すべきかどうかを判断させる関数
|
||||||
|
async function shouldReactToTranscription(transcription) {
|
||||||
|
try {
|
||||||
|
const systemPrompt = 'あなたは「セカイ」というキャラクターです。次のユーザーの発言に対して反応すべきかどうかを判断してください。反応すべき場合は「はい」、反応しない場合は「いいえ」と答えてください。';
|
||||||
|
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
model: 'gpt-4',
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: transcription }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.choices && response.choices.length > 0) {
|
||||||
|
const result = response.choices[0].message.content.trim();
|
||||||
|
return result.toLowerCase() === 'はい';
|
||||||
|
} else {
|
||||||
|
console.error('Error: ChatGPT response is missing choices.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during shouldReactToTranscription generation:', error.response ? error.response.data : error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ChatGPTに応答を生成させる関数
|
// ChatGPTに応答を生成させる関数
|
||||||
async function generateChatResponse(transcription) {
|
async function generateChatResponse(transcription) {
|
||||||
try {
|
try {
|
||||||
|
const systemPrompt = 'あなたはこれから「セカイ」という名前の幼い少女を演じてユーザーとの会話を楽しんでください。回答メッセージは最大でも200字になるようにしてください。';
|
||||||
|
|
||||||
const chatResponse = await openai.chat.completions.create({
|
const chatResponse = await openai.chat.completions.create({
|
||||||
model: 'gpt-3.5-turbo', // ChatGPTのモデルを指定
|
model: 'gpt-4',
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: transcription }
|
{ role: 'user', content: transcription }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (chatResponse && chatResponse.choices && chatResponse.choices.length > 0) {
|
if (chatResponse && chatResponse.choices && chatResponse.choices.length > 0) {
|
||||||
return chatResponse.choices[0].message.content; // 応答内容を返す
|
return chatResponse.choices[0].message.content;
|
||||||
} else {
|
} else {
|
||||||
console.error('Error: ChatGPT response is missing choices.');
|
console.error('Error: ChatGPT response is missing choices.');
|
||||||
return null;
|
return null;
|
||||||
@ -218,4 +289,76 @@ async function generateChatResponse(transcription) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChatGPTの返答を合成音声に変換する関数
|
||||||
|
async function generateVoiceFromText(text, retryCount = 10) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://192.168.100.252:5000/generate_voice', {
|
||||||
|
text: text,
|
||||||
|
narrator: "SEKAI",
|
||||||
|
emotion_happy: "80",
|
||||||
|
emotion_sad: "20",
|
||||||
|
emotion_angry: "10",
|
||||||
|
emotion_fun: "50",
|
||||||
|
pitch: "1"
|
||||||
|
}, {
|
||||||
|
responseType: 'arraybuffer' // 音声ファイルをバイナリデータとして取得
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputFilePath = `./voices/output-${Date.now()}.wav`;
|
||||||
|
await fs.promises.writeFile(outputFilePath, response.data); // 非同期でファイルを書き込み
|
||||||
|
console.log(`Voice file saved to ${outputFilePath}`);
|
||||||
|
return outputFilePath; // 音声ファイルのパスを返す
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating voice:', error.message);
|
||||||
|
|
||||||
|
// リトライ処理
|
||||||
|
if (retryCount > 0) {
|
||||||
|
console.log(`Retrying to generate the voice... (${retryCount} retries left)`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000)); // 1秒待機
|
||||||
|
return generateVoiceFromText(text, retryCount - 1); // リトライ
|
||||||
|
} else {
|
||||||
|
console.error('Max retries reached. Could not generate the voice.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ボイスチャンネルで音声を再生する関数
|
||||||
|
function playVoiceInChannel(connection, filePath, retryCount = 3) {
|
||||||
|
const player = createAudioPlayer();
|
||||||
|
const resource = createAudioResource(filePath);
|
||||||
|
|
||||||
|
player.play(resource);
|
||||||
|
connection.subscribe(player);
|
||||||
|
|
||||||
|
player.on(AudioPlayerStatus.Idle, () => {
|
||||||
|
console.log('Finished playing voice.');
|
||||||
|
|
||||||
|
// 再生が終了した後にWAVファイルを削除
|
||||||
|
setTimeout(() => {
|
||||||
|
fs.unlink(filePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`Error deleting file: ${err}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Deleted file: ${filePath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1000); // 1秒の待機後にファイル削除
|
||||||
|
});
|
||||||
|
|
||||||
|
player.on('error', error => {
|
||||||
|
console.error('Error playing audio:', error.message);
|
||||||
|
|
||||||
|
// リトライ処理
|
||||||
|
if (retryCount > 0) {
|
||||||
|
console.log(`Retrying to play the voice... (${retryCount} retries left)`);
|
||||||
|
setTimeout(() => {
|
||||||
|
playVoiceInChannel(connection, filePath, retryCount - 1);
|
||||||
|
}, 1000); // 1秒待機してリトライ
|
||||||
|
} else {
|
||||||
|
console.error('Max retries reached. Could not play the audio.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
client.login(process.env.BOT_TOKEN);
|
client.login(process.env.BOT_TOKEN);
|
||||||
|
@ -4,7 +4,8 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
container_name: discord-voice-bot
|
container_name: discord-voice-bot
|
||||||
environment:
|
environment:
|
||||||
- BOT_TOKEN=MTI0MDY1MDc3NTE2MDY4NDU1NA.Gl_Z2X.SBWCjk3OzIViNbSXocoPBt_817BZQ12fONLVSY
|
- BOT_TOKEN=MTI4MjM0OTkwMjM3ODMwNzY2Nw.G4AEby.jtFyPkoNbWH3OHPLNzbo0-QZ1odddtu-r9bzdo
|
||||||
- OPENAI_API_KEY=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
|
- OPENAI_API_KEY=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
|
||||||
volumes:
|
volumes:
|
||||||
- ./recordings:/usr/src/app/recordings
|
- ./recordings:/usr/src/app/recordings
|
||||||
|
- ./voices:/usr/src/app/voices
|
||||||
|
Loading…
Reference in New Issue
Block a user