Slack のメッセージと RaspberryPi の音声を相互変換する

適切なタイトルをどうつけようか悩んだ。

Slack から Amazon Echo の Alexa を操作したいなぁと思って作った。
かれこれ作ってから1年くらい経ってしまったけど・・、ようやくアウトプット。

# 実現したいこと
* Slack のメッセージを RaspberryPi のスピーカーから音声として出力する
* RaspberryPi のマイクへの音声を Slack へメッセージとして送信する

# 先に結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97877/6e254619-e245-e167-7768-7c6e086b5319.png)

- Slack のメッセージをスピーカー再生ができた
- マイクの入力音声を、Slack のメッセージで通知できた

※ 相手が、Alexa を想定したイメージになっているが、実際に Amazon Echo は無いので、自分の声でテストしている

# 全体構成
![Untitled (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97877/4e76bc48-109d-99cc-d38a-a949ab17503c.png)

# Slack to RaspberryPi
AWS を用いて構成する。
Slack の Webhook 受信に Amazon API Gateway を、
RaspberryPi との送受信には、AWS IoT Core を用いた。

## MQTTトピック
### Lambda → AWS IoT → RaspberryPi
下記のトピックを使用。
`raspberrypi/request/#`

テキスト→音声再生のトピックで通知。
`raspberrypi/request/speak`

### RaspberryPi → AWS IoT → Lambda
下記のトピックを使用する。
`raspberrypi/response`

処理終了後 ACK メッセージを応答送信する。

## RaspberryPi
python スクリプトをデーモン化し、systemd でサービス化。
RaspberryPi には、イヤホン端子スピーカを接続。

### セットアップ
- config を設定
- AWS IoT Core の接続情報、SSL証明書を設定
- AWS Amazon polly のアクセスキー/シークレットキーを設定

### 処理の流れ
- paho ライブラリを使用して、AWS IoT Core へ MQTT subscribe
- MQTT メッセージ受信
- AWS SDK for Python boto3 を用いて、Amazon Polly でテキスト→音声変換
    - 変換後の音声ファイルは、ファイルシステムに一時保存
- pyaudio ライブラリで、保存したmp3ファイルをスピーカー再生
    - 再生待ち処理をスレッド化、再生終了後にコールバック
- 要求元の Slack 向けに、ACKメッセージを MQTT publish
- 音声再生終了コールバック処理で、録音プロセス向けに、録音開始指示を MQTT publish（後述）

### Paho - Pyton MQTT Client
下記を参考に。
MQTT with AWS IoT Platform using Python and Paho
https://iotbytes.wordpress.com/mqtt-with-aws-iot-using-python-and-paho/
https://github.com/pradeesi/AWS-IoT-with-Python-Paho

### Amazon Polly
synthesize_speech でテキスト→音声変換
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/polly.html#Polly.Client.synthesize_speech


```python
		response = self.client.synthesize_speech(
			Text = text,
			OutputFormat = 'mp3',
			VoiceId = voice
		)
```
Text には、Slack から 受信したメッセージを設定する。

mp3 生成処理は、下記を参考に。
https://dev.classmethod.jp/cloud/aws/change-polly-voice-using-boto3/

### mp3 再生
mp3 の再生は下記を参考に。
https://deviceplus.jp/hobby/raspberrypi_entry_012/

python からの再生は下記を参考に。
https://qiita.com/Nyanpy/items/cb4ea8dc4dc01fe56918

### Tips：pygame の再生終了待ちをスレッドで行う
下記等を参考にした。
http://d.hatena.ne.jp/kadotanimitsuru/20090216/thread
https://www.raspberrypi.org/forums/viewtopic.php?t=46096

### ソースコード
下記に。
https://github.com/nilesflow/AwsIoTSpeaker/

## AWS IoT
### セットアップ
- RaspberryPi 用の things を作成
- 証明書を作成
- ダウンロードした証明書とroot証明書、エンドポイントを控えておく

## Lambda
RaspberryPi と合わせて、Python で構築。
API Gateway からの要求を AWS IoT Core にMQTT送信。
応答メッセージを Slack へ返却するために、
Lambda 関数内でしばらく ACK 待ちを行っているのがポイント。
AWS IoT Core と 統合している訳ではないので、デプロイ時に MQTT 接続用のSSL証明書類をアップロードしている。
RaspberryPi からの通知でも同じ関数を使用しているので、処理分岐している。

### セットアップ
- Lambda 関数をコンソール、または
- EC2 から AWS CLI でアップロード
- 環境変数
    - SUBSCRIBE_HOST：AWS IoT Core のエンドポイント、`yourhostname.iot.region.amazonaws.com` 等
    - SUBSCRIBE_CAROOTFILE：同ルート証明書、`certs/root-CA.crt` 等
    - SUBSCRIBE_CERTFILE：同SSL証明書、`certs/certificate.pem.crt` 等
    - SUBSCRIBE_KEYFILE：同SSL証明書鍵、`certs/private.pem.key` 等

### 処理の流れ
- API Gateway からリクエストを受信
- 呼び出し元が、Slack か RaspberryPi を判定
- paho ライブラリを使用して、AWS IoT Core へ MQTT subscribe （ACK 受信用）
    - トピックは、`raspberrypi/response`
- paho ライブラリを使用して、AWS IoT Core へ MQTT publish
    - トピックは、`raspberrypi/request/speak`
- ループ処理で sleep して、応答を待つ
- paho ライブラリで MQTT 応答メッセージ受信
- 正しい応答かどうかを確認
- API Gateway へ 応答メッセージを返却
- 応答メッセージは、slack 上にメッセージテキストとして表示される
    - 例えば、`指定されたテキストを読み上げました。invoked xxxxxx`

### 応答について
API Gateway 側でプロキシ統合を選んだ場合は、Lambda 側で実装するが、
API Gateway 側で吸収させるなら、既定の形式で返す必要がある。

下記等を参考。
https://qiita.com/taknuki/items/dd47d1c6d4190b52df9a#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9

### ソースコード
https://github.com/nilesflow/RaspberryPiClient/tree/master/Lambda/RaspberryPiClient

## API Gateway
Slack の Outgoing Webhooks を受信可能なエンドポイントを定義する。

- リソース
    - `/raspberrypi/{proxy+}` でリソース定義
    - POST
- メソッドリクエスト
    - 認可：`AuthorizationRequestToken` で Lammbda 関数を利用
    - リクエストの検証：クエリ文字列パラメータおよびヘッダーの検証
    - APIキーの必要性：false
    - URL クエリ文字列パラメータ：token を必須
    - HTTP リクエストヘッダー：Content-Type
- 統合リクエスト
    - 統合タイプ：Lambda
    - Lambda プロキシ統合の使用：しない
    - Lambda 関数は、上述の関数を指定
    - URLパスパラメータ：`method.request.path.proxy`
    - マッピングテンプレート
        - リクエスト本文のパススルー：なし
        - `application/x-www-form-urlencoded`：ソースコード参照
            - Slack のPOSTパラメータから、Lambda の引数へのマッピングを行っている
- 統合レスポンス
    - 200 のマッピングテンプレート
        - `application/json`：ステータスコードが 200 かどうかを判定する
            - Lambda の応答から、Slack への応答JSON形式へのマッピングしている
- メソッドレスポンス
    - 応答を返却できるように下記のステータスコードを定義
        - 200
        - 400
        - 500

- ステージ作成
    - 必要に応じて、`dev`、`prod` を作成

- オーソライザー
    - `AuthorizationRequestToken` を作成
        - Lambda 関数：APIGateway-Authorization
            - ソースコード参照
        - IDソース：クエリ文字列：token
        - 認可のキャッシュ：有効
        - TTL(秒)：300

- カスタムドメイン名
    - 必要に応じて作成
    - ACM 証明書も配置する

### ソースコード
https://github.com/nilesflow/RaspberryPiClient/tree/master/API_Gateway
https://github.com/nilesflow/RaspberryPiClient/tree/master/Lambda/APIGateway-Authorization

## Slack Outgoing Webhooks
slack標準の`Outgoing Webhook`を使用。

### セットアップ
- チャンネル：対象のチャンネルを指定
- 引き金となる言葉：「alexa,Alexa,ALEXA,アレクサ,あれくさ」等
- URL：API Gateway のエンドポイントと token を指定
    - `https://{your domain}/raspberrypi/speak?token={your token}`
- トークン：生成してURLパラメータに設定
- 説明ラベル：任意
- 名前をカスタマイズ：任意
- アイコンをカスタマイズする：任意

### 参考
送信 POSTパラメータ等、下記。
https://api.slack.com/custom-integrations/outgoing-webhooks

# RaspberryPi to Slack
Azure と AWS を用いて構成する。
RaspberryPi から Slack への通知には、同じく、AWS IoT Core を利用している。

## MQTTトピック
### RaspberryPi → AWS IoT → RaspberryPi
下記のトピックを使用。
`raspberrypi/request/#`

音声録音のトピックで通知。
音声再生デーモン処理終了時に発行される。
`raspberrypi/request/listen`

### RaspberryPi → AWS IoT → Lambda
下記のトピックを使用する。
`raspberrypi/response`

テキスト変換処理終了後 通知メッセージを送信する。
`raspberrypi/notify`

## Slack Incoming Webhooks
slack標準の`Incoming Webhook`を使用。

### セットアップ
- チャンネルへの投稿：対象の通知チャンネルを指定
- Webhook URL：生成、後ほど Lambda に入力
- 説明ラベル：任意
- 名前をカスタマイズ：任意
- アイコンをカスタマイズする：任意

### 参考
送信 JSON フォーマット等、下記。
https://api.slack.com/incoming-webhooks

## Lambda
同じ関数を使用。

AWS IoT Core を通じた RaspberryPi からの MQTT メッセージを処理、
Slack Incoming Webhooks の URL へ送信する。

### セットアップ
- 同じ Lambda 関数を使用
- 環境変数
    - SLACK_WEBHOOK_URL：Slack の Webhook URL、上述。

### 処理の流れ
- AWS IoT Core から統合リクエストを受信
- 呼び出し元が、Slack か RaspberryPi を判定
- 指定の JSON 形式で、HTTP リクエスト送信

### ソースコード
https://github.com/nilesflow/RaspberryPiClient/tree/master/Lambda/RaspberryPiClient

## AWS IoT
### セットアップ
- ACT を作成：RaspberryPiNotification
- ルールクエリステートメント：`SELECT * FROM 'raspberrypi/notify'`
- アクション：上述の Lambda 関数を指定
- エラーアクション：任意

## Azure Speech Service
### セットアップ
- Azure ポータル
- Cognitive Services から追加
- Speech を選択
- キーとエンドポイントを控えておく

## RaspberryPi
python スクリプトをデーモン化し、systemd でサービス化。
RaspberryPi には、USBマイクを接続。

### セットアップ
- config を設定
- AWS IoT Core の接続情報、SSL証明書を設定
- Azure Speech Service のアクセスキーを設定

### 処理の流れ
- paho ライブラリを使用して、AWS IoT Core へ MQTT subscribe
    - 接続認証のため、AWS IoT Core で発行したSSL証明書を配置
- MQTT メッセージ受信
- 音声録音処理
    - pyaudio ライブラリで音声録音
        - 音を検知したら録音データ取得
        - 無音が暫く続いたら、またはタイムアウトで終了
    - wav ファイルを生成
- 音声→テキスト変換処理
    - cognitive-services の speech-service REST API をコール
        - wav ファイル → テキスト変換処理
- 結果を MQTT publish で AWS IoT Core へ送信

### H/W情報
```
$ arecord -l
**** ハードウェアデバイス CAPTURE のリスト ****
カード 1: Device [USB PnP Sound Device], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0
```
```
$ lsusb
Bus 001 Device 004: ID 8086:0808 Intel Corp.
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
```
```
$ cat /proc/asound/modules
 0 snd_bcm2835
 1 snd_usb_audio
```
```
$ amixer -c 1 sget Mic
Simple mixer control 'Mic',0
Capabilities: cvolume cvolume-joined cswitch cswitch-joined
Capture channels: Mono
Limits: Capture 0 - 16
Mono: Capture 0 [0%] [0.00dB] [on]

$ amixer -c 1 sset Mic 60%
Simple mixer control 'Mic',0
Capabilities: cvolume cvolume-joined cswitch cswitch-joined
Capture channels: Mono
Limits: Capture 0 - 16
Mono: Capture 10 [62%] [14.88dB] [on]

$ amixer -c 1 sget Mic
Simple mixer control 'Mic',0
Capabilities: cvolume cvolume-joined cswitch cswitch-joined
Capture channels: Mono
Limits: Capture 0 - 16
Mono: Capture 10 [62%] [14.88dB] [on]
```

### 参考
音声録音処理は、下記処理を参考にさせていただいた。
https://qiita.com/mix_dvd/items/dc53926b83a9529876f7

Azure Cognitive Services は、
https://docs.microsoft.com/ja-jp/azure/cognitive-services/speech-service/rest-apis

### ソースコード
https://github.com/nilesflow/AwsIoTListener

# 参考
こちらの方が簡単そう・・。
https://qiita.com/miya236a/items/4f56f5b3dd3d3e6a3f8e

こちらの方とは似てる気がします・・。
https://hacknote.jp/archives/39454/