SensorTag のデータを Amazon Polly で読み上げる

 前回の記事では SensorTag で取得した値を AWS IoT に送信して、照度の値によって LED を点灯したり、SNSからメールを送信したりしてみましたが、今回はさらに Polly で照度の値を読み上げる音声ファイルを生成し、Raspberry Pi で再生する処理を追加してみたいと思います。

全体構成

 全体の構成は下記のようになります。

f:id:akanuma-hiroaki:20170806143016p:plain

 前回と違うのは図の右下の赤線で囲んだ部分で、Lambda、Polly、s3との連携が追加になっています。

 処理の流れとしては、 AWS IoT Rule で SNS によるメール送信を行なっていた部分の Action に Lambda の Function 呼び出しを追加し、その Function の中から Polly のAPIを呼び出して照度の値を読み上げる mp3 ファイルを生成して s3 に格納し、 presigned URL を生成して AWS IoT に Publish します。Raspberry Pi 側では新たに音声ファイルの presigned URL が Publish されるトピックにも Subscribe しておき、 presigned URL を受け取った場合はそこから mp3 をダウンロードして再生します。

Raspberry Pi での音声ファイルの再生

 まずは下記ページを参考に Raspberry Pi 上で音声ファイルの再生を試してみます。

qiita.com

 Raspberry Pi のイヤホンジャックにヘッドフォンを挿し、下記コマンドを実行してみると、すんなり再生されました。

pi@raspberrypi:~ $ aplay /usr/share/sounds/alsa/Front_Center.wav
Playing WAVE '/usr/share/sounds/alsa/Front_Center.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono

 下記のようなテスト用のコマンドも用意されているようです。

pi@raspberrypi:~ $ speaker-test -t sine -f 1000

speaker-test 1.0.28

Playback device is default
Stream parameters are 48000Hz, S16_LE, 1 channels
Sine wave rate is 1000.0000Hz
Rate set to 48000Hz (requested 48000Hz)
Buffer size range from 512 to 65536
Period size range from 512 to 65536
Using max buffer size 65536
Periods = 4
was set period_size = 16384
was set buffer_size = 65536
 0 - Front Left
Time per period = 1.409599
 0 - Front Left
Time per period = 2.563393

 また、amixer というコマンドで現在のデバイスの設定の確認や変更ができます。

pi@raspberrypi:~ $ amixer info
Card default 'ALSA'/'bcm2835 ALSA'
  Mixer name    : 'Broadcom Mixer'
  Components    : ''
  Controls      : 6
  Simple ctrls  : 1
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer scontrols
Simple mixer control 'PCM',0
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer scontents
Simple mixer control 'PCM',0
  Capabilities: pvolume pvolume-joined pswitch pswitch-joined
  Playback channels: Mono
  Limits: Playback -10239 - 400
  Mono: Playback -2000 [77%] [-20.00dB] [on]
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer controls
numid=3,iface=MIXER,name='PCM Playback Route'
numid=2,iface=MIXER,name='PCM Playback Switch'
numid=1,iface=MIXER,name='PCM Playback Volume'
numid=5,iface=PCM,name='IEC958 Playback Con Mask'
numid=4,iface=PCM,name='IEC958 Playback Default'

 下記のようにハードウェアの情報を確認することもできます。

pi@raspberrypi:~ $ cat /proc/asound/modules 
 0 snd_bcm2835
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ cat /proc/asound/cards
 0 [ALSA           ]: bcm2835 - bcm2835 ALSA
                      bcm2835 ALSA
pi@raspberrypi:~ $ 

 aplay は mp3 ファイルは再生できないので、 mpg321 を使ってみます。下記コマンドでインストールします。

pi@raspberrypi:~ $ sudo apt-get install mpg321

 下記のように mp3 ファイルを指定することで再生することができます。

pi@raspberrypi:~/sounds $ mpg321 mondo_01.mp3

Polly と連携するための Lambda Function

 まずは Lambda Function の全文を掲載しておきます。

import json
import boto3
from boto3 import Session
from boto3 import resource
from contextlib import closing

REGION         = 'ap-northeast-1'
POLLY_REGION   = 'us-east-1'
BUCKET_NAME    = 'hiroaki.akanuma.iot'
FILE_NAME_BASE = 'voices/%s_lux.mp3'
SPEECH_BASE    = '現在の照度は、%sルクスです。'
AWS_IOT_TOPIC  = '/iot/sensor_tag/voices'

session = Session(region_name = POLLY_REGION)
polly   = session.client('polly')
s3      = resource('s3')
bucket  = s3.Bucket(BUCKET_NAME)
iot     = boto3.client('iot-data')

def synthesize_speech(lux):
    speech_text = SPEECH_BASE % lux

    response = polly.synthesize_speech(
        Text         = speech_text,
        OutputFormat = 'mp3',
        VoiceId      = 'Mizuki'
    )
    
    return response['AudioStream']

def put_to_s3(audio_stream, file_name):
    with closing(audio_stream) as stream:
        bucket.put_object(
            Key         = file_name,
            Body        = stream.read(),
            ContentType = 'audio/mpeg'
        )

def generate_presigned_url(file_name):
    return boto3.client('s3').generate_presigned_url(
        ClientMethod = 'get_object',
        Params       = {'Bucket' : BUCKET_NAME, 'Key' : file_name},
        ExpiresIn    = 3600,
        HttpMethod   = 'GET'
    )

def publish_to_iot(speech_url):
    iot.publish(
        topic = AWS_IOT_TOPIC,
        qos   = 0,
        payload = json.dumps({'speech_url': speech_url})
    )

def lambda_handler(event, context):
    lux = event['lux']
    
    audio_stream = synthesize_speech(lux)

    file_name = FILE_NAME_BASE % lux

    put_to_s3(audio_stream, file_name)

    presigned_url = generate_presigned_url(file_name)

    publish_to_iot(presigned_url)

    return presigned_url

 event に照度の値が入ってくるのでまずはそれを取り出し、読み上げ用のテキストを作成したらそれを Polly の synthesize_speech メソッドに渡して、結果を AudioStream として取得します。出力フォーマットには mp3 を指定し、日本語での読み上げなので VoiceId には Mizuki を指定しています。レスポンスの中にはメタデータ等も含んでいますが、今回は使用しなかったので、 AudioStream のみを取り出しています。ちなみに Polly はまだ東京リージョンでは使用できないので、 Polly のクライアント取得時のリージョンにバージニアを指定しています。

 次に AudioStream から音声データを取得し、それを s3 に格納します。ファイル名は読み上げている照度の値を使って、それぞれの値ごとの音声ファイルを作成します。

 そして Raspberry Pi から認証なしで s3 上の音声ファイルにアクセスできるように、 presigned URL を発行して、その URL を音声ファイルの URL のやりとり用の AWS IoT トピックに Publish しています。AWS IoT Things の Shadow 更新用とは別のトピックになります。

 この Lambda Function を Rule の Action として追加します。

f:id:akanuma-hiroaki:20170806180209p:plain:w450

Raspberry Pi 側での読み上げ

 Raspberry Pi 側では音声ファイルのURLのやりとり用のトピックに Subscribe して、音声ファイルを読み上げる処理を追加します。

SPEECH_TOPIC = '/iot/sensor_tag/voices'
SOUNDS_DIR = '/home/pi/sounds/voices'

def run_speech_thread(log)
  log.info("Running speech thread.")
  Thread.new do
    begin
      MQTT::Client.connect(host: BEAM_URL) do |client|
        client.subscribe(SPEECH_TOPIC)
        log.info("Subscribed to the topic: #{SPEECH_TOPIC}")

        client.get do |topic, json|
          speech_url = JSON.parse(json)['speech_url']
          speech_uri = URI.parse(speech_url)
          speech_file = speech_uri.path.split('/').last
          speech_file_path = "#{SOUNDS_DIR}/#{speech_file}"

          unless File.exist?(speech_file_path)
            log.info("Opening URL: #{speech_url}")
            open(speech_url) do |file|
              open(speech_file_path, 'w+b') do |out|
                out.write(file.read)
              end
            end
          end

          log.info("Speaking: #{speech_file_path}")
          Open3.capture3("mpg321 #{speech_file_path}")
        end
      end
    rescue => e
      log.error(e.backtrace.join("\n"))
    end
  end
end

 MQTT でトピックに Subscribe して client.get するとそこで待ち受けるようになるため、とりあえず今回は今までの Shadow 更新用の処理とは別スレッドで音声ファイル用トピックに Subscribe して処理を行うようにしました。トピックからメッセージを受信したら、音声ファイルのURLをパースして、同名のファイルがローカルにない場合は s3 からダウンロードします。そして mpg321 コマンドを実行して音声ファイルを再生しています。

動作確認

 Lambda Function の保存や Rule での Action の追加を行ったら、Subscribeします。

$ sudo bundle exec ruby subscribe.rb

 そして SensorTag のデータの Publish を開始します。

$ sudo bundle exec ruby publish.rb

 すると、照度の値によって照明の ON/OFF が切り替わるタイミングで、音声ファイルも再生され、照度が読み上げられることになります。

まとめ

 Polly を使うことで手軽に音声ファイルを作成することができ、生成されたファイルをローカルに保存して繰り返し使うこともできますので、上手く使えばリーズナブルに音声を使ったサービスを作ることができます。今回は AWS SDK の認証情報をデバイス上におきたくなかったので、 SORACOM Beam 経由の AWS IoT の Rule から使うやり方をとりましたが、 AWS SDK から直接 Polly を使えばもっと直接的に手軽に音声ファイルを利用できると思います。

 今回のコードは下記リポジトリにも公開しています。

github.com

 ちなみに今回は下記ページを参考にさせていただきました。

qiita.com