昨日から今日にかけて、0からAlexaスキル開発にチャレンジしたのでその方法をまとめていきます。
今回作成したのは、AWSのLambdaとS3を連携させて、AlexaでS3のテキストファイル内のIT用語をランダムに取り出してくるスキルです。
今回は、こちらのスキルを作った際に学んだことなどを備忘録的にまとめていきます。
作ったものはこちら。
今日はイベントで使う用のAlexaスキルをPythonとAWSで作ってましたー!
AlexaもLambdaもS3も何も知らない状態だったけど、何とか5時間くらいかけてここまで作れた!
動画はひたすらIT用語をランダムに話し続けるAlexa笑 pic.twitter.com/up9YPiECHU— かしわばゆき (@yuki_kashiwaba) August 28, 2019
0からと言いつつ、Alexaスキル開発のためのアカウント登録方法や、Alexaスキルの作成方法は基本的に解説しません。
AWS関係音サービスはすぐにUI変わっちゃうので余計な誤解を生むだけですし。
事前の準備に関しては公式のチュートリアルがわかりやすいのでそちらを参照してください!
このページです。
Alexa Skills Kit(ASK)を使ってみよう
ちなみに、PythonでAlexaスキルを書こうと思われている方は、こちらのブログもおすすめです。
naritoさんのAlexaスキル関連の記事です。
Alexa Skills Kit入門シリーズ(Python、AWS Lambda使用)
もくじ環境と連携サービス
今回使用した環境はこちらです。
・Runtime:Python3.6
・Alexaカスタムスキル
・AWS Lambda
・AWS S3
先に書いておきますが、LambdaでS3のファイル操作を行う場合、権限の割り当てが必須なので要注意。
特に、テキストファイルなどへの書き込み操作を行う場合は、デフォルトの権限が存在しないので、新たに権限を作成する必要があります。
今回ハマったポイントなので要注意です。
LambdaとS3を連携させる
こちらのnaritoさんのAlexaスキル関連の記事の4番まで進めてある前提で、LambdaとS3連携に関する話をまとめていきたいと思います。
Alexa Skills Kit入門シリーズ(Python、AWS Lambda使用)
まずは連携に使うためのS3バケットを作成します。
とりあえずこちらの公式の手順に従えばOKです。
S3 バケットを作成する方法
バケットを作成したら何か適当なテキストファイルをアップロードしておきましょう!
そもそもなぜS3連携をしたいのかという話なんですが、Lambdaはステートレスで状態を保持できないためです。
つまり、Lambda単体では、データを保存したりとか、ユーザーの状態に合わせて処理を変えたりできないんですね。
ハマったところとポイント
まず、LambdaとS3を連携させる際には、きちんと権限を設定する必要があります。
これ、あんまりどの記事にも書いてなかったんで困りました。
常識なんでしょうか。
僕はAWSの知識はほぼ皆無だったので一番苦戦したところです。
もう一つは、botoモジュールを使ってs3のオブジェクトを呼び出す際に、読み込み用と書き込み用で記法が違う点も注意が必要です。
これは読み込み用。
s3 = boto3.client(‘s3’)
こっちが書き込み用。
s3 = boto3.resource(‘s3’)
まとめ
とりあえず全文はります。
できれば詳細に解説したかったのですが、よく考えたらほかの記事に書いてあることばかりなので割愛します。
Alexaスキルの開発に取り掛かろうとドキュメントを読み始めてからここまでで、大体5~6時間くらいでできました。
AWS周りはちょっと厄介ですが、Alexaスキルに使ったPythonのコードはどれも入門書レベルのものだけだったので、Alexaスキル開発は初心者にもおすすめかなと思います。
import boto3 import boto3 import os import sys import uuid import random class BaseSpeech: """シンプルな、発話するレスポンスのベース""" def __init__(self, speech_text, should_end_session, session_attributes=None): """初期化処理 引数: speech_text: Alexaに喋らせたいテキスト should_end_session: このやり取りでスキルを終了させる場合はTrue, 続けるならFalse session_attributes: 引き継ぎたいデータが入った辞書 """ if session_attributes is None: session_attributes = {} # 最終的に返却するレスポンス内容。これを各メソッドで上書き・修正していく self._response = { 'version': '1.0', 'sessionAttributes': session_attributes, 'response': { 'outputSpeech': { 'type': 'PlainText', 'text': speech_text }, 'shouldEndSession': should_end_session, }, } # 取り出しやすいよう、インスタンスの属性に self.speech_text = speech_text self.should_end_session = should_end_session self.session_attributes = session_attributes def simple_card(self, title, text=None): """シンプルなカードを追加する""" if text is None: text = self.speech_text card = { 'type': 'Simple', 'title': title, 'content': text, } self._response['response']['card'] = card return self def build(self): """最後にこのメソッドを呼んでください...""" return self._response class OneSpeech(BaseSpeech): """1度だけ発話する(ユーザーの返事は待たず、スキル終了)""" def __init__(self, speech_text, session_attributes=None): super().__init__(speech_text, True, session_attributes) class QuestionSpeech(BaseSpeech): """発話し、ユーザーの返事を待つ""" def __init__(self, speech_text, session_attributes=None): super().__init__(speech_text, False, session_attributes) def reprompt(self, text): """リプロンプトを追加する""" reprompt = { 'outputSpeech': { 'type': 'PlainText', 'text': text } } self._response['response']['reprompt'] = reprompt return self def hello(): """ハローと言っておわり""" return OneSpeech('ハロー、ハロー、ハロー').build() def welcome(): #バケットとファイル名 bucket_name = '' file_name = '○○.txt' #ファイル読み込み用のオブジェクト読み込み s3 = boto3.client('s3') #テキストファイルの取得 response = s3.get_object(Bucket=bucket_name, Key=file_name) #ランダムな数字を取得 body = response['Body'].read() bodystr = body.decode('utf-8') nums = bodystr.split(',') length = len(nums) - 1 r = random.randint(0, length) num = nums.pop(r) #かるたファイルの取得 karuta_name = 'karuta.txt' response = s3.get_object(Bucket=bucket_name, Key=karuta_name) body = response['Body'].read() bodystr = body.decode('utf-8') karuta = bodystr.split(',') n = int(num) karuta = karuta.pop(n) #書き込み用のオブジェクト読み込み s3 = boto3.resource('s3') #テキストを更新 numtext = ','.join(nums) file_contents = numtext obj = s3.Object(bucket_name, file_name) obj.put(Body=file_contents) if num: return QuestionSpeech(karuta).reprompt('よく聞こえませんでした').build() else: return QuestionSpeech('ゲーム終了です リセットしてください').reprompt('よく聞こえませんでした').build() def reset(): #バケットとファイル名 bucket_name = '' file_name = '○○.txt' #書き込み用のオブジェクト読み込み s3 = boto3.resource('s3') numtext = '' for i in range(26): numtext = numtext + str(i) + ',' file_contents = numtext obj = s3.Object(bucket_name, file_name) obj.put(Body=file_contents) return OneSpeech('ゲームをリセットしました').simple_card('遊んでくれてありがとう!').build() def lambda_handler(event, context): """最初に呼び出される関数""" # リクエストの種類を取得 request = event['request'] request_type = request['type'] # LaunchRequestは、特定のインテントを提供することなく、ユーザーがスキルを呼び出すときに送信される... # つまり、「アレクサ、ハローワールドを開いて」のようなメッセージ # 「アレクサ、ハローワールドで挨拶しろ」と言うとこれはインテントを含むので、IntentRequestになる if request_type == 'LaunchRequest': return welcome() # 何らかのインテントだった場合 elif request_type == 'IntentRequest': intent_name = request['intent']['name'] if intent_name == 'reading': return welcome() # 「キャンセル」「取り消し」「やっぱりやめる」等で呼び出される。組み込みのインテント elif intent_name == 'AMAZON.CancelIntent' or intent_name == 'AMAZON.StopIntent': return reset()