2017年3月20日月曜日

Raspberry Pi + Open JTalkによる音声合成で天気予報付き温度計に喋らせる

0. はじめに

前回の記事「Raspberry Pi + Julius + LIRC により家電製品を音声認識で操作する」では、Raspberry Piで音声認識を行いました。

今回はその逆、ということで音声合成を行ってみましょう。音声合成システムとしてはOpen JTalkを用います。

題材としては、本書第4章で作成した「天気予報付き温度計」に、「現在〇〇度」や「今日(明日)の天気は○○」のように音声合成により話す機能を追加してみます。

作成した「喋る天気予報付き温度計」の様子は下図のようになります。見やすいよう大きいLCDを使っていますが、回路自体は書籍第4章とほぼ同じです。唯一異なるのが、温度計に喋らせるためのタクトスイッチを1つ追加した点です。

喋る天気予報付き温度計

この喋る天気予報付温度計の動作の様子を示したのが下記の動画です。温度計以外にも、音声認識と組み合わせて「音声認識の結果をそのままオウム返しで喋る」というデモンストレーションの様子も示しています。



1. Raspberry Piでのスピーカー(イヤフォン)の利用

まずはRaspberry Piで音を鳴らせるようにしましょう。主に下記の3つの方法があります。
  1. Raspberry Pi本体のミニジャックにスピーカーやイヤフォンを接続する方法
  2. Raspberry PiとつながったHDMIディスプレイにスピーカーやイヤフォンを接続する方法
  3. USB接続のサウンドカードを利用する方法
このうち、Raspberry Pi本体のミニジャックから音声を出力したい方は、OSのバージョンによって異なる設定が必要です。
2020-12-02以降の Raspberry Pi OS をお使いの方は、下記の設定によりミニジャックによる音声出力を設定してください。なお、raspi-config コマンドを実行中は、キーボードの Esc キーが「戻る」に対応しますので、困ったら Esc キーを何度か押してみると良いでしょう。
  1. ターミナルで「 sudo raspi-config 」コマンドを実行し、設定画面を開く
  2. キーボードの「Enter」キーを押し、「1 System Options」に入る
  3. キーボードの「↓」キーを一回押し、「S2 Audio」にフォーカスを合わせる
  4. キーボードの「Enter」キーを押し、「S2 Audio」の設定画面に入る
  5. キーボードの「↓」キーを一回押し、「1 Headphones」にフォーカスを合わせる
  6. キーボードの「Enter」キーを押し、「1 Headphones」を選択する
  7. キーボードの「TAB」キー二回を押し、「Finish」にフォーカスを合わせる
  8. キーボードの「Enter」キーを押し、raspi-config の設定画面を終了する
また、ミニジャックを利用したい方で 2020年2月までの Raspbian をお使いの方は、 ターミナルを起動して以下コマンドを実行してミニジャックを有効にしておきましょう。その他の場合の方はここでは何もせず先に進みます。
$ amixer cset numid=3 1

なお、USB接続のサウンドカードとしては以下の3種のみ動作確認をしました。

2. Open JTalkのインストール

まず音声合成システムOpen JTalkのインストール方法を紹介します。

ターミナルを起動して下記のコマンドを順に実行し、必要なソフトウェアをネットワークからインストールします。もちろんRaspberry Piがインターネットに接続されている必要があります。
$ sudo apt update
$ sudo apt install open-jtalk open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001
2 つ目のコマンド実行後に「続行しますか? [y/n]」などと聞かれた場合は、キーボードで「y」をタイプしてEnterキーを押して下さい。これらのインストールにより、「男性の声」での音声合成が可能になります。

さらに、女性の声で音声合成する際に必要になるファイルもインストールしておきましょう。こちらより、MMDAgent_Example-1.8.zipをブラウザでダウンロードしておきます。バージョンが新しくなっている場合、新しいもののダウンロードで問題ないと思います。 ダウンロードされたファイルはDownloadsディレクトリ(フォルダ)(/home/pi/Downloads)に格納されますので、本書p.322の図A-1のようにファイルマネージャを用いてユーザーpiのホーム(/home/pi)に移動しましょう。
その後、ターミナルで下記のコマンドを順に実行します。なお、MMDAgentの新しいバージョンがダウンロードされた場合、バージョン番号の部分(「1.8」)を適切に読み替えてください。
$ unzip MMDAgent_Example-1.8.zip
$ sudo cp -r  MMDAgent_Example-1.8/Voice/mei /usr/share/hts-voice/

最後に、実際にOpen JTalkを呼び出して発話をさせるプログラム (スクリプトといいます) をダウンロードして実行可能にしましょう。
$ wget https://raw.githubusercontent.com/neuralassembly/raspi/master/speech.sh
$ chmod a+x speech.sh
$ sudo mv speech.sh /usr/local/bin
以上でRaspberry Piに音声合成させるためのソフトウェアの準備ができました。

3. Open JTalkの動作確認

Open JTalkで音声合成を行う際、USB接続のサウンドカードを用いていないならば、そのまま実行が可能です。ターミナルを起動し、例えば下記のようなコマンドを実行します。
$ speech.sh ラズベリーパイ
「ラズベリーパイ」の部分は前回の記事「Raspberry Pi + Julius + LIRC により家電製品を音声認識で操作する」でインストールしたMozcにより日本語入力を行い実現します。 うまくいけば、男性の声で「ラズベリーパイ」と発声されます。想像がつくように、「ラズベリーパイ」の部分を変更すれば、合成される音声もかわりますので試してみて下さい。また、音声の指示に半角空白を含ませたい場合、「speech.sh 'ラズベリー パイ'」のように「'」でくくる必要があります。

なお、ここまでで音が正しく鳴るのは以下の2つの方法のどちらかで音声を聞いているでしょう。
  • Raspberry Pi本体のミニジャックにスピーカーやイヤフォンを接続する方法(2020年2月までのOSをご利用の場合)
  • Raspberry PiとつながったHDMIディスプレイにスピーカーやイヤフォンを接続する方法(OSのバージョンによらない)
それ以外の方法を用いている方は、プログラム実行前にプログラムの変更が必要です。ターミナルを起動し、実行スクリプトを編集用に管理者権限のleafpadで開きます。
$ sudo leafpad /usr/local/bin/speech.sh
なお、NOOBS 3.2.1以降ではテキストエディタとしてleafpadではなくmousepadを用います。
$ sudo mousepad /usr/local/bin/speech.sh
その中で下記の部分を見つけてください。
# for the device on Raspberry Pi
aplay -q $TMPVOICE

# for USB sound card (old Raspbian)
#  or earphone jack on Raspberrry Pi (Raspberry Pi OS 2020-05-27 or later)
#aplay -D plughw:1,0 -q $TMPVOICE

# for USB sound card (Raspberry Pi OS 2020-05-27 or later)
#aplay -D plughw:2,0 -q $TMPVOICE
「aplay」と書かれた行が3行ありますが、1つめが有効になっており、2つ目と3つ目は先頭に「#」があるため無効になっています。

ここで、「Raspberry Pi本体のミニジャックにスピーカーやイヤフォンを接続する方法(2020年5月以降のOSをご利用の場合)」および 「USB接続のサウンドカードを利用する方法(2020年2月までのOSをご利用の場合)」 を用いている場合は、 下記のように2番目のaplayのみを有効に変更してください。
# for the device on Raspberry Pi
#aplay -q $TMPVOICE

# for USB sound card (old Raspbian)
#  or earphone jack on Raspberrry Pi (Raspberry Pi OS 2020-05-27 or later)
aplay -D plughw:1,0 -q $TMPVOICE

# for USB sound card (Raspberry Pi OS 2020-05-27 or later)
#aplay -D plughw:2,0 -q $TMPVOICE
すなわち、1つ目のaplayに「#」をつけて無効化し、2つ目のaplayから「#」を削除して有効化するわけです。「#」は半角文字で記述しましょう。

また、「USB接続のサウンドカードを利用する方法(2020年5月以降のOSをご利用の場合)」を用いている場合は、 下記のように3番目のaplayのみを有効に変更してください。
# for the device on Raspberry Pi
#aplay -q $TMPVOICE

# for USB sound card (old Raspbian)
#  or earphone jack on Raspberrry Pi (Raspberry Pi OS 2020-05-27 or later)
#aplay -D plughw:1,0 -q $TMPVOICE

# for USB sound card (Raspberry Pi OS 2020-05-27 or later)
aplay -D plughw:2,0 -q $TMPVOICE
自分に該当する方法での編集が終わったらファイルを保存してleafpadを閉じて構いません。この編集が終わったら、 ターミナルで「speech.sh ラズベリーパイ」などと実行することで、それぞれの場合でも音声合成が行えるはずです。

なお、ボリュームの変更方法にもいくつかのパターンがあります。speech.sh を変更せずに済んだ方は、デスクトップの右上のアイコンでボリュームを変更できます。

一方、「Raspberry Pi本体のミニジャックにスピーカーやイヤフォンを接続する方法(2020年5月以降のOSをご利用の場合)」を用いている方は、次のコマンドが使えます。「70%」の部分を0~100%で変化させて適切なボリュームを見つけてください。
amixer -q -c1 cset numid=1 70%
また、「USB接続のサウンドカードを利用する方法(2020年2月までのOSをご利用の場合)」、ターミナルで例えば下記のようなコマンドを実行します。「10」の部分は0%~100%で表されたボリュームなので、適切な数値で読み替えます。特にイヤフォンを用いている場合、小さな値から試すのが無難です。
$ amixer -c 1 sset 'Speaker' 10
最後に、「USB接続のサウンドカードを利用する方法(2020年5月以降のOSをご利用の場合)」、ターミナルで例えば下記のようなコマンドを実行します。「10」の部分は0%~100%で表されたボリュームなので、適切な数値で読み替えます。特にイヤフォンを用いている場合、小さな値から試すのが無難です。
$ amixer -c 2 sset 'Speaker' 10


4. 音声の変更(お好みで)

デフォルトでは男性の声で音声合成されますが、これを女性の声に変更したい場合は、下記の手順に従ってください。 まず、ターミナルで下記のコマンドを実行し、実行スクリプトspeech.shを編集用に管理者権限のleafpadで開きます。
$ sudo leafpad /usr/local/bin/speech.sh
なお、NOOBS 3.2.1以降ではテキストエディタとしてleafpadではなくmousepadを用います。
$ sudo mousepad /usr/local/bin/speech.sh
その中で、下記の「HTSVOICE」で始まる部分に着目しましょう。
HTSVOICE=/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice
#HTSVOICE=/usr/share/hts-voice/mei/mei_happy.htsvoice
#HTSVOICE=/usr/share/hts-voice/mei/mei_angry.htsvoice
#HTSVOICE=/usr/share/hts-voice/mei/mei_bashful.htsvoice
#HTSVOICE=/usr/share/hts-voice/mei/mei_normal.htsvoice
#HTSVOICE=/usr/share/hts-voice/mei/mei_sad.htsvoice
この部分では、音声合成する際に用いる音声を選択しています。先頭に「#」がついている行はコメント文となっており、無効な行です。「#」がついていない 1 行のみが有効となっており、これが男性の声だったというわけです。

有効とする行を変更し(「#」のつかない有効な行を1行だけとする)、leafpadで保存してから、もう一度「speech.sh ラズベリーパイ」などと実行してみましょう。
その際、leafpadを起動したターミナルとは別に新たにターミナルを開いて実行するのが良いでしょう。声がかわるのが分かるはずです。なお、女性の声に変更するためには、インストール時にMMDAgent_Example-1.8.zipをダウンロードして、必要なファイルを適切な位置にコピーしていることが必要ですのでご注意ください。

5. 応用1:天気予報機能付き温度計に喋らせる

さて、以上により、Open JTalkとそれを呼び出すスクリプトにより音声合成ができることがわかりました。あとはこの機能をどう活用するかですが、まずは本書第4章で作成した「天気予報付き温度計」に発話機能を追加してみましょう。対象となるのは、本書第4章でこの温度計を作成した方となります。温度計とLCDの両方がI2C通信を行うものを用いますので、I2Cがあらかじめ有効にされている必要があります。

まず、必要な回路は下記のようになります。本書と同様にLCDとして横8文字x縦2文字のAQM0802を用いる場合は下図のようになります。GPIO 23に接続したタクトスイッチが一つ増えているだけです。ただし、最近はAQM0802を用いる際に、特殊な対処法が必要になることが多いです。本書補足ページ「本書発売後の追加情報」の「完成品のLCDを購入しても認識されない場合の暫定的な対処法」をご覧ください。


また、同じく「本書発売後の追加情報」の「4章全般:利用できるLCDについて」で紹介したストロベリーリナックスの横16文字x縦2文字のLCDを用いる場合の回路図はこちらです。LCDのピン配置が異なるだけで、基本的には同じ回路です。


これらの回路を動かすためのプログラムをダウンロードするため、ターミナルで下記のコマンドを実行してください。
$ wget https://raw.githubusercontent.com/neuralassembly/raspi/master/speech-weather.py
このプログラムは、書籍と同じくLCDとしてAQM0802を用いる場合のプログラムとなっています。 ストロベリーリナックスの横16文字x縦2文字のLCDを用いる場合、プログラムを少し変更する必要があります。そのためには、このプログラムを編集用にleafpadで開きます。ターミナルで下記のコマンドを実行しましょう。
$ leafpad speech-weather.py
なお、NOOBS 3.2.1以降ではテキストエディタとしてleafpadではなくmousepadを用います。
$ mousepad speech-weather.py
プログラム中で、211行目にある下記の部分に着目します。横16文字のLCDを用いる場合はこのうちchars_per_lineの数値を8から16に変更してください。また、contrastの値は、文字が薄い場合に40程度に変更してください。変更が終わったら保存してleafpadを閉じて構いません。
contrast = 32 # 0から63のコントラスト。通常は32、文字が薄いときは40を推奨
chars_per_line = 8  # LCDの横方向の文字数
display_lines = 2   # LCDの行数
さて、プログラムを実行するには、ターミナル上で、プログラムspeech-weatherがあるディレクトリ(フォルダ)にて下記のコマンドを実行します。Python3 のみサポートしています。
$ python3 speech-weather.py
ただし、 「OpenWeatherの天気予報データをLCDに表示する」にもとづき、123行目の API_KEY の部分を、自分の API Key に置き換える必要があります。
key = 'API_KEY'
適切に動作すると、冒頭の動画のように「左のタクトスイッチで温度計のモード切替」、「右のタクトスイッチで音声合成」が実現されるはずです。

このプログラム中で動作のポイントとなっているのは、81行目から始まる下記の部分です。modeというのは温度計の4つのモードを決める変数なのですが、その値に応じて、話す内容を場合分けしているのが見て取れます。
    elif channel==23:
        if mode==0:
            s = '今日の天気は'+weather_kanji
            args = ['speech.sh', s]
            try:
                subprocess.Popen(args)
            except OSError:
                print('no speech.sh')
        elif mode==1 or mode==3:
            s = '現在'+'{0:.1f}'.format(temperature)+'度'
            args = ['speech.sh', s]
            try:
                subprocess.Popen(args)
            except OSError:
                print('no speech.sh')
        elif mode==2:
            s = '明日の天気は'+weather2_kanji
            args = ['speech.sh', s]
            try:
                subprocess.Popen(args)
            except OSError:
                print('no speech.sh')


6. 応用2:音声認識と組み合わせてオウム返しで喋らせる

最後に、音声認識と組み合わせ、音声認識の結果をオウム返しで喋る、という例を試してみましょう。こちらも冒頭の動画にて例示されていましたね。 この例は音声認識にかなりの計算パワーを必要としますので、Raspberry Pi 3を用いることを推奨します。Raspberry Pi 3を用いても、認識から発声まで5秒程度の時間がかかることがあることが動画から見て取れるでしょう。 さらに、ここから先の内容は「Raspberry Pi + Julius + LIRC により家電製品を音声認識で操作する」を一通り終えた方を対象とします。

まず、音声認識を行う上で、どの辞書を用いるかに注意しておきましょう。「Raspberry Pi + Julius + LIRC により家電製品を音声認識で操作する」を終えた状態では「テレビ操作用の辞書」を用いる設定になっています。 一方、冒頭の動画では「Juliusのディクテーションキットのデフォルトの辞書」を用いました。どの辞書を用いても実行は可能ですが、もしデフォルトの辞書に戻したいならば、まずターミナルを起動して下記のように設定ファイルをleafpadで編集用に開きます。
$ leafpad dictation-kit-v4.4/main.jconf
なお、NOOBS 3.2.1以降ではテキストエディタとしてleafpadではなくmousepadを用います。
$ mousepad dictation-kit-v4.4/main.jconf
この中で、辞書選択部分を下記のように「model/lang_m/bccwj.60k.htkdic」を用いるようにするのでした(先頭に「#」がない行のみが有効です)。
## 単語辞書ファイル
##
-v model/lang_m/bccwj.60k.htkdic
#-v ../remocon.dic
編集が終わったら保存してleafpadを閉じます。これにより、デフォルトの辞書が用いられるようになりました(ちなみに、自分でカスタマイズした辞書の方が単語数が少ないので認識速度は速いです)。

次に、必要なプログラムをダウンロードするため、ターミナルを起動して下記のコマンドを実行します。
$ wget https://raw.githubusercontent.com/neuralassembly/raspi/master/recog-speech.py
このプログラムを実行するためには、事前に音声認識エンジンJuliusをモジュールモードで起動しておく必要があります。 そのためには、ターミナルを起動して下記のコマンドを実行するのでした。
$ julius -C dictation-kit-v4.4/main.jconf -C dictation-kit-v4.4/am-gmm.jconf -demo -module
その状態で、もう一枚別のターミナルを起動し、その上で下記のコマンドでプログラムを実行します。
$ python recog-speech.py
あとはマイクに向かって話せば冒頭の動画のような動作が実現します。動画にも注釈がありますが、音声合成の結果の音声をマイクが拾わないように注意してください。そうしないと、「認識」→「合成」→「認識」→「合成」→…が無限に繰り返されます(この現象の原因に気づくまでちょっと悩みました)

さて、このプログラムrecog-speech.pyのポイントは、38行目から始まる下記の部分です。 認識された語は「''」→「語1」→「語2」→…「語n」→「'。'」という順で届くのですが、これをひとまとめに 「語1語2…語n。」と結合してからspeech.shに渡しています。
# 認識された単語wordの中に、u('...') という文字列が含まれるかどうかを
# チェックし、文字列に応じたアクションを記述します。
# u('...')でくくるのは、python2とpython3の互換性を保つためです。

if word != u(''):
    recognized_word += word

if u('。') in word: 
    print(recognized_word)
    args = ['speech.sh', recognized_word]
    subprocess.Popen(args).wait()
    recognized_word = u('')
以上、お疲れさまでした。

1 件のコメント: