音声認識してテキストチャットを自動化

## はじめに

手が離せない時に、喋った内容を自動でテキストチャットして欲しいなと思って作った時のメモです。
テキストチャットに対応したオンラインゲーム用に作りました。

---

## 動作環境

私が試した環境です。

- OS : Windows10pro 1809
- プログラム言語 : Python 3.7.3

---

## フローの概要

![gaiyo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483481/61de52e4-3150-8493-c85e-3f9c66abfb15.png)


1. マイクからの音声を`sox`で音声ファイルに出力します。
1. 音声データを`Cloud Speech-to-Text`へ送信し、テキストに変換します。
1. 受け取った文字列を`PyAutoGui`でキーボードを操作して出力します。

---

## 録音ファイルの作成

以下のページを参考にさせて頂きました。（１は環境構築の手順、2は音声認識のためのトリガー設定の方法）

1. [音声を録音してみる（Python）SoX(Sound exchenge)](http://blog1.tela.daa.jp/?eid=142&imageviewer&image=20181015_1756611.png)
1. [SoX（Sound eXchange)で音声感知録音:その1](https://p--q.blogspot.com/2015/03/soxsound-exchange1.html)

以下にコードを記載しています。

```py
import os
import subprocess

# 環境パスの設定
os.environ['AUDIODRIVER'] = \
    'waveaudio'
WAVE_OUTPUT_FILENAME = 'data.FLAC'

def rec_wav():
    subprocess.run('sox -r 16000 -c 1 -d ' + WAVE_OUTPUT_FILENAME \
        + ' silence 1 00:00:00.5 -55d 1 00:00:02 -45d', \
        shell=True)    # 録音
    print('rec end')
```

音声を録音するためのトリガーは、**一定時間、音量が一定値を超える**としています。コードの `silence 1 00:00:00.5 -55d` の部分が対応しています。 
録音の終了条件は、**一定時間、音量が一定値を下回る**としています。コードの `1 00:00:02 -45d` の部分が対応しています。 
また、`Cloud Speech-to-Text` に対応した音声データとするために、モノラルで出力しています。コードの `-c 1` の部分が対応しています。  

---

## 音声からテキストへ変換

以下のページを参考にさせて頂きました。（１はサービスアカウントキー取得までの環境構築の手順、2は実際のプログラムコードの参考、3はオプションなどの理解）

1. [Google Cloud Speech API を使った音声の文字起こし手順](https://qiita.com/knyrc/items/7aab521edfc9bfb06625)
1. [短い音声ファイルの文字変換](https://cloud.google.com/speech-to-text/docs/sync-recognize?hl=ja#speech-sync-recognize-python)
1. [Cloud Speech-to-Text の基本](https://cloud.google.com/speech-to-text/docs/basics?hl=ja)


以下にコードを記載しています。

```py
import io

# 環境パスの設定
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = \
    '########.json'

def transcribe_file():
    from google.cloud import speech
    from google.cloud.speech import enums
    from google.cloud.speech import types

    get_message = ''
    client = speech.SpeechClient()
    # 音声データのセット
    with io.open(WAVE_OUTPUT_FILENAME, 'rb') as audio_file:
        content = audio_file.read()
    audio = types.RecognitionAudio(content=content)
    config = types.RecognitionConfig(
        sample_rate_hertz=16000,
        language_code='ja-JP')
    # サーバへの送受信
    response = client.recognize(config, audio)
    for result in response.results:
        get_message = u'{}'.format(result.alternatives[0].transcript)
    return get_message
```

コードは、ほぼ[参考ページ２](https://cloud.google.com/speech-to-text/docs/sync-recognize?hl=ja#speech-sync-recognize-python)のままです。 
`###.json` のところには、ダウンロードしたサービスアカウントキーのパスを指定しています。 
サンプルレートは、録音時の `sox` の設定 `-r 16000` と同じにしています。 
言語は、日本語 `ja-JP` を指定しています。

---

## テキストチャットとして出力

以下のページを参考にさせて頂きました。

1. [PyAutoGUIの使い方 キーボード操作編](http://bttb.s1.valueserver.jp/wordpress/blog/2017/08/25/pyautogui%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9-%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E6%93%8D%E4%BD%9C%E7%B7%A8/)
1. [日本語文字をローマ字に変換するpykakasiモジュールのインストールと利用について](https://qiita.com/komorin0521/items/8cd1eb0cdb4a9ede217e)

以下にコードを記載しています。

```py
import pyautogui as pgui
from pykakasi import kakasi

# ローマ字変換の設定
kakasi = kakasi()
kakasi.setMode('H', 'a')
kakasi.setMode('K', 'a')
kakasi.setMode('J', 'a')
conv = kakasi.getConverter()

def put_chat(text_chat):
    # ローマ字変換
    text_chat = conv.do(text_chat)
    # チャット出力
    pgui.typewrite(text_chat)
    pgui.press(keys="space")
    pgui.press(keys="enter",presses=2)
```

テキストチャットを行いたいウィンドウをアクティブにした状態で、実行しています。 
`Cloud Speech-to-Text` で認識されたテキストは、漢字/ひらがな/カタカナ交じりの形となっています。キーボードではローマ字入力としていましたので、認識されたテキストを `pykakasi` でローマ字（アルファベット文字列）に変換しています。 
アルファベットを入力した後、`SPACE` で変換し、１回目の `ENTER` で確定、２回目の `ENTER` でチャット送信を行っています。

---

## プログラムコード全体

以下に全体のコードを記載しています。

```py
import time
import io
import os
import subprocess
import pyautogui as pgui
from pykakasi import kakasi

# -------------------------------
# 設定
# -------------------------------
# 動作
WAITE_TIME_INITIAL = 3  # 起動時の待機時間
# 環境パスの設定
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = \
    '########.json'
os.environ['AUDIODRIVER'] = \
    'waveaudio'
# 音声ファイルの設定
WAVE_OUTPUT_FILENAME = 'data.FLAC'
# ローマ字変換の設定
kakasi = kakasi()
kakasi.setMode('H', 'a')
kakasi.setMode('K', 'a')
kakasi.setMode('J', 'a')
conv = kakasi.getConverter()


# -------------------------------
# 録音
# -------------------------------
def rec_wav():
    subprocess.run('sox -r 16000 -c 1 -d ' + WAVE_OUTPUT_FILENAME \
        + ' silence 1 00:00:00.5 -55d 1 00:00:02 -45d', \
        shell=True)    # 録音
    print('rec end')

# -------------------------------
# 音声認識
# -------------------------------
def transcribe_file():
    from google.cloud import speech
    from google.cloud.speech import enums
    from google.cloud.speech import types

    get_message = ''
    client = speech.SpeechClient()
    # 音声データのセット
    with io.open(WAVE_OUTPUT_FILENAME, 'rb') as audio_file:
        content = audio_file.read()
    audio = types.RecognitionAudio(content=content)
    config = types.RecognitionConfig(
        sample_rate_hertz=16000,
        language_code='ja-JP')
    # サーバへの送受信
    response = client.recognize(config, audio)
    for result in response.results:
        get_message = u'{}'.format(result.alternatives[0].transcript)
    return get_message


# -------------------------------
# ゲーム画面にチャットを出力
# -------------------------------
def put_chat(text_chat):
    # ローマ字変換
    text_chat = conv.do(text_chat)
    # チャット出力
    pgui.typewrite(text_chat)
    pgui.press(keys="space")
    pgui.press(keys="enter",presses=2)


# -------------------------------
# main
# -------------------------------
if __name__ == '__main__':
    time.sleep(WAITE_TIME_INITIAL)
    while True:
        rec_wav()
        get_message = transcribe_file()
        if not get_message == '':
            put_chat(get_message)
            print(get_message)
```

`main` 関数内で一連の処理をループさせています。 
普段は `rec_wav()` で音量を監視している状態となり、トリガー条件を満たすと一連の処理を実行し、再び `rec_wav()` での音量監視の状態に戻ります。

以下に 'requirements.txt` の内容を記載しています。一部必要のないものも入っているかもしれません。

```requirements.txt
astroid==2.2.5
cachetools==3.1.1
certifi==2019.6.16
chardet==3.0.4
colorama==0.4.1
google-api-core==1.14.2
google-auth==1.6.3
google-cloud-speech==1.2.0
googleapis-common-protos==1.6.0
grpcio==1.22.0
idna==2.8
isort==4.3.21
lazy-object-proxy==1.4.1
mccabe==0.6.1
MouseInfo==0.0.4
Pillow==6.1.0
protobuf==3.9.1
pyasn1==0.4.6
pyasn1-modules==0.2.6
PyAudio==0.2.11
PyAutoGUI==0.9.47
pygame==1.9.6
PyGetWindow==0.0.7
pykakasi==1.0
pylint==2.3.1
PyMsgBox==1.0.7
pyperclip==1.7.0
PyRect==0.1.4
PyScreeze==0.1.22
PyTweening==1.0.3
pytz==2019.2
requests==2.22.0
rsa==4.0
semidbm==0.5.1
six==1.12.0
typed-ast==1.4.0
urllib3==1.25.3
Wave==0.0.2
wrapt==1.11.2
```

---

## おわりに

ゲーム中に手元が忙しい時もチャットできるようになって、便利になりました。 
googleの音声認識の精度は素晴らしく、ちゃんとした日本語がかなりの確率で返ってきます。むしろ録音環境やしゃべり方の側で認識精度を低下させている印象が強いです。使っていたマイクの入力音量がどうも小さく、いいマイクをamazonに注文してしまいました。おそらく、さらに使いやすくなるのではないかと期待しています。 
最初はwindows10のデフォルトの音声認識機能を試したのですが、認識精度が悪かったため、google音声認識に切り替えました。 
録音のトリガーは、最初はゲームパッドの特定のボタンを押す事にしようと考えていたのですが、ゲームウィンドウをアクティブにした状態から、ボタンの入力をpython側で検知する方法が分からず、音量に切り替えました。（pygameでいけるのかなと思っていたのですが、pygameで作成したウィンドウに対する入力を検知する方法しか分かりませんでした） 
録音には `PyAudio` ライブラリもありましたが、`sox` の方が初心者向きに思えたので今回は `sox` を使用しました。 `PyAudio` であれば、`pip` だけで管理できるので、環境構築はシンプルに思えたので、そちらも今後確認してみたいと思います。