Chromeで音声認識して、Discordに書き込み

## 1. 概要
Google Chromeの Web Speech API を利用して音声認識し、結果を Discordのテキストチャンネルに書き込みをする方法を紹介します。
やっていることは、[NAMAROIDの音声認識をChromeでやってみた](https://qiita.com/atticatticattic/items/590e5173148195ad80e1) とほぼ同様ですが、本記事では実装面を解説します。

本実装は Webサーバを介した、クライアント・サーバ形式で、相互のメッセージングに socket.ioを利用しています。
socket.ioを利用することにより、Web APIに比べて実装が簡単で、送受信の高速化が期待できます。

<img width="1013" alt="スクリーンショット 2019-12-12 17.22.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12429/5404a99c-2390-f1f1-26c5-f6fbdc0cfa4f.png">
アイコンは[おむ烈 様](https://seiga.nicovideo.jp/seiga/im4842809)のフリーアイコンをお借りしております！

#### 参考記事
- Discord Bot の登録方法  
[Discord Botアカウント初期設定ガイド for Developer](https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf)

#### 動作環境
- OS: Windows 10
- node: v9.11.1
- chrome: 78.X

#### ソースコード
https://bitbucket.org/YoshikazuOota/post_stt

## 2. 使い方
### 2.1 ライブラリのインストール
初回のみ `npm install` (setting.bat) を実行して

### 2.2 Discord Bot・テキストチャンネルの設定
`config/discord.json`で下記の2つを指定する
- Botのトークン
- 書き込み先のテキストチャンネル

### 2.3 サーバサイドの実行
1.  `node server.js` 実行(start.bat) を実行
2. ブラウザで http://localhost:4000 を開く

### 2.4 ブラウザ操作
1. 『音声認識開始』ボタンをクリックして、音声認識かいし
2. 『停止』ボタンで音声認識を終了

![スクリーンショット 2019-12-12 17.13.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12429/169369c1-fbeb-24e0-eaf6-4ff5525cc4e6.png)


**補足**
音声入力がうまく出来ていないときは、マイクの使用許可設定が適切か確認してください。

## 3. 実装の解説
### 3.A クライアントサイド(index.html)
**Chrome で音声認識して、サーバにテキストを送る**

'server.js' を起動すると、`http://localhost:4000` で 'index.html' にアクセスすることができます。
この `index.html`がクライアントサイドになります。

音声認識のメイン処理は `speech_recognition.js` で行われます。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>音声認識 Discordブリッジ</title>

    <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css" >
    <link rel="stylesheet" href="assets/bootstrap/css/bootstrap-grid.min.css" >
    <link rel="stylesheet" href="assets/bootstrap/css/bootstrap-reboot.min.css" >

    <script src="assets/lib_js/jquery-3.3.1.min.js" ></script>
    <script src="assets/bootstrap/js/bootstrap.js" ></script>
    <script src="/socket.io/socket.io.js"></script> <!-- socket.io を読み込み -->
    <script src="assets/js/speech_recognition.js"></script> <!-- メインロジック -->

    <style>
        .row { margin-top: 20px;}
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-sm-12">
            <h3>Chromeで音声認識して、Discordに書き込み</h3>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-5">
            <button class="btn btn-primary" id="start_btn">音声認識開始</button> <!-- 音声認識開始ボタン -->
            <button class="btn btn-secondary" id="end_btn">停止</button> <!-- 音声認識停止ボタン -->
        </div>
        <div class="col-sm-7 d-flex align-items-center">
            <strong><span id="status" style="font-size: 24px"></span></strong> <!-- 音声認識 ON/OFF 表示 -->
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <div class="card-text">
                <span id="prosess"></span> <!-- 音声認識イベント表示 -->
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <div class="card">
                <div class="card-body">
                    <div class="card-text" id="content"></div> <!-- 認識したテキスト表示 -->
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>
```

```speech_recognition.js
const socket = io.connect();                  // ソケットio
const speech = new webkitSpeechRecognition(); // 音声認識APIの使用
speech.lang = "ja";                           // 言語を日本語に設定
let keep_standby = false;

function update_prosess(text) { $("#prosess").text(text); } // 音声認識 スタンバイ/停止 表示
function update_status(text)  { $("#status").html(text); }  // 音声認識 イベント 表示

// 音声認識した文字表示
function update_result_text(text)  {
    update_prosess('[結果表示]');
    $("#content").text(text);
    console.log(text);
}

// 音声認識の結果取得時の処理
speech.onresult = result => {
    const text = result.results[0][0].transcript;
    update_result_text(text);
    socket.emit('get_word', {word: text}); // 認識文字を socket.io 経由でサーバに送信する
};

// 音声認識の継続継続処理
speech.onend = () => {
    if (keep_standby) speech.start();
    else speech.stop();
};

speech.onspeechstart = () => update_prosess('[音声取得開始]');
speech.onspeechend   = () => update_prosess('[解析開始]');

$(function () {
    $("#start_btn").on('click', () => {
        update_status('<span class="text-success">『音声認識スタンバイ』</span>');
        keep_standby = true;
        speech.start();
    });
    $("#end_btn").on('click', () => {
        update_status('<span class="text-danger">『停止中』</span>');
        update_prosess('[音声認識停止中]');
        keep_standby = false;
        speech.stop();
    });

    $("#end_btn").trigger('click'); // 初期
    $("#start_btn").trigger('click'); // 自動スタート
});

```

### 音声認識の処理フロー
**1. index.html: `socket.io`, `speech_recognition.js` を読み込み**
`/socket.io/socket.io.js` の実態ファイルがソースコード一式にないため、不思議な感じですが、このファイルは `server.js` で `socket.io` の設定をすると勝手に用意してくれます。

```javascript
<script src="/socket.io/socket.io.js"></script> <!-- socket.io を読み込み -->
<script src="assets/js/speech_recognition.js"></script> <!-- メインロジック -->
```

**2. speech_recognition.js: socket.io オブジェクト取得**
サーバ側(server.js)へのテキスト送信はこのオブジェクトを介して行います。

```javascript
const socket = io.connect();                  // ソケットio
```
 
**3. speech_recognition.js: "音声認識APIの使用"**
音声認識のオブジェクト(speech)を取得します。
音声認識に関わる処理はこちらを介して行います。
その際、`speech.lang = "ja"` をお忘れなく。

```javascript
const speech = new webkitSpeechRecognition(); // 音声認識APIの使用
speech.lang = "ja";   
```

**4. speech_recognition.js: `$("#start_btn").on`**
index.html で表示している、『音声認識開始』ボタンをクリックした時のイベント。
ここの `speech.start();`で音声認識が開始されます。
以降、音声認識処理が完了すると `speech.onresult` で、音声認識した文字列を取得出来ます。

```javascript

    $("#start_btn").on('click', () => {
        update_status('<span class="text-success">『音声認識スタンバイ』</span>');
        keep_standby = true;
        speech.start();
    });
```


**5. speech_recognition.js: `speech.onresult ` でサーバ**(server.js)への文字送信
音声認識した文字列は `result.results[0][0].transcript;`に格納されています。大抵このまま決め打ちでいいようです。
細かい仕様は https://wicg.github.io/speech-api/#speechreco-result を参照してください。

**6. speech_recognition.js: `speech.onend`で音声認識を継続**
処理を継続するための処理です。
5で `keep_standby = true`として、このフラグが`true`であれば、`speech.start()`をコールして継続処理をします。

## B. サーバーサイド(server.js)
**音声認識文字を受け取り、Discord の テキストチャンネル書き込む**

`server.js` では クライアント側の`speech_recognition.js` の `socket.emit` で送信される文字を取得して、Discord のテキストチャンネルに書き込みをします。

```server.js
const PORT = 4000;

const express = require('express');
const os = require('os');

const app = express();
app.use(express.static(__dirname));
const server = require('http').createServer(app).listen(PORT);
const io = require('socket.io').listen(server);

// ログイン処理
const config = require("./config/discord");
const Discord = require('discord.js');
const client = new Discord.Client();
const token = config.token;

client.on('ready', () => {
    console.log('start server');
    console.log(`open chrome: http://localhost:${PORT}`);

    const targetTextChannel = client.channels.find(val => val.id === config.channel_id); // 指定テキストチャンネルを取得

    io.sockets.on('connection', function(socket) {
         socket.on('get_word', function (data) {
             if(data === undefined || data === "" || data === null) return;

             console.log(data);
             targetTextChannel.send(data.word);
        });
    });
});

client.login(token);
```

### 音声認識の文字取得・テキストチャンネル書き込み処理フロー
**1. Webサーバー**
expressを利用して index.html 用の Webサーバを立ち上げ、Webサーバに socket.ioを追加する。
 
```server.js
const app = express();
app.use(express.static(__dirname));
const server = require('http').createServer(app).listen(PORT);
const io = require('socket.io').listen(server);
```

**2. Discord Botの設定**
`discord.js`を利用して Botを起動させる。
その際、`config/discord.json` にDiscord Bot トークンと、書き込みをするDiscordテキストチャンネルIDを記載する。

```json
{
  "token": "Discord bot token",
  "channel_id": "テキストチェンネルID"
}
```

`client.login(token)`で Botが起動して、起動完了後`client.on('ready', () => {})`がコールされます。

```server.js
const config = require("./config/discord");
const Discord = require('discord.js');
const client = new Discord.Client();
const token = config.token;
...
client.login(token);
```
**3. 指定のテキストチャンネルを取得**
`config.channel_id`に一致する channelを取得します。
`find(val => val.name === 'チャンネル名')`とすると、チャンネル名でチャンネルを取得できます。

```server.js
    const targetTextChannel = client.channels.find(val => val.id === config.channel_id); // 指定テキストチャンネルを取得
``` 
 
**4. socket.io の接続待ち**
index.html側で socket.ioに接続するのを待ち、接続があった場合は`io.sockets.on('connection', () => {})`が実行されます。
この処理は index.htmlを開いたページ毎にコールされ、音声入力する index.htmlは複数でも対応可能です。
 
**5. 取得文字をテキストチャンネルにポスト**
index.html側で `get_word`イベントを発火すると、下記の`get_word`がコールされます。
`data.word`に音声認識した文字列が格納されているので、それを送信するだけで、テキストチャンネルに書き込みをします。
 
```server.js
 socket.on('get_word', function (data) {
     if(data === undefined || data === "" || data === null) return;

     console.log(data);
     targetTextChannel.send(data.word);
});
```

## 最後に 
google chromeの音声認識は無料で、API利用手続きなどをする必要もないので、個人利用するのであればすごく便利ですね。
google Cloud の STT(Speech-to-Text)も低料金で利用できるのですが、音声処理周りの実装やAPI利用のための設定が大変ですので、Chromeの Web Speechが一番楽です。

音声入力は未来感があって、筆者はとてもワクワクするインターフェイスだと思っています！