SlackメッセージをLambda経由でPollyを使って音声出力

## AWS Pollyを使ってみた
<img width="993" alt="SlackEmmaResponse.png" src="https://qiita-image-store.s3.amazonaws.com/0/64427/322cdd42-5a59-8e9c-9c0a-c1f9477f439b.png">

<img width="973" alt="SlackMessage.png" src="https://qiita-image-store.s3.amazonaws.com/0/64427/6d1e81c9-9bed-2c43-6e57-5e21ce396ba8.png">

https://github.com/ShotaKameyama/slack-lambda-polly

音声読み上げる人工知能Pollyがリリースされた。普段大量の文章を読まなければならないので、移動中に読めればと考えていたが、このPollyのAPIを使って普段使いできるようにしたいと思い、作った。


## 仕様

1. Slackに文章を投稿
2. Slack BotからAmazon API Gatewayに文章を投げる
3. Amazon API Gatewayを通じて、Lambda(nodejs)経由でPollyで音声処理
4. Pollyから返って来たBufferの音声ファイルをS3に保存
5. S3のURLをSlackに投稿


## 下準備
### 1. SlackのSlash Commandsを使う
まずは、SlackのSlash CommandsをActivateさせて、Tokenを取得。なんのコマンドで呼び出せるようにするか決めておく。`例： /emma`
<img width="1000" alt="Slack Integration Setting.png" src="https://qiita-image-store.s3.amazonaws.com/0/64427/6aab3dfb-222a-2cd4-d921-f9ecafe2cf36.png">


### 2. AWS上にS3のバケットとIAMロールの作成。
2.1. S3に音声ファイルアップロード先のバケット作成。（Pollyが一部でしか対応していないので、僕はus-east-1に作りました）
2.2. API Gateway, S3, Lambda, Polly, CloudWatchを適切な権限で作成。（ご自分の要望に応じて）


### 3. npmをローカルにインストールしておく
3.1. npmをインストール
`brew install npm`

3.2. **最新の**AWS-SDKをインストールする。**何故ならLambdaにデフォルトでインストールされているaws-sdkがPollyをサポートしていないから。**2.7.10が最新（2016年12月2日現在）
`npm install aws-sdk@2.7.10`


### 4. 自分の好みの声を探しておく笑
AWSコンソールのPollyで音声出力させる際の声をチェックしておく。また、どの言語がサポートされているのかも同時にチェックしておいてください。


## コードを書く
今回書いたコード。因みにこのコードは英国英語対応の音声なので、日本語を試したい方はMizukiさんを選びましょう。

```index.js

'use strict';
/* ファイル名に現在時刻をつける用のlibrary
   npm install date-utils
*/
require('date-utils');

/* 注意：最新のAWS SDKにしないとPollyは機能しないので、AWS SDKは別途アップする。
   npm install aws-sdk@2.7.10
   2016年12月2日現在
*/
const AWS = require('aws-sdk');
const qs = require('querystring');
const kmsEncryptedToken = process.env.kmsEncryptedToken;
let token;

var polly = new AWS.Polly({apiVersion: '2016-06-10'});
var s3 = new AWS.S3({apiVersion: '2006-03-01'});



function processEvent(event, callback) {
    const params = qs.parse(event.body);
    const requestToken = params.token;
    if (requestToken !== token) {
        console.error(`Request token (${requestToken}) does not match expected`);
        return callback('Invalid request token');
    }

    // Slackから取得したパラメータ達。必要に応じて使ってください。
    const user = params.user_name;
    const command = params.command;
    const channel = params.channel_name;
    const commandText = params.text;

    // Pollyの初期設定とSlackから取得したテキスト
    const pollyParams = {
      OutputFormat: 'mp3', // 音声フォーマット
      Text: commandText, // スラックから呼び出したメッセージ
      VoiceId: 'Emma', // 読ませたい音声。これは英国英語
      TextType: 'text'
    };

    // Pollyでテキストから音声に変換
    polly.synthesizeSpeech(pollyParams, function(err, data) {
      if (err) console.log(err, err.stack); // an error occurred
      else
          console.log(data);           // successful response
          // 現在の日付時刻取得
          var dt = new Date();
          var timenow = dt.toFormat("YYYYMMDDHH24MISS");

          // s3にPutする用のパラメータ
          var s3Params = {
            ACL: 'public-read', //S3権限
            Bucket: 'YOUR-BACKET', //アップロードバケット先
            Key: `YOUR-FILE${timenow}.mp3`, //S3のファイル名
            Body: new Buffer(data.AudioStream) // AudioStreamの取得
          };

          // s3にputする
          s3.putObject(s3Params, function(err, data) {
            if (err) console.log(err, err.stack); // an error occurred
            else     console.log(data);           // successful response
            // Slackにコールバック
            callback(null, `Hi, ${user}. I read your text and recorded my voice at https://s3.amazonaws.com/${s3Params.Buket}/${s3Params.Key} The original text is "${commandText}"`);

          });
    });

}

// LambdaでSlack用のBluePrintを選ぶと付いてくる。これはそのまま使う。
exports.handler = (event, context, callback) => {
    const done = (err, res) => callback(null, {
        statusCode: err ? '400' : '200',
        body: err ? (err.message || err) : JSON.stringify(res),
        headers: {
            'Content-Type': 'application/json',
        },
    });

    if (token) {
        // Container reuse, simply process the event with the key in memory
        processEvent(event, done);
    } else if (kmsEncryptedToken && kmsEncryptedToken !== '<kmsEncryptedToken>') {
        const cipherText = { CiphertextBlob: new Buffer(kmsEncryptedToken, 'base64') };
        const kms = new AWS.KMS();
        kms.decrypt(cipherText, (err, data) => {
            if (err) {
                console.log('Decrypt error:', err);
                return done(err);
            }
            token = data.Plaintext.toString('ascii');
            processEvent(event, done);
        });
    } else {
        done('Token has not been set.');
    }
};

```

## Zip化する
作ったファイルとライブラリをzip化
` zip -r myfunc.zip index.js node_modules;`


## LamdaのSlackのBluePrintを使ってAmazon API GatewayとLambdaを設定する

1. AWSコンソールにログインした後、Pollyがサポートされているリージョン（us-east-1）のLambdaにアクセスする。（AWSコンソールで、Pollyに触る必要はなし）
2. LamdaにSlackのBluePrintを選択。
3. Slackで使ったTokenをEnvironment Variables（kmsEncryptedToken）に入れて、暗号化しておく。
4. Roleなど適切に設定した上で、Lambdaに先ほど作成したZipファイルをアップロードする
5. API Gatewayで作ったURLをSlackのIntegration SettingのURLに貼る。

これで、slackに`/emma MESSAGE`と書くと、トップ画像のようにメッセージが返ってくる。(選んだ音声がたまたまEmmaだったので、エマワトソンの画像を貼った笑）


## つまずいた点
だいぶ初歩的なところでつまづいた感が・・・

- IAMのロールを付け忘れていたので、無駄に時間がかかってしまった。
- Lambda(nodejs)では、まだデフォルトでPollyのSDKがサポートされていないことにしばらく気づいていなかった。（新機能は使う際にサポートされているかどうかはチェックすべき）

これで、自分が読みたい文章をどんどん音声に変換できるので期待しているが、最大５分間しか出力できないらしい。。。