AWS Pollyでメッセージを読み上げてくれるDiscord Botを作る

5 min読了の目安(約4700字TECH技術記事

Discordでボイスチャットをしながらゲームをしているときに、聞き専の人が送ったメッセージを読み上げてくれるShovelというBotがあるのですが、最近ユーザー数が増えすぎたのか使えないことが多々あったのでAWS PollyとDiscord.jsを使って自前で作ってみました。

最初Azureで作ろうとしてだいぶ手こずりましたがPollyにすると1時間程度ですぐにできました。

AWS Polly

AWSのTTS(Text to speech)サービスです。安く、簡単に高品質な音声を生成できます。
今回は簡単に作ることを重視したかったのでJavaScriptで作りました。

{
    "accessKeyId": "",
    "secretAccessKey": "",
    "region": "us-east-1"
}

config.jsonファイルに上のようにIAMユーザーのアクセスキーとregionを書き、以下のように使います。下のコードでは生成した音声をファイルに保存しています。

var AWS = require("aws-sdk");
AWS.config.loadFromPath("./config.json");

var polly = new AWS.Polly();

polly.synthesizeSpeech(
            {
                OutputFormat: "mp3",
                Text: "読み上げたい文章",
                VoiceId: "Mizuki",
                SampleRate: "22050",
                TextType: "text",
            },
            (err, data) => {
                if (err) console.log(err, err.stack);
                else {
                        fs.writeFileSync(
                            `polly.mp3`,
                            data.AudioStream
                        );
                    
                }
            }
        );

Discord.js

Discord.jsはJavaScriptでDiscord Botを作るためのライブラリです。こちらもとても簡単に使うことができます。

const Discord = require('discord.js');
const client = new Discord.Client();

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

client.on('message', msg => {
  if (msg.content === 'ping') {
    msg.reply('pong');
  }
});

client.login('token');

トークンの取得などのやり方はたくさん記事があると思うので触れません。

躓いたところ

Discord.jsのAudioDispatcherが一つしか存在できず、同時に複数メッセージが送信されると先に送られたメッセージの読み上げが途中でとまってしまうのでキューを用意して一つずつ再生するというところに時間がかかりました。

できたもの

VCに入った後、;sと送信するとBotがVCに参加してきます。任意のチャンネルにメッセージを送信すると勝手に読み上げてくれます。

自分で使う用なので複数サーバーでの使用などは考えていません。
JS初心者なのでおかしな部分等あればコメントいただけると幸いです。

var AWS = require("aws-sdk");
AWS.config.loadFromPath("./config.json");
var fs = require("fs");
const Discord = require("discord.js");
const client = new Discord.Client();

let queue = []
let isPlaying = false
prefix = ";"

function isOwner(id) {
    const owners = ["自分のdiscordのユーザーID"]
    return owners.includes(id)
}

function addAudioToQueue(path, voiceChannel) {
    queue.push(
        { path: path, voiceChannel: voiceChannel }
    );
}

function playAudio() {
    if (queue.length >= 1 && !isPlaying ) {
        queue[0].voiceChannel.join().then(connection => {
            const dispatcher = connection.play(queue[0].path)
            dispatcher.on('finish', () => {
                queue.shift()
                playAudio()
                isPlaying = true
            })
        })
    } else {
        isPlaying = false
    }
}

client.on("ready", () => {
    console.log(`Logged in as ${client.user.tag}!`);
});

client.on("message", async (message) => {
    const authorChannelId = message.member.voice.channel.id
    const connection = message.client.voice.connections.find(connection => connection.channel.id === authorChannelId)

    if (message.user == message.client.user || authorChannelId == null) {
        return;
    }

    if (message.content.startsWith(prefix)) {
        const input = message.content.replace(prefix, "").split(" ")
        const command = input[0]
        const args = input.slice(1)

        if (command === "s") {
            await message.member.voice.channel.join()
        } else if (command === "shutdown" && isOwner(message.member.id)) {
            message.client.voice.connections.each(connection => {
                connection.disconnect()
            })
            process.exit()
        }
    } else if (connection != null) {

        var polly = new AWS.Polly();

        polly.synthesizeSpeech(
            {
                OutputFormat: "mp3",
                Text: message.content,
                VoiceId: "Mizuki",
                SampleRate: "22050",
                TextType: "text",
            },
            (err, data) => {
                if (err) console.log(err, err.stack);
                else {

                    if (connection != null) {
                        fs.writeFileSync(
                            `audio/${message.id}.mp3`,
                            data.AudioStream
                        );
                        addAudioToQueue(`audio/${message.id}.mp3`, message.member.voice.channel)

                        if (!isPlaying) {
                            playAudio()
                        }
                    } else {
                        console.log(message.client.voice.connections)
                        console.log(message.member.voice.channel.id)
                    }
                    
                }
            }
        );
    }
});

client.login("BotのTOKEN");

この記事に贈られたバッジ