はじめに
Alexaへの音声入力をトリガーとして、その内容を特定のLINEに通知する仕組みを作ってみました。
個人的にミソだと思っているのは、一度に複数の人に通知を飛ばせる、というところです。
関連ツール
-
Alexa開発者コンソール
Alexa開発者コンソールにて、Alexaのスキルを作成します。
Alexa開発トレーニングの「スキル開発基礎トレーニング」の第1回~第4回に、スキル開発の最初の一歩が書かれています。 -
AWS
バックグラウンドではAWSのLambdaを使います。
今回はPythonで実装しました。 -
LINE Developers
LINE Developersでは、通知を行う公式アカウントを作成します。自分の「LINEアカウント」でログインし、作業を行います。
具体的には、
・プロバイダを作成します。
・そのプロバイダにぶら下げる形で、新規チャネルを作成します。「Messaging API」を選択します。
詳細はMessaging APIを始めように記載されています。
また、Messaging APIにはいろいろなコースがあり、そのコースによって料金が異なります。今回は無料で使いたいのでコミュニケーションプランを選択しました(参考:こちら)。 -
LINE Official Account Manager
LINE Developersで作成した公式アカウントの設定を行います。
例えば、チャンネル名やアカウントアイコンの変更などができます。
関連技術
Messaging API
概要
Messaging APIを使って、ユーザー個人に合わせた体験をLINE上で提供するボットを作成できます。
(Messaging API概要より)
作成したプログラムからLINEのAPIを実行することで、「メッセージの通知」を実現します。
今回はブロードキャストメッセージを送るのAPIをプログラム側からコールします。
今回は「関連ツール」の3.で作成した通知用アカウントと友達になったLINEユーザに一括で通知を送信したいので、このAPIを利用することとします。
コマンドから実行してみる
自分のLINEアカウントが、今回作成した通知用のアカウントと友達になっている状態で、コマンド実行してみます。
curl -v -X POST https://api.line.me/v2/bot/message/broadcast \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {作成した通知用アカウントのMessaging APIのチャンネルアクセストークン} \
-d '{
"messages":[
{
"type":"text",
"text":"Hello, world1"
},
{
"type":"text",
"text":"Hello, world2"
}
]
}'
コマンド実行した結果、通知用のアカウントから「Hello, world1」「Hello, world2」という2通のメッセージが届きました。
ASK SDK
概要
Alexaスキルの開発には、ASK(alexa-kiills-kit) SDKを使います。
様々な言語のSDKが公開されていますが、今回はPythonのASK SDKを使います。
ASK SDK for Python
・ASK SDKのセットアップ
・初めてのスキル開発
- スキルの設定とテストを行うにて、Alexa開発者コンソールで作成したスキルと、AWS Lambdaを紐づけます。
- これにより、Alexaに話しかけると、バックエンドで自分のAWS Lambda上で処理が走ります。
- 今回は、PythonでMessaging APIを実行する処理を記載します。
制約
ですます調で内容を伝える必要がある
例えば「頭が痛いです」「朝から熱があります」といった感じです。そのため、内容によっては多少日本語としては不自然な文法で発話しないといけないケースも発生します。例えば「あとで電話してください」と伝えたい場合、「あとで電話してくださいです」と言った感じでしょうか。
発話する側が語尾を意識しないといけない、というのが現状です。
通知される文言が不自然なところで切れることもある
例えば「頭が痛いです」と話しかければ、「頭が痛い」というメッセージが通知されます。
「朝から熱があります」と話しかければ、「朝から熱があり」というメッセージが通知されます。
これは機械的に「です・ます」の直前で区切った文言をメッセージ通知に使っているからです。
日本語としては変かもしれませんが、こういうのは文法的に正しいかどうかではなく「何かが起こっている」と伝わることが大事なので…、ということでここはひとつ…。
実装
ローカル環境
Ubuntu(WSL)で行いました。
$ python --version
Python 3.14.2
$ pyenv version
3.14.2 (set by 〇〇/.python-version)
Alexa開発者コンソールにてスキル作成
Skill Invocation Name
自作スキルを起動するための文言を設定します。
例えば、「Alexa、ハローワールドを開いて」と話しかけることでスキルを起動したいのであれば、「ハローワールド」と入力すればOKです。
Intents
任意の名前のインテントを追加します。
例えば、「頭が痛い」といった、「何か起こってる」ことを通知したいのであれば、TroubbleContentIntentとか、そういう名前になるでしょうか。
作成したインテントの設定画面を開き、Intent Slotsにてスロットを追加します。任意の名前(例:trouble_content)を入力し、SLOT TYPEとしてAMAZON.SearchQueryを選択します。
これは言うなれば自由記述みたいなもので、このスロットを使うことで「頭が痛いです」「熱があります」「おつかいを頼みたいです」といった多様な形態の発話を拾えるようになります。
AMAZON.SearchQueryは元々用意されている(Built-in)のスロットで、他にも数字に特化したAMAZON.NUMBERなどがあります。
また、自分でカスタムしたスロットを事前に作り、それをインテント設定で利用する、という使い方もできます。
そして最後、Sample Utterance(サンプル発話)にて、 {trouble_content}ですと {trouble_content}ますという2つの発話サンプルを追加します。
前述の制約で書いていた内容がここでリンクします。
ですます調で話しかける必要があるのはサンプル発話でこういう指定をしているからで、trouble_content(スロットタイプ:自由記述であるAMAZON.SearhQuery)に入った文言を機械的に通知するので、発話内容によっては多少変な形の文言が通知されるということもあります。
発話内容に応じたハンドリング
発話内容によって、実行される内容をハンドリングします。
「{スキル名}を開いて」と話しかければ、Alexaに「どうしましたか?」と喋らせる、であったり、「頭が痛いです」と話しかければ、LINEの該当アカウントに通知したうえで「通知しました」とAlexaに喋らせる処理が走る、といった感じです。
話しかけた内容はJSON形式のデータにまとめられます。そのデータをプログラムから解釈します。
JSONの内容は、Alexa開発者コンソールの「テスト」タブでの入力で確認できます。
ケース①:「{スキル名}を開いて」と話しかける
このときのリクエストのJSONは↓このような感じです(抜粋)。
{
…,
"context": {
…,
"request": {
"type": "LaunchRequest",
"requestId": "amzn1.echo-api.request.abcd1234",
"locale": "ja-JP",
"timestamp": "2026-02-11T15:48:32Z",
"shouldLinkResultBeReturned": false
}
}
ここでのポイントは、「LaunchRequest」というタイプのリクエストが発行されているということです。
それをプログラムで解釈する際は↓このような感じで記載します。
class LaunchRequestHandler(AbstractRequestHandler):
def can_handle(self, handler_input):
# type: (HandlerInput) -> bool
return is_request_type("LaunchRequest")(handler_input)
def handle(self, handler_input):
# type: (HandlerInput) -> Response
speech_text = "どうしましたか?"
handler_input.response_builder.speak(speech_text).set_card(
SimpleCard("あなたの困りごとを教えてください。", speech_text)).set_should_end_session(
False)
return handler_input.response_builder.response
can_handleメソッドにて、リクエストのタイプをチェックしています。
LaunchRequestだったらtrueが返り、LaunchRequestHandlerのhandleメソッドの中身が実行されます。
ケース②:具体的な内容を話しかける(例:頭が痛いです)
このときのリクエストのJSONは↓このような感じです(抜粋)。
{
…,
"request": {
"type": "IntentRequest",
"requestId": "amzn1.echo-api.request.abcd1234",
"locale": "ja-JP",
"timestamp": "2026-02-11T15:53:59Z",
"intent": {
"name": "TroubleContentIntent",
"confirmationStatus": "NONE",
"slots": {
"trouble_content": {
"name": "trouble_content",
"value": "頭が痛い",
"confirmationStatus": "NONE",
"source": "USER",
"slotValue": {
"type": "Simple",
"value": "お腹が痛い"
}
}
}
}
}
}
ここでのポイントは、「IntentRequest」というタイプのリクエストが発行されていて、かつ、そのインテントの名前が「TroubleContentIntent」であるということです。
内部的にどんなロジックでこのインテントが選ばれているのかを我々が意識する必要性は無いでしょう。
しかし、意図したインテントで処理させるための判断材料を提供する必要はあり、その判断材料となるのが、前述のインテントの発話サンプルの設定です。
また、「trouble_content」の「value」に、通知したい内容である「頭が痛い」が存在しています。
それをプログラムで解釈する際は↓このような感じで記載します。
class TroubleContentIntentHandler(AbstractRequestHandler):
def can_handle(self, handler_input):
# type: (HandlerInput) -> bool
return is_intent_name("TroubleContentIntent")(handler_input)
def handle(self, handler_input):
# type: (HandlerInput) -> Response
trouble_content = get_trouble_content(handler_input)
# 通知処理
...
return handler_input.response_builder.response
can_handleメソッドにて、インテント名をチェックしています。
TroubleContentIntentだったらtrueが返り、LaunchRequestHandlerのhandleメソッドの中身が実行されます。
通知したい内容を取得
上記JSONのデータ構造をたどっていき、通知したい内容を取り出します。
↓のように書くことで、通知したい内容を取得できました。
from ask_sdk_core.handler_input import HandlerInput
def get_trouble_content(handler_input: HandlerInput):
slots = handler_input.request_envelope.request.intent.slots
return slots['trouble_content'].value
Messaging APIを実行
コマンドでMessaging APIを実行しましたが、それをプログラムから実行します。
もっと良い書き方があるのだと思いますが、今回は↓こんな感じで書きました。
import json
import os
from urllib.request import urlopen
from urllib.request import Request
class MessageSender():
def broadcast_message(self, content: str):
try:
req = self._create_request(self._create_notification_content(content), 'POST_MESSAGE_BROADCAST_URL')
with urlopen(req, timeout=10) as response:
return self._create_response(response.status, response.reason)
except Exception as e:
return self._create_response(-1, str(e))
def _create_headers(self):
return {
'Content-Type': 'application/json',
'Authorization': f'Bearer {os.environ['CHANNEL_ACCESS_TOKEN']}'
}
def _create_body(self, notification_content: str):
return {
'messages': [{
'type': 'text',
'text': notification_content
}]
}
def _create_notification_content(self, content: str):
return f"通知内容:{content}"
def _create_request(self, notification_content: str, target_url_environ_key: str):
return Request(
url=os.environ[target_url_environ_key],
data=json.dumps(self._create_body(notification_content)).encode('utf-8'),
headers=self._create_headers(),
method='POST'
)
def _create_response(self, status: int, reason: str):
return {
'status': status,
'reason': reason
}
なお、APIのURLとチャンネルアクセストークンは環境変数に出しています。
AWS Lambdaにて環境変数の設定ができるのでそこに書きました。ソースコードにベタ書きしたくなかったので。
また、チャンネルアクセストークンというシークレットな値を自分の目が届くところで使いたかった、というのが「自分のAWS Lambdaをバックエンド側で動かす」理由にもなります。
zip化
Lambda開発者ガイドの「デプロイパッケージ(仮想環境)を作成するには」を参考にして作成します。都度コマンドを入力して実行するのも面倒なので、それ用のスクリプトを作ってもいいかもしれません。
まとめ
音声入力をトリガーとして、その内容をブロードキャスト通知する仕組みをお試しで作ってみました。
Alexaスキル開発のチュートリアルからはじめて、今回作りたいと思った機能はおおむね作れた気がします。
例外のcatchがExceptionのみになっている、テストコードが無い、特定の末尾で終わらないと通知内容を拾えない、たぶんもっと良い書き方はある、といった課題点はありますが、それらについては気が向いたら…。
また、今回の検証では通知用のアカウントと友達になっているLINEアカウントは自分のアカウントひとつだけなので、複数のアカウントに通知が届くことの確認は取れていませんが、ブロードキャストメッセージを送るという仕様のAPIが用意されていて、それを呼ぶ実装をしているので、複数のアカウントにも通知を飛ばせるようになっている…はずです。
ひとりごと
Alexaスキルの開発に挑戦してみましたが、その中でインテントが出てきました。
Androidの世界でもインテントという登場人物がいますが、「Alexaスキルの世界のインテントも、Androidの世界のそれと同じようなもの」、もっと言うと「実行時にどれを使うかをシステムに判断させるという意味では、明示的インテントというよりは、暗黙的インテントに似ている」と思っているのですが、実際のところどうなのでしょうか…?
参考
・http.client --- HTTP プロトコルクライアント
・AlexaからLINE Botに通知してみる(ASK+Messaging API+LINEログイン+Social API)
・AlexaとLINEをアカウントリンクしてAlexaスキルにLINEメッセージ送信機能を実装する
・LINE: 公式アカウント入門(1) - 公式アカウントを作成してみる
・AWS LambdaでPython基礎
・標準のリクエストタイプのリファレンス
