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


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

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

実現したいこと

  • Slack のメッセージを RaspberryPi のスピーカーから音声として出力する
  • RaspberryPi のマイクへの音声を Slack へメッセージとして送信する

先に結果

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

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

全体構成

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

        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

ソースコード

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
  • ステージ作成

    • 必要に応じて、devprod を作成
  • オーソライザー

    • 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 リクエスト送信

ソースコード

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://qiita.com/miya236a/items/4f56f5b3dd3d3e6a3f8e

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