テキスト読み上げを行えるようにした

This commit is contained in:
ntki72 2024-11-09 15:52:09 +09:00
parent b968a8f4bc
commit a5149d5ca1
9 changed files with 204 additions and 13 deletions

View File

@ -1,5 +1,7 @@
FROM node:23.1.0-alpine FROM node:23.1.0-alpine
RUN apk add --no-cache ffmpeg
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./

View File

@ -5,9 +5,15 @@ services:
NODE_ENV: development NODE_ENV: development
env_file: env_file:
- config/development.env - config/development.env
environment:
- NODE_ENV=development
volumes: volumes:
- .:/app - .:/app
- /app/node_modules - /app/node_modules
environment:
- NODE_ENV=development
command: ["npm", "run", "dev"] command: ["npm", "run", "dev"]
depends_on:
- voicevox
voicevox:
image: voicevox/voicevox_engine:latest
ports:
- "50021:50021"

View File

@ -6,8 +6,8 @@ services:
NODE_ENV: production NODE_ENV: production
env_file: env_file:
- config/production.env - config/production.env
environment:
- NODE_ENV=production
volumes: volumes:
- .:/app - .:/app
restart: always restart: always
environment:
- NODE_ENV=production

132
package-lock.json generated
View File

@ -10,8 +10,11 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@discordjs/voice": "^0.17.0", "@discordjs/voice": "^0.17.0",
"axios": "^1.7.7",
"discord.js": "^14.16.3", "discord.js": "^14.16.3",
"dotenv": "^16.4.5" "dotenv": "^16.4.5",
"libsodium-wrappers": "^0.7.15",
"uuid": "^11.0.2"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.7" "nodemon": "^3.1.7"
@ -268,6 +271,23 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -337,6 +357,18 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -362,6 +394,15 @@
} }
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/discord-api-types": { "node_modules/discord-api-types": {
"version": "0.37.100", "version": "0.37.100",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.100.tgz", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.100.tgz",
@ -425,6 +466,40 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -516,6 +591,21 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/libsodium": {
"version": "0.7.15",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz",
"integrity": "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==",
"license": "ISC"
},
"node_modules/libsodium-wrappers": {
"version": "0.7.15",
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz",
"integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==",
"license": "ISC",
"dependencies": {
"libsodium": "^0.7.15"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -534,6 +624,27 @@
"integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -632,6 +743,12 @@
} }
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": { "node_modules/pstree.remy": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@ -748,6 +865,19 @@
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz",
"integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",

View File

@ -12,8 +12,11 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@discordjs/voice": "^0.17.0", "@discordjs/voice": "^0.17.0",
"axios": "^1.7.7",
"discord.js": "^14.16.3", "discord.js": "^14.16.3",
"dotenv": "^16.4.5" "dotenv": "^16.4.5",
"libsodium-wrappers": "^0.7.15",
"uuid": "^11.0.2"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.7" "nodemon": "^3.1.7"

View File

@ -4,20 +4,28 @@ const { joinVoiceChannel } = require('@discordjs/voice');
module.exports = { module.exports = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('join') .setName('join')
.setDescription('Join the voice channel you are in'), .setDescription('Join the voice channel you are in and listen to this text channel'),
async execute(interaction) { async execute(interaction) {
const voiceChannel = interaction.member.voice.channel; const voiceChannel = interaction.member.voice.channel;
const textChannel = interaction.channel;
if (!voiceChannel) { if (!voiceChannel) {
return interaction.reply('ボイスチャンネルに接続されていません。'); return interaction.reply('ボイスチャンネルに接続されていません。');
} }
// ボイスチャンネルに参加
joinVoiceChannel({ joinVoiceChannel({
channelId: voiceChannel.id, channelId: voiceChannel.id,
guildId: voiceChannel.guild.id, guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator, adapterCreator: voiceChannel.guild.voiceAdapterCreator,
}); });
await interaction.reply('ボイスチャンネルに接続しました。'); // サーバーごとにチャンネル情報を保存
interaction.client.guildChannels.set(interaction.guild.id, {
textChannelId: textChannel.id,
voiceChannelId: voiceChannel.id
});
await interaction.reply('ボイスチャンネルに接続し、このテキストチャンネルのメッセージを読み上げます。');
}, },
}; };

View File

@ -1,11 +1,27 @@
const { createAudioPlayer, createAudioResource, getVoiceConnection } = require('@discordjs/voice');
const { synthesizeSpeech } = require('../services/tts');
module.exports = { module.exports = {
name: 'messageCreate', name: 'messageCreate',
execute(message) { async execute(message) {
const client = message.client;
// サーバーごとのチャンネル情報を取得
const guildChannels = client.guildChannels.get(message.guild.id);
if (!guildChannels) return;
// 記憶されたテキストチャンネルのメッセージのみ処理
if (message.channel.id !== guildChannels.textChannelId) return;
if (message.author.bot) return; if (message.author.bot) return;
if (message.mentions.has(message.client.user)) { const connection = getVoiceConnection(message.guild.id);
const response = message.content.replace(`<@${message.client.user.id}>`, '').trim(); if (!connection) return;
message.channel.send(response || "メッセージを受け取りました!");
} const audioUrl = await synthesizeSpeech(message.content);
const resource = createAudioResource(audioUrl);
const player = createAudioPlayer();
player.play(resource);
connection.subscribe(player);
}, },
}; };

View File

@ -12,6 +12,9 @@ const client = new Client({
] ]
}); });
// サーバーごとのテキストチャンネルとボイスチャンネルの情報を保持
client.guildChannels = new Map();
// コマンドの読み込み // コマンドの読み込み
client.commands = new Collection(); client.commands = new Collection();
const commands = []; const commands = [];

23
src/services/tts.js Normal file
View File

@ -0,0 +1,23 @@
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
async function synthesizeSpeech(text) {
if (process.env.NODE_ENV === 'development') {
console.log('[development] Synthesizing speech...');
const voicevoxUrl = `http://voicevox:50021/audio_query?text=${encodeURIComponent(text)}&speaker=1`;
const { data } = await axios.post(voicevoxUrl);
const synthesisUrl = `http://voicevox:50021/synthesis?speaker=1`;
const response = await axios.post(synthesisUrl, data, { responseType: 'arraybuffer' });
const filePath = path.join('/tmp', `${uuidv4()}.mp3`);
fs.writeFileSync(filePath, Buffer.from(response.data));
return filePath;
} else {
console.log('[production] Synthesizing speech...');
return path.join(__dirname, 'dummy.mp3'); // 本番用の音声生成処理
}
}
module.exports = { synthesizeSpeech };