できること

Pythonでデバイスを制御しよう 【第1回】日本語テキストを読み上げる(4) 

OpenJTalkを使ってオフラインで音声合成

 

(1)日本語テキストを音声に変換するには?
(2)あらかじめmp3に録音した音声を再生する
(3)REST(Web API)を使って発話する

 

「ラズパイで日本語テキストを読み上げる」のパート4では、オフラインで音声ファイルを作成する方法を説明します。
オフラインで音声ファイルを作成するためには「音声合成エンジン」と呼ばれるプログラム本体と「辞書」や「音響モデルと呼ばれる音声データ」などのファイルをラズパイ上にインストールしておく必要があります。
手軽に使用できる音声合成エンジンにはOpen JTalkとpyttsx3(ラズパイでは英語のみ)があり、それぞれの音声ファイルの作り方を説明します。
作成するPythonプログラムと音声合成エンジンとの連携には「パイプ」と呼ばれるプログラム間でデータをやり取りする仕組みを利用します。

control-device-with-raspberrypi-01-01

 

目次

  1. オフラインで音声ファイルを作成
  2. OpenJTalkでネットワークに依存しない発話プログラム
    1. 2.1. 本プログラムの構成
    2. 2.2. 準備
    3. 2.3. Pythonプログラム p6.py
    4. 2.4. プログラムの説明
    5. 2.5. プログラムの実行と検証
  3. subprocessとPopenを使ってPythonと他のプログラムを連携させる
  4. シェルスクリプトを使って音声ファイルの作成と再生を連携
    1. 4.1. 本プログラムの構成
    2. 4.2. シェルスクリプト s2.sh
    3. 4.3. シェルスクリプトの説明
    4. 4.4. シェルスクリプトの実行と検証
  5. pyttsx3を使った音声ファイル作成(現状は英語のみ)
    1. 5.1. 本プログラムの構成
    2. 5.2. 準備
    3. 5.3. Pythonプログラム p7.py
    4. 5.4. プログラムの説明
    5. 5.5. プログラムの実行と検証
  6. まとめ

 

1. オフラインで音声ファイルを作成

パート4では読み上げるテキストをオフラインで音声ファイルに変換し再生する方法を紹介します。

control-device-with-raspberrypi-04-01

オフラインで作成した音声ファイルを再生

 

ネットワーク接続のない環境で発話するには、ラズパイに音声合成エンジンが必要です。また、音声合成エンジンを動作させるには辞書や「音響モデル」と呼ばれる音声データなども必要です。
日本語の音声合成が手軽に行える音声合成エンジンにはOpen JTalkとpyttsx3の2つがあります。ただしpyttsx3はラズパイ環境で日本語発音に非対応(2021年3月時点)なので、今回はパート3で作成したPythonプログラムを改良してOpen JTalkに対応させます(p6.py)。
Open JTalkはラズパイにインストールされたコマンドとして使用します。そこでPythonプログラムとOpen JTalk間で情報をやり取りするために、「標準入力、標準出力」を連結してデータをやり取りする「パイプ」について説明します。
また、本機の解説ではpyttsx3の使用例として英文を読み上げ音声ファイルを作成するプログラム(p7.py)も作成します。

 

2. OpenJTalkでネットワークに依存しない発話プログラム

Open JTalkを使って音声ファイルを作成する例をPythonプログラム「p6.py」を作りながら説明します。

2.1. 本プログラムの構成

作成するプログラム「p6.py」は、発話するテキストをサーバに送り音声ファイルを作成する「p5.py」とほぼ同じです。

control-device-with-raspberrypi-04-02

システム構成

 

p5.pyと今回のプログラムの異なる点は発話するテキストの送信先がGoogle gTTSか、Open JTalkかの違いです。
Open JTalkにテキストを送るためには「パイプ」と呼ばれるプログラム間でデータをやり取りする仕組みを利用します。

2.2. 準備

Open JTalkをインストールします。最低限必要なパッケージは次の3つです。

パッケージ 機能
open-jtalk 音声合成エンジン
open-jtalk-mecab-naist-jdic 辞書
hts-voice-nitech-jp-atr503-m001 音響モデル

辞書にはテキストの文節を判断する情報や読み仮名などが登録されています。新語などを間違って読み上げてしまうケースはこの辞書が原因です。
音響モデルは音声の元になる情報であり「声色」に相当します。音響モデルを変更することで年齢・性別・感情などが変わります。

ターミナルから以下のコマンドを入力してください。

sudo apt install open-jtalk open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001

コマンドは長く、環境によって複数行に見えるかもしれませんが実際は改行なしの1行です。

2.3. Pythonプログラム p6.py

以下が、Open JTalkを使って音声ファイルを作成するプログラムです。p6.pyの名前で保存してください。

#!/usr/bin/env python

import sys
import os
import datetime
import subprocess


def main():
    
    if len(sys.argv) < 2 or sys.argv[1] == "":
        return 1
    voice_file = sys.argv[1]
    
    message = create_message()
    print("読み上げるテキスト: 「" + message + "」")
    
    code = create_voice_file(message, voice_file)
    return code


def create_message():
    
    now = datetime.datetime.now()
    message = now.strftime("%Y年%-m月%-d日") + "、"
    
    day_of_week = ["月", "火", "水", "木", "金", "土", "日"]
    message = message + day_of_week[now.weekday()] + "曜日、"
    
    message = message + now.strftime("%-H時%-M分%-S秒") + "です。"
    
    return message


def create_voice_file(message, voice_file):
    
    if os.path.exists(voice_file):
        os.remove(voice_file)
    
    cmd = ["open_jtalk"]
    
    cmd += ["-x", "/var/lib/mecab/dic/open-jtalk/naist-jdic"]
    cmd += ["-m", "/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice"]
    
    cmd += ["-r", "1.0"]
    cmd += ["-ow", voice_file]
    
    proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
    proc.stdin.write(message.encode())
    proc.stdin.close()
    proc.wait()
    
    return 0


if __name__ == "__main__":
    sys.exit(main())

2.4. プログラムの説明

パート3のサーバで音声ファイルを作成するプログラム「p5.py」との違いは、関数「create_voice_file()」内の発話するテキストを送信する部分がGoogle gTTSからOpen JTalkに変わった点です。
Google gTTSはgttsライブラリ、Open JTalkはコマンドの呼び出しなのでimport文も
from gtts import gTTS
から
import subprocess
に変える必要があります。

Open JTalkはRaspberry Pi OSにインストールされるコマンドです。そのため先にターミナル上でOpen JTalkを扱う方法を理解しておくのがおすすめです。

Open JTalkはopen_jtalkコマンドで起動します。標準的な使用方法では、ターミナルから次のようなコマンドを入力します。

open_jtalk -x /var/lib/mecab/dic/open-jtalk/naist-jdic -m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice -r 1.0 -ow voice.wav

コマンドは長く、環境によっては複数行に見えるかもしれませんが、実際は改行なしの1行です
9個の文字列がスペースで区切られています。それぞれの意味は以下の通りです。

文字列 意味
open_jtalk コマンド名
-x 続く文字列は「辞書のあるディレクトリ」
/var/lib/mecab/dic/open-jtalk/naist-jdic 辞書のあるディレクトリ
-m 続く文字列は「音響モデル
/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice 音響モデル
-r 続く文字列は「発話の速度」
1.0 発話の速度は標準
-ow 続く文字列は「音声ファイル名」(wavファイルのみ)
voice.wav カレントディレクトリに「voice.wav」の名前で保存

-xなどハイフンで始まる文字列は、続く文字列が何を意味するものかを表します。

ターミナル上でコマンドを入力するとopen_jtalkは入力待ちの状態になります。テキスト(日本語)を入力してエンターキーを押すと入力待ちの状態が解除され、コマンドは終了します。
同時に入力したテキストが音声合成され-owの後に指定したファイルに出力されます。
ここで入力したテキストはopen_jtalkコマンドの「標準入力」と呼ばれる場所に送られ処理されます。

ここまでが、ターミナル上でopen_jtalkコマンドを使用する場合の説明です。本プログラムではcreate_voice_file()関数で上記を行っているイメージです。
具体的には前述のターミナル上で入力した1行のコマンドを、スペース位置で分割して配列にしsubprocess.Popen()に渡しています。コマンドの呼び出し方法はパート2で説明したsubprocess.call()関数と変わりません。
今回作成するPythonプログラムは、subprocess.Popen()関数の2番目の引数stdin(標準入力)の扱いがポイントとなります。
subprocess.Popen()は後ほど『subprocessとPopenを使ってPythonと他のプログラムを連携させる』で解説します。

2.5. プログラムの実行と検証

作成したアプリケーションの実行にはターミナルを使用します。
p6.pyのあるディレクトリに移動し、以下のコマンドを入力してください。また音声ファイルの確認にp4.pyを使用するので同じディレクトリ内に配置します。

./p6.py voice.wav

Open JTalkはwavファイルにのみ対応するので、出力する音声ファイルの名称もwavファイルであることが分かるように変更します。
現在のディレクトリ内に「voice.wav」が作成されたはずです。音声ファイルを再生するにはターミナル上で次のように入力します。

./p4.py voice.wav

作成された音声ファイルの内容は、サーバで音声ファイル作成するプログラム「p5.py」と同様
「2021年2月2日、火曜日、10時43分24秒です。」
といったp6.pyを起動した時点の日付時刻です。

 

3. subprocessとPopenを使ってPythonと他のプログラムを連携させる

Raspberry Pi OSのベースとなるLinuxでは、単一の機能を持つ小さなプログラムを連携することで目的の処理を実現するという手法が好まれます。
この手法を実現できるのは、プログラム同士を連携させるインターフェースが明確に定義され、個々のプログラムがそのインターフェースを適切に実装しているためです。
必ずしもこのインターフェースに従う必要はありませんが、特別な理由がない限り作成するプログラムはこのインターフェースを適切に実装しておくことをお勧めします。
本記事でも音声ファイルを再生するプログラム「p4.py」や、音声ファイルを作成する「p5.py」「p6.py」は連携を意識してインターフェースを実装しています。

プログラム同士が連携するためのインターフェースは、以下の5つが用意されています。

control-device-with-raspberrypi-04-03

プログラム同士が連携するためのインターフェース

 

インターフェース 機能
コマンドライン引数 プログラムの起動時に処理条件などを示す文字列を渡す
終了ステータス プログラムの終了時に処理結果を示す数字を返す
標準入力(stdin) 実行中のプログラムに文字列を渡す
標準出力(stdout) 実行中のプログラムから文字列を受け取る
標準エラー(stderr) 実行中のプログラムから文字列を受け取る(例外時)

コマンドライン引数終了ステータスは既に使用しました。
残る標準入力/標準出力/標準エラーは「パイプ」と呼ばれる非常に重要な概念です。
例えばプログラムAの標準出力とプログラムBの標準入力を連結すれば、プログラムAが実行中に出力したメッセージをプログラムBで受け取れます。
パイプは適切につなぎ合わせることで、プログラム間の連携を手軽にするインターフェースとして使用できます。
標準出力/標準エラーと文字列を出力するパイプが2本ありますが、それぞれ用途が異なります。
標準出力は「処理が正常に行われた時に出力する文字列」、標準エラーは「実行途中で問題が発生(例外)した際のエラーメッセージ」を送出するパイプです。
これによって、ほかのプログラムの標準出力/標準エラーと連結するプログラムは正常時の処理結果と、異常時のエラーメッセージを意識して分離することなく使用できます。

control-device-with-raspberrypi-04-04

パイプの概念

 

Pythonでは、上記5つのインターフェースを扱う関数がsubprocessライブラリに用意されています。(コマンドライン引数終了ステータスはsysライブラリでも扱えます)パート2からはその中の1つであるsubprocess.call()関数を既に使用しています。
subprocess.call()は引数に指定されたコマンド(配列)を実行し、コマンドの処理が終了した後に制御が戻ります。コマンドに続いて引数に
stdout=subprocess.DEVNULL

stderr=subprocess.DEVNULL
を指定しました。これは「呼び出したコマンドのパイプをどこに接続するのか?」を指定しています。subprocess.DEVNULLはWindowsで言うところのゴミ箱を意味しています。これによって
「呼び出したコマンドの標準出力(stdout)に送出されたデータはゴミ箱に捨てる」 「呼び出したコマンドの標準エラー(stderr)に送出されたデータはゴミ箱に捨てる」
という意味となり、コマンドが処理中に出力するメッセージを表示させないようにしています。

subprocess.call()関数は仕様によりパイプをゴミ箱以外に接続できないため、起動したコマンド(プログラム)とデータの送受信はできません。コマンド(プログラム)を起動し、通信する用途で使用できるのがsubprocess.Popen()関数です。
subprocess.Popen()はsubprocess.call()のように、呼び出したコマンドが終了するのを待ちません。また「stdin=subprocess.PIPE」のように、subprocess.PIPEを指定することで呼び出したコマンドの標準入力にアクセスできるようになります。標準出力(stdout)/標準エラー(stderr)も同様です。
作成したプログラム「p6.py」ではstdin.write()関数を使って文字列を送ります。本記事では使用していませんが標準出力/標準エラー
「outs, errs = proc.communicate()」
とすることで呼び出したコマンドが、送出するデータをそれぞれ変数「outs」「errs」に格納できます。
前述の通りsubprocess.Popen()は呼び出したコマンドの終了を待ちません。呼び出したコマンドの終了を確認する必要がある場合はwait()関数を使用します。

 

4. シェルスクリプトを使って音声ファイルの作成と再生を連携

今回もパート3と同様、音声ファイルの「作成」と「再生」を別のプログラムで処理し両者をシェルスクリプトで連携します。

4.1. 本プログラムの構成

パート3で作成したシェルスクリプトとほぼ同じです。音声ファイルの名前と起動するプログラムが異なっている点に注意してください。

4.2. シェルスクリプト s2.sh

以下が、作成したPythonのプログラムを連携するシェルスクリプトです。s2.shの名前で保存してください。

#!/bin/bash

# 音声ファイルの名前を指定
VOICE_FILE="voice.wav"

# 古い音声ファイルが残っている?
if [ -f $VOICE_FILE ]; then
    # 古い音声ファイルを削除
    rm $VOICE_FILE
fi

# 音声ファイルを作成
./p6.py $VOICE_FILE
CODE=$?
if [ $CODE -ne 0 ]; then
    # 音声ファイル作成に失敗 -> エラーメッセージ
    echo 音声ファイルの作成に失敗 code=$CODE
    exit
fi

# 音声ファイル作成に成功 -> 再生
./p4.py $VOICE_FILE

4.3. シェルスクリプトの説明

パート3で作成したシェルスクリプト「s1.sh」と異なる部分は

  • VOICE_FILE変数に設定する音声ファイルの名前を変更
  • 音声ファイルを作成するプログラムを「p5.py」から「p6.py」に変更

の2点です。

4.4. シェルスクリプトの実行と検証

作成したアプリケーションの実行にはターミナルを使用します。
s2.sh、p6.py、p4.pyのあるディレクトリに移動し、以下のコマンドを入力してください。

./s2.sh

音声ファイル「voice.wav」が新たに作成された後(既に存在していた場合はファイルの更新日時が現在の日付時刻に変わる)に再生されたはずです。

 

5. pyttsx3を使った音声ファイル作成(現状は英語のみ)

手軽に利用できるもう一つの音声合成エンジン「pyttsx3」を使用するサンプルです。

5.1. 本プログラムの構成

本記事作成時点ではpyttsx3は日本語に対応していません。正確にはpyttsx3が内部で使用している音声合成ライブラリ「espeak」が日本語に対応していないため、ラズパイでは英語の発話しかできません。
ちなみに、WindowsやMacのpyttsx3は別の音声合成ライブラリを使用するため、日本語を発話することができます。

本記事では「Hello, good to meet you.」という英文テキストから音声ファイルを作成する例を紹介します。(他プログラムとの連携は行いません)

5.2. 準備

pyttsx3を使用するにはライブラリのインストールが必要です。ターミナルを起動して以下のコマンドを入力してください。

pip install pyttsx3

5.3. Pythonプログラム p7.py

以下が、pyttsx3を使って音声ファイルを作成するプログラムです。p7.pyの名前で保存してください。

#!/usr/bin/env python

import sys
import os
import datetime

import pyttsx3


def main():
    
    voice_file = "voice.wav"
    message = "Hello, good to meet you."
    
    if os.path.exists(voice_file):
        os.remove(voice_file)
    
    engine = pyttsx3.init()
    voices = engine.getProperty("voices")
    
    engine.setProperty("voice", voices[11].id)
    engine.save_to_file(message, voice_file)
    engine.runAndWait()
    
    return 0


if __name__ == "__main__":
    sys.exit(main())

5.4. プログラムの説明

pyttsx3を使用するためにはimport文が必要です。Pythonの標準ライブラリの読み込みの後に
import pyttsx3
を記載します。
プログラム内では最初にpyttsx3.init()関数を呼び出し、pyttsx3を初期化します。
以降は

  1. 発話する言語の指定(英語)
  2. テキストを音声合成しファイルに保存する指示
  3. 指示した処理が完了するまで待機

の順に処理を進めます。
「1.」の発話する言語は音声合成ライブラリ「espeak」が提供する「言語を表す文字列」を指定する必要があります。
正確な文字列を指定するため、先にengine.getProperty(“voices”)を呼び出し、対応する言語の一覧が格納される配列を取得します。この配列の11番の要素(voices[11].id)には「英語を意味する文字列」が入っているので
「engine.setProperty(“voice”, voices[11].id)」
とすることで発話する言語を英語に設定できます。

5.5. プログラムの実行と検証

作成された音声ファイルの再生はターミナルから

./p7.py

と実行します。
作成した音声ファイルの再生は以下のように入力します。

./p4.py voice.wav

 

6. まとめ

ネットワーク接続のない環境では音声合成エンジン(ライブラリ)をラズパイにインストールしておく必要があります。
一般的に、ライブラリはほかのライブラリに依存して動作することが多く、簡単な機能を実現するにもさまざまなファイルをインストールしなければなりません。また、ライブラリのバージョンの違いなど、インストールする環境の差異によって上手く動作しないこともあります。
そのため、ラズパイ上にインストールしたライブラリだけで比較的簡単に日本語の発話が実現できるOpen JTalkは貴重な存在です。

また、ライブラリだけでなくプログラムも特定の機能を実現するため、連携を前提とした仕様で実現されています。
Linuxはこの傾向が強く、個々のプログラムは特定の機能に特化したシンプルな作りにし、それらを連携させることによって目的の動作を実現することが好まれます。そのため標準入力標準出力標準エラーを連結してデータをやり取りするパイプを扱うテクニックは必須となるでしょう。

第1回の「ラズパイで日本語テキストを読み上げる」では、ラズパイで発話することをテーマにゼロからアプリケーションを作り上げていく流れを紹介しました。
ラズパイの登場によってマイコンボードでもパソコンと同じ感覚で開発を進められるようになり、開発の規模も大きくなっています。プログラムだけでなくOSやハードウェアなど幅広い視点で設計を進めなければバランスの良いシステムはできません。
本記事では、今後もPythonを使ったシステム開発に必要なノウハウを紹介していきますので、皆さんのお役に立てれば幸いです。

 

 

今回の連載の流れ

(1)日本語テキストを音声に変換するには?
(2)あらかじめmp3に録音した音声を再生する
(3)REST(Web API)を使って発話する
(4)OpenJTalkを使ってオフラインで音声合成(今回)

アバター画像

エレクトロニクスやメカトロニクスを愛するみなさんに、深く愛されるサイトを目指してDevice Plusを運営中。

https://deviceplus.jp

RoboMaster 2019 参戦ファーストステップガイド