Slackに入力した文字をAmazon Pollyを使ってしゃべらせる


さて、2016年12月にAWSにText-to-Speachのサービスが追加されました。数あるText-to-Speachのユースケースのの中で、やっぱりIoTと絡めたく、色々検討した結果、Slackに入力した文字列をAWS IoT経由でラズパイにメッセージを飛ばして、文字列から発話データをPollyから取得する流れを考えました。

![IMG_4532.JPG](https://qiita-image-store.s3.amazonaws.com/0/60724/d66c76ed-4e97-4aa0-b7be-93f3138de0e0.jpeg "IMG_4532.JPG")


以下が構成図です。

![Kobito.61TEBE.png](https://qiita-image-store.s3.amazonaws.com/0/60724/9a6c7872-381e-9c03-fb32-be412ef48cc0.png "Kobito.61TEBE.png")

### ざっくりとした流れ
1. Lambdaのコーディング
2. API GWの設定
3. SlackのOutgoing WebHooksの設定
4. ラズパイの設定
5. ラズパイ上で動作するアプリの実装


## Lambdaのコーディング

流れ的には、Lambdaから設定する流れになります。
Lambdaでは、Slackで入力された文字列からSlackのWebHookトリガーの文字列を除いて、Base64エンコードしたデータをAWS IoTの特定トピックにPublishするものです。
トリガー文字列は"speak"にしてます。
抽出した文字列の中身チェックなどのバリデーションは、省略しております。

Lambda Function

```index.py
import boto3
import json
import logging
import os
import base64
from urlparse import parse_qs

iot = boto3.client('iot-data')

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)


def respond(err, res=None):
    return {
        'statusCode': '400' if err else '200',
        'body': err.message if err else json.dumps(res),
        'headers': {
            'Content-Type': 'application/json',
        },
    }

def mqtt_message(text, res=None):
    return {
        'message': text
        }

def lambda_handler(event, context):
    logger.info(event)
    user = event['user_name']
    channel = event['channel_name']
    command_text = event['text']
    logger.info(command_text)
    text = command_text.split("speak ")
    logger.info(text[1])
    iot.publish(
        topic='/rasp/test',
        qos=0,
        payload=json.dumps(mqtt_message(base64.b64encode(text[1])))
        )

    return respond(None, "%s invoked %s in %s" % (user, command_text, channel ))
```

## API GWの設定

API GWの詳細設定は、SlackのOutgoing WebHooksがPOSTのようですので、POSTでアクセスできるリソースとメソッドを用意しました。
今回は、slackというリソースを作り、そこにPOSTメソッドを設定してます。

![Kobito.3EulTd.png](https://qiita-image-store.s3.amazonaws.com/0/60724/b8c2af47-e16e-21ad-2dc1-e53eaaffb3f0.png "Kobito.3EulTd.png")

統合リクエストについては、先程作成したLambdaを選択します。
設定が終わったら、忘れずに、デプロイをしましょう。
デプロイしたステージでのエンドポイントをメモしておきます。

![Kobito.2G1cfk.png](https://qiita-image-store.s3.amazonaws.com/0/60724/86ebecf3-4d4a-2f76-973d-bd2c7e8d3ed9.png "Kobito.2G1cfk.png")



## SlackのOutgoing WebHooksの設定
Botを作る上で、とても簡単なOutgoing WehHooksを使いました。
Channel、Trigger Word、URLなどを設定します。

![スクリーンショット 2016-12-11 23.07.44.png](https://qiita-image-store.s3.amazonaws.com/0/60724/05d6faf7-3a75-c643-1826-4f126c79d651.png "スクリーンショット 2016-12-11 23.07.44.png")

今後も、コマンドを増やすかもしれないので、Trigger Wordをいれました。
URLには、先程のAPI GWのエンドポイントを入力しておきます。
基本的にはこれだけですね。

## ラズパイの設定
こちらは、過去の記事を参考にしてください。

[太陽光パネルの発電量をAWS IoTとAmazon Elasticsearch Serviceを使って可視化してみる](http://qiita.com/ToshiakiEnami/items/7b4b3090f3687979d21a)

やることは、AWS IoTにThing作成、証明書発行、証明書にThingとポリシをアタッチする作業になります。

一番手間がかかったのは、Node.jsのバージョンです。過去の記事では、Pythonを使ってましたが、今回は、Node.jsにしてみました。
Node.jsでAWS IoTが求める、TLS1.2を利用するには、デフォルトに入っているNode.jsのバージョンでは実行できません。なので、最新のNode.jsを、ポーティングするのに時間がかかりました。

## ラズパイ上で動作するアプリの実装
ここまで、終えると、Slackで文字列をいれるとラズパイ側でMQTTにてメッセージを受信できるところまできました。
いやー、簡単ですね。
あとは、受信したメッセージをAmazon Pollyにいれるだけです。
Amazon Pollyには、レキシコンを用いた発音のカスタマイズができる[仕組み](http://docs.aws.amazon.com/ja_jp/polly/latest/dg/managing-lexicons.html)がありますが、今回は、シンプルに[SynthesizeSpeech API](http://docs.aws.amazon.com/ja_jp/polly/latest/dg/API_SynthesizeSpeech.html)だけ使います。
SynthesizeSpeech APIでは、音声フォーマットをmp3, ogg_vorbis, pcmから選択できます。今回は、mp3を使いました。
ラズパイ、かつ、Node.jsで、MP3を再生したかったので、色々探したのですが、Playerというモジュールが使いやすそうだったので、そちらをnpmでいれました。
あまり検証せずにやったので、もっと、きれいな実装ができたと思いますが、ひとまず、ファイルに書いて再生する形にしてます。スマートでないですが。。
あとは、AWS SDK、AWS IoT Device SDKをいれて、実装するだけです。

```client.js

var awsIot = require('aws-iot-device-sdk');
var AWS = require('aws-sdk');
AWS.config.update({region: 'us-east-1'});
var ep = new AWS.Endpoint('polly.us-east-1.amazonaws.com');
var polly = new AWS.Polly({endpoint: ep});
var fs = require('fs');
var Player = require('player');
var path = require('path');
var debug = require('debug');

// AWS IoT Device初期化
var device = awsIot.device({
  region: 'us-east-1',
  clientId: 'raspi3',
  privateKey: 'certs/pri.pem',
  clientCert: 'certs/cert.pem',
  caCert : 'certs/root-CA.crt'
});

// ConnectしたらSubscribe
device.on('connect', function() {
  console.log('connect');
  device.subscribe('/rasp/test');
});

// Messageを受け取ったときの処理
device.on('message', function(topic, payload) {
  var messObj = JSON.parse(payload);
  var decode = new Buffer(messObj.message, 'base64').toString();
  console.log('message', topic, decode);
  
  // Amazon Pollyのパラメータ
  var params = {
    OutputFormat: 'mp3',
    Text: decode,
    VoiceId: 'Mizuki',
  };
  
  // SynthesizeSpeech API
  polly.synthesizeSpeech(params, function(err, data) {
    if (err){
      console.log(err, err.stack);
    } else {
      console.log(data);
      
      // 結果が、AudioStreamにBlobで返ってくるので、ファイルに同期で書き出し
      fs.writeFileSync('test.mp3',data.AudioStream);
      
      // Playerで再生
      var songs = [
        path.join(__dirname, './test.mp3')
      ]
      new Player(songs)
        .on('playing', function(song) {
          debug(song);
        })
        .on('playend', function(song) {
          debug('Play done');
        })
        .on('error', function(err) {
          debug(err);
        })
        .play()
   }
  });
});

process.stdin.resume();
process.on('SIGINT', function() {
  console.log('receive SIGINT signal');
  process.exit();
});

```

##最後に
一番時間がかかったNode.jsのバージョンアップをいれても半日くらいで、Slackに入力した文字列を発話までできるようになりました。
色々荒削りですが、試すには充分かなと。
便利な世の中になりました。


# 免責
こちらは個人の意見で、所属する企業や団体は関係ありません。