discord.js でボイスチャットを音声認識してテキストログを取る

2020/04/24現在、@TumoiYorozu 様のコメントより、Botがボイスチャンネル接続時に、発話することで動作するとのことです！

~~**2019/11/06現在 この記事通りに音声を受信できません。推測ですが、Discord側の仕様が変わった可能性があります**~~

# 1 概要
Discord(discord.js) 経由で取得した音声をgoogleの音声解析APIを利用することで、テキスト化する方法を紹介します。
Discord ボイスチャンネルから音声取得する方法は、[先日の記事](https://qiita.com/atticatticattic/items/f8d91b99dca0cdc6e6d8) を参照してください。
ここではDiscord経由で取得した音声データを google API に流し込む方法を中心に紹介します。

＊すいません、アドベントカレンダーには音声合成もするって書いていましたが、間に合いませんでした！

**本記事は下記の事項を把握していることを前提としています**
- [『discord.js で ボイスチャットの音声録音ボットを作ってみる』](https://qiita.com/atticatticattic/items/f8d91b99dca0cdc6e6d8) 
- [『Node.jsからGoogle Speech APIを使ってみる』](https://qiita.com/amama-nagigi/items/1470f16b48ca061e194e) 

# 2 動作確認環境 

- マシン : iMac Late 2015
- OS : mac OSX 10.14.1 (Mojave)
- node : v8.12.0(Mac環境でライブラリが動くのがこのバージョンだった)
- **discord.js: 11.4.2**

# 3 やりたいこと
やりたいことは下図のように会話内容をテキストログ化するようなbotです。
ここまでできれば、音声応答botが作れます！！！

<img width="947" alt="スクリーンショット 2018-12-21 19.05.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/12429/2258960f-71b7-85da-5db8-0544c92e1dc6.png">
<img src="https://qiita-image-store.s3.amazonaws.com/0/12429/7addd1a2-d850-7b15-fb1d-717e5fec112b.png" width=100px>
＊bot アイコンは キヨイチ様に制作していただきました(https://twitter.com/kiyoichi261)

# 4 会話音声のテキストログ化

プログラム一式はこちらのレポジトリから取得できますので、合わせてご参照ください。
https://github.com/YoshikazuOota/discord_voice

## 4.1 google Speech-To-Text へのつなぎ込み
後述する『音声フォーマットの問題』(4.2)がなければ難しいことはなく、 [Évelyne Lachance(eslachance) 様のプログラム](https://gist.github.com/eslachance/fb70fc036183b7974d3b9191601846ba)の createPCMStream の出力を googleのSpeech-To-Text の streamingRecognize に繋いであげるだけです。   

```javascript:主な変更部分（L41-L53）
const audioStream = receiver.createPCMStream(user);
const recognizeStream = speechClient.streamingRecognize(stt_request)
  .on('error', console.error)
  .on('data', (data) => {
    if(data.error === null){
      console.log(`${user.username} : ${data.results[0].alternatives[0].transcript}`);
  }
});
audioStream.pipe(recognizeStream);
```


```javascript:v9-voice-reveive_text.js(フルソース)
require('dotenv').config();　// 環境変数の設定(exportしなくてもよくなる)

const Discord = require("discord.js");
const fs = require('fs');

const client = new Discord.Client();
const config = require('./auth.json');

const speech = require('@google-cloud/speech');

const SAMPLE_RATE = 48000;
const speechClient = new speech.v1p1beta1.SpeechClient();
const stt_request ={
    config: {
        encoding: 'LINEAR16', // signed 16bit PCM
        sampleRateHertz: SAMPLE_RATE, // 48kHz
        languageCode: 'ja-jp',
    }
};

client.on('message', msg => {
    if (msg.content.startsWith(config.prefix+'join')) {
        let [command, ...channelName] = msg.content.split(" ");
        if (!msg.guild) {
            return msg.reply('no private service is available in your area at the moment. Please contact a service representative for more details.');
        }
        const voiceChannel = msg.guild.channels.find("name", channelName.join(" "));
        //console.log(voiceChannel.id);
        if (!voiceChannel || voiceChannel.type !== 'voice') {
            return msg.reply(`I couldn't find the channel ${channelName}. Can you spell?`);
        }
        voiceChannel.join()
            .then(conn => {
                msg.reply('ready!');
                // create our voice receiver
                const receiver = conn.createReceiver();

                conn.on('speaking', (user, speaking) => {
                    if (speaking) {
                        msg.channel.sendMessage(`I'm listening to ${user}`);

                        // !! modify Streo to "MONO" !!
                        // this creates a 16-bit signed PCM, "Single" 48KHz PCM stream.
                        const audioStream = receiver.createPCMStream(user);
                        const recognizeStream = speechClient.streamingRecognize(stt_request)
                            .on('error', console.error)
                            .on('data', (data) => {
                                if(data.error === null){
                                    console.log(`${user.username} : ${data.results[0].alternatives[0].transcript}`);
                                }
                            });
                        audioStream.pipe(recognizeStream);

                        // when the stream ends (the user stopped talking) tell the user
                        audioStream.on('end', () => {
                            msg.channel.sendMessage(`I'm no longer listening to ${user}`);
                        });
                    }
                });
            })
            .catch(console.log);
    }
    if(msg.content.startsWith(config.prefix+'leave')) {
        let [command, ...channelName] = msg.content.split(" ");
        let voiceChannel = msg.guild.channels.find("name", channelName.join(" "));
        voiceChannel.leave();
    }
});

client.login(config.token);

client.on('ready', () => {
    console.log('ready!');
});
```

## 4.2 discord.js と google Speech-To-Text の音声フォーマット問題
上記のソースのようにできれば幸いなのですが、discord.js と google Speech-To-Text でフォーマットを合わせる必要があります。

オリジナルの discord.js の createPCMStream の出力フォーマットは

- PCM 
- 16 bit
- 48Hz   
- 2ch (ステレオ)

ですが　google の Speech-To-Text は 1ch しかサポートしていません。(2chで送信する必要がないので当然ですが...)
ここでも前回同様 discord.js のライブラリをいじって対応しました。

**補足**
何らかの ライブラリなどを使って createPCMStream 出力を 1ch 化すれば良いのですが、良さげなライブラリが見つからず、オーバーヘッドが増えるのも嫌だったので、ライブラリを直接編集しました。

**参考 google Speech to text で対応している音声フォーマット**
https://cloud.google.com/nodejs/docs/reference/speech/2.1.x/google.cloud.speech.v1p1beta1.html?hl=ja#.AudioEncoding

## 4.3 discord.js の createPCMStream 出力を モノラル化(1ch)する方法 
**注** : この方法は オーディオエンジンに "node-opus"をしている場合のみ有効です。”opusscript”を使用している場合は、この方法を行ってもモノラル化しません。 

レポジトリに用意してある
<b>node_modules_modify</b>/discord.js/src/client/voice/opus/NodeOpusEngine.js
を、npm install でローカルに保存されたソース
<b>node_modules</b>/discord.js/src/client/voice/opus/NodeOpusEngine.js
に、マージすることで、createPCMStream の出力が 1ch になります。

diff/* に diffのレポートファイルなどあるので、こちらを参考にマージしてください。

<img width="1057" alt="スクリーンショット 2018-12-20 17.18.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/12429/db577f30-2950-4a41-3714-9ee84e1c464e.png">

## 4.4 ファイル出力してモノラル化を確認
4.3 まで完了したら、[『discord.js で ボイスチャットの音声録音ボットを作ってみる』](https://qiita.com/atticatticattic/items/f8d91b99dca0cdc6e6d8) の 3.5 の要領で音声保存して、再生してみて正常に音声が再生されることを確認してください。

<img width="303" alt="スクリーンショット 2018-12-20 17.22.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/12429/582e8042-9c94-fce7-2d15-bc5955ff201b.png">

## 4.5 動作確認
以上を踏まえて、具体的な実行手順を説明します。

### (1)レポジトリダウンロード、ライブラリダウンロード
```console
git clone https://github.com/YoshikazuOota/discord_voice
npm install
```
### (2) ライブラリ discord.js の修正
下記の2ファイルを修正してください。

- node_modules/discord.js/src/client/voice/VoiceUDPClient.js (必要があれば)
- node_modules/discord.js/src/client/voice/opus/NodeOpusEngine.js

レポジトリで作業している場合は、下記コマンドで修正箇所を確認できます。

```console
 diff node_modules/discord.js node_modules_modify/discord.js 
```

diff/* にも差分レポートファイルなどを置いておきましたので参照ください。

### (3) google cloud の証明書を設置
[『Node.jsからGoogle Speech APIを使ってみる』](https://qiita.com/amama-nagigi/items/1470f16b48ca061e194e) で紹介されている要領で、 
- .env
- 証明書のjsonファイル
を設置してください　

### (4) コマンド実行
最後に、コマンド実行するだけです。

```command
node v9-voice-reveive_text.js  
```

この際、[『discord.js で ボイスチャットの音声録音ボットを作ってみる』](https://qiita.com/atticatticattic/items/f8d91b99dca0cdc6e6d8) にあるように、UDPポートをバイパスするコマンドも実行してください。

```command:例
udpforwarder --protocol udp4 --port 13698 --address 0.0.0.0 --forwarderPort 0 --forwarderAddress 0.0.0.0 --destinationPort 33333 --destinationAddress 0.0.0.0
```




上記プログラムを正常実行できると、コンソールに音声に対応するテキストが表示されます。

![スクリーンショット 2018-12-20 17.48.57.png](https://qiita-image-store.s3.amazonaws.com/0/12429/db6a9be5-a610-134f-1274-31ac23449752.png)


# 5 最後に
googleの Speech to Text は趣味に使える程度の料金なのに、精度は高く本当にありがたいですね。
あとは辞書登録機能などで専門用語の判定確率が上がれば言うことはないのです。
専門用語の登録機能を考えると、[「AmiVoice Cloud」](https://www.advanced-media.co.jp/products/service/amivoice-cloud-2)が良さげなのですが、お小遣いでは運用できなさそうなのですよね。

Discord bot で音声を取り扱えれば、アレクサなどとの連携も可能で、アイデア次第でいろいろなことができそうですよね！
よろしければ、皆様のアイデアなどもコメントいただけるととても嬉しいです！