[Amazon Lex] RaspberryPIでLexクライアントを作ってみました

2021.06.20

1 はじめに

CX事業本部の平内(SIN)です。

今回は、RaspberryPIでAmazon Lex(以下、Lex)のクライアントを作ってみました。

最初に動作している様子です。スイッチを押している間だけマイクの音声を録音し、スイッチを離したタイミングで、Lexに送信しています。

テストに使用したボットは、以下で作成したものです。

2 スイッチ

マイクのON/OFFは、スイッチで制御しています。

ブレッドボード上のスイッチは、GPIO14に接続され、1Kでプルアップされています。スイッチを押すとGNDに落ちます。

スイッチを制御しているコードです。

GPIO14を入力モードにして、HEIGH/LOWを検出しています。チャタリングを考慮して、変化が少しの間、継続することを確認して、スイッチの変化としています。

GPIO15は、GPIO14のHEIGH/LOWをLEDで表示しているものです。

# -*- coding: utf-8 -*-
"""
スイッチを制御するクラス
"""
import RPi.GPIO as GPIO
import time

# スイッチ制御クラス
class Switch:
    def __init__(self, on, off):
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(15,GPIO.OUT)
        GPIO.setup(14,GPIO.IN)
        self.__on = on
        self.__off = off
        self.__loop()

    def __loop(self):
        status = 'off'
        count = 9999                                                   
        while True:
            if(GPIO.input(14) == GPIO.HIGH):
                # OFF状態をLEDで表示する
                GPIO.output(15,GPIO.LOW)
                if(status == 'on'):
                    status = 'off'
                    count = 0
                else:
                    count += 1
            else:
                # ON状態をLEDで表示する
                GPIO.output(15,GPIO.HIGH)
                if(status == 'off'):
                    status = 'on'
                    count = 0
                else:
                    count += 1
            if(count == 5): # 状態変が0.25sec継続した場合、スイッチ変化のコールバックを起動する
                if(status == 'on'):
                    self.__on()
                else:
                    self.__off()
            time.sleep(0.05)

3 マイク

マイクは、USBに接続するタイプのサンワサプライ USBマイクロホン 単一指向性 直挿し型 MM-MCU02BKを使用しています。

OSからは、以下のように認識されています。

 $ lsusb
Bus 001 Device 004: ID 0d8c:0016 C-Media Electronics, Inc.

$ arecord -l
**** List of CAPTURE Hardware Devices ****
card 1: Microphone [USB Microphone], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

以下のコードでマイクの諸元を取得すると、チャンネル数は、1で、サンプリングレートが、44.1KHzであることが分かります。

import pyaudio

p = pyaudio.PyAudio()
info = p.get_device_info_by_index(0)
print(info)
{'index': 0, 'structVersion': 2, 'name': 'USB Microphone: Audio (hw:1,0)', 'hostApi': 0, 'maxInputChannels': 1, 'maxOutputChannels': 0, 'defaultLowInputLatency': 0.008684807256235827, 'defaultLowOutputLatency': -1.0, 'defaultHighInputLatency': 0.034829931972789115, 'defaultHighOutputLatency': -1.0, 'defaultSampleRate': 44100.0}

マイクにアクセスしているコードです。start()で録音を開始し、stop()で、wavファイルに落とします。 なお、Lexへは、16KHzで送信する必要があるので、stopのタイミングで、ffmpegを使用してサンプリングレートを変換しています。

# -*- coding: utf-8 -*-
"""
マイクから録音するクラス
"""

import pyaudio
import wave
import os
import threading
import time

class Record():
    def __init__(self, device_index,channels, sample_rate, outfile):
        self.__CHANNELS = channels
        self.__SAMPLE_RATE = sample_rate # サンプルレート
        self.__CHUNK = int(self.__SAMPLE_RATE/4) # 0.25秒ごとに取得する
        self.__FORMAT = pyaudio.paInt16
        self.__DEVICE_INDEX = device_index
        self.__OUTFILE = outfile

        self.__p = pyaudio.PyAudio()

    def start(self):
        self.__frames = []
        self.__stream = self.__p.open(format = self.__FORMAT,
            channels = self.__CHANNELS,
            rate = self.__SAMPLE_RATE,
            input =  True,
            input_device_index = self.__DEVICE_INDEX,
            frames_per_buffer = self.__CHUNK)

        self.__recording = True
        self.__thread = threading.Thread(target=self.__loop)
        self.__thread.start()

    def __loop(self):
        while(self.__recording == True):
            data = self.__stream.read(self.__CHUNK)
            self.__frames.append(data)
            print ("data size:{}".format(len(data)))

    def stop(self):
        self.__recording = False
        data = b''.join(self.__frames)
        time.sleep(0.1)

        self.__thread.join()
        self.__stream.stop_stream()
        self.__stream.close()

        # save
        file_name = "./tmp.wav"
        wf = wave.open(file_name, 'wb')
        wf.setnchannels(self.__CHANNELS)
        wf.setsampwidth(self.__p.get_sample_size(self.__FORMAT))
        wf.setframerate(self.__SAMPLE_RATE)
        wf.writeframes(data)
        wf.close()
        # 44.1KHz -> 16KHzへの変換
        os.system("ffmpeg -i ./tmp.wav -ar 16000 {}".format(self.__OUTFILE))
        os.system("rm ./tmp.wav")

    def __del__(self):
        self.__p.terminate()

4 再生

再生は、simpleaudioを使用させて頂きました。

$ sudo apt-get install -y python3-pip libasound2-dev
$ pip3 install simpleaudio
# -*- coding: utf-8 -*-
"""
WAVファルを再生するクラス
"""

import simpleaudio as sa

class Audio:
    def play(self, wav_file):
        print("audio start")
        wave_obj = sa.WaveObject.from_wave_file(wav_file)
        play_obj = wave_obj.play()
        play_obj.wait_done()
        print("audio finish.")

スピーカーの音量は、以下のコマンドで調整しています。

$ alsamixer

5 Lex

Lexへのアクセスは、boto3で行っています。
LexRuntimeService

lex-runtimepost_content() を使用すると、音声ファイルを送信することができ、accept'audio/pcm' を指定することで、レスポンスもオディオデータで受け取ることができます。
post_content(**kwargs)

# -*- coding: utf-8 -*-
"""
Amazon Lexにアクセスするクラス
"""

import boto3
from cognito import createSession # CognitoのPoolIdでSessionを取得する
import os
import wave

class LexBot:
    def __init__(self, pool_id, region):

        session = createSession(pool_id, region)
        self.__client = session.client('lex-runtime', region_name=region)
        self.__alias = "$LATEST"
        self.__username = "raspi_client"
        self.__bot_name = "OrderFlowers_jaJP"

    def post_content(self, audio_file):
        f = open(audio_file, 'rb')
        response= self.__client.post_content(
                botName = self.__bot_name,
                botAlias = self.__alias,
                userId = self.__username,
                inputStream=f,
                accept='audio/pcm',
                contentType="audio/l16; rate=16000; channels=1")
        print(response)
        audio_stream = response['audioStream'].read()
        response['audioStream'].close()
        f = wave.open(audio_file, 'wb')
        f.setnchannels(1)
        f.setsampwidth(2)
        f.setframerate(16000)
        f.setnframes(0)
        f.writeframesraw(audio_stream)
        f.close()

6 Cognito

Lexへアクセスする権限は、CognitoのプールIDに最小限のパーミッションを設定して使用しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "Lex:PostContent"
            ],
            "Resource": "arn:aws:lex:ap-northeast-1:*:bot:OrderFlowers_jaJP:$LATEST"
        }
    ]
}
# -*- coding: utf-8 -*-
"""
CognitoのPoolIdでSessionを取得する
"""

import boto3
from boto3.session import Session

def createSession(poolId, region):
    client = boto3.client('cognito-identity', region)
    resp =  client.get_id(IdentityPoolId = poolId)
    identityId = client.get_credentials_for_identity(IdentityId=resp['IdentityId'])
    secretKey = identityId['Credentials']['SecretKey']
    accessKey = identityId['Credentials']['AccessKeyId']
    token = identityId['Credentials']['SessionToken']
    return boto3.Session(aws_access_key_id = accessKey, aws_secret_access_key = secretKey, aws_session_token = token)

7 最後に

マイクの入力レベルで、発話の終わりを検出するようにすれば、いちいちスイッチを押さなくていいのですが・・・ちょっと安定させるのが難しかったので、とりあえずスイッチでやってみました。この後、スイッチ無しもやってみたいと考えています。

RasPi上の全てのコードは、下記に置きました。
https://github.com/furuya02/LexClient_made_with_RasPI

8 参考にさせて頂いたリンク


Github amazon-pollexy/lex/
[Amazon Lex] HTML+JavaScriptでLexクライアントを作ってみました(音声対応)