Thursday, 30 September, 2021 UTC


Summary

背景
Twilioは電話に関する機能を多岐にわたって提供しています。留守番電話検出機能(Answer Machine Detection、以下「AMD」)もその 1つです。AMDは、着信側を特定し、それに合わせて通話フローを調整できる機能です。AMDを使えば、音声APIからの着信を人間、留守番電話、ファックスのどちらが対応したかを判断できます。この情報に基づいた通話の更新処理や通話終了後の処理の分岐を実装できます。
このチュートリアルでは、AMDの基本機能や設定方法だけでなく、AMDを使った配達リマインドシステムの作成方法までを紹介します。
目標
このチュートリアルを最後まで進めると、AMDの基本機能、設定方法を実践的に学べるとともに、Node.jsを使った配達リマインドシステムを作成できます。この配達リマインドシステムでは、着信者が電話に出た場合は音声リマインドを、留守電だった場合は留守電メッセージを残したうえで以下のようなSMSメッセージを送信します。
以下の処理フローを実装します。
想定される技術知識
本稿では以下の知識を想定しています。
  • JavaScriptの基礎知識
  • Node.jsの基礎知識
必要なツール
  • 安定バージョンのNode.jsとnpm。
  • Twilioアカウント。アカウント作成方法はHelp Centerの「Twilioアカウントの作成方法」を参照してください。
  • 電話とSMSが発信できるTwilioの電話番号。電話番号の取得方法はTwilio Docsの「最初のTwilio電話番号を取得」を参照してください。
  • インストール、アカウント登録、AuthToken設定済みのngrok。詳しくはngrok公式サイトを参照してください。
  • 留守番電話が有効になっている電話番号
Twilioトライアルアカウントでは、電話をかけるとTwiMLが実行される前にトライアルである旨の短いメッセージが流れます。このチュートリアルをスムースに進めるためには、Twilio有料アカウントへのアップグレードを推奨します。詳しくはHelp Centerの「Twilio有料アカウントへのアップグレード方法」を参照してください。
AMDの基本機能
AMDはTwilio Programmable Voiceで提供される機能です。この機能は、通話が開始してから最初の数秒間に発生した音声パターンを解析します。応答者が人間の場合、発信側からの発信に対して応答者が「もしもし」などを発話し、その後、発信者の応答を待つために無音状態となる、というパターンが一般的です。
これに対し、留守番電話の場合「もしもし、こちらは...です」のように応答者の発話が継続し、無音状態はありません。AMDはこのパターンを検知し、発話の後に無音が一定期間続けば人間、音声が継続すれば留守番電話という判断をします。
AMDは米国の電話文化に合わせた実装のため、日本で使用される留守番電話の確実な検知を保証するものではありません。成功率は100%ではないことをご理解ください。
基本設定
AMDの基本機能が理解できたところで、基本設定をはじめましょう。
任意のディレクトリでamd-node-delivery-reminderディレクトリを作成してください。
amd-node-delivery-reminderに移動し、ルートディレクトリにこのプログラムの核となるmake-call.js、環境変数を保存するための.env、ダミーテータを保存するdelivery-data.jsonの3つのファイルを作成してください。
次に、必要な依存パッケージをインストールします。ターミナルを開き、amd-node-delivery-reminderのルートディレクトリで以下のコマンドをで実行してください。
npm install --save dotenv express twilio 
インストールした依存パッケージの詳細は以下のとおりです。
  • dotenv: .envファイルに定義された値を環境変数として取り込むためのパッケージ。
  • express: Node.jsで使うウェブアプリケーションサーバーフレームワーク。
  • twilio: Twilio Node Helper Library。Twilio APIに対するHTTPリクエストを、Node.jsを使って書けるようにします。
これで必要なファイルとパッケージが揃いました。
配達リマインドシステムを構築する
次に、配達リマインドシステムを実際に構築します。

環境変数を設定する

最初に、環境変数の設定をします。
Twilio Node Helper Libraryを使用するには、Twilioアカウントを一意に表す識別情報であるAccount SIDと、アカウント情報をプログラムに安全に伝達するために使用されるAuth Tokenが必要です。これらのIDを安全かつ効率的に管理するために、環境変数として保管します。
作成した.envファイルをテキストエディターで開き、以下のコードをコピーして貼り付けてください。
TWILIO_ACCOUNT_SID=XXXXX TWILIO_AUTH_TOKEN=XXXXX TWILIO_PHONE_NUMBER=XXXXX NGROK_URL=XXXXX 
XXXXXの文字列は、Twilioの認証情報のプレースホールダーを表しています。これらの認証情報を取得し、.envファイルに追加する方法を以下でご紹介します。

Account SID

ご利用されている環境によってTwilio Consoleの画面表示が異なる場合があります。
Twilio Consoleにログインし、ACCOUNT SIDの値をコピーします。.envファイルのTWILIO_ACCOUNT_SID変数にペーストしてください。

Auth Token

Account SIDの下に、AUTH TOKENがあります。値をコピーし、.envファイルのTWILIO_AUTH_TOKEN変数にペーストしてください。

Twilioの電話番号

Twilioで取得した電話番号をTWILIO_PHONE_NUMBER変数にE.164形式でペーストしてください。

ngrok URL

本項ではプログラムをローカルで動かす方法をご紹介します。ローカル環境で稼働するプログラムにTwilioのAPIがアクセスできるよう、ngrokを設定します。プログラムのNodeサーバーを実行するlocalhost:3000をウェブ上に公開します。
ターミナルで新しいウィンドウを開き、以下のコマンドを実行してください。
ngrok http 3000 
問題なくngrokが動作すると、以下のようにウェブ上に公開されたURLが表示されます。
Forwardingの右側にあるhttpsから始まるURLをコピーし、NGROK_URL変数にペーストしてください。
.envファイルを保存してください。

ダミーデータを作る

本稿でご紹介する配達リマインドシステムでは、簡単な配達に関するダミーデータをJSON形式で作成し、使用します。delivery-data.jsonを開き、以下のコードをペーストしてください。
[ { "id": 123456, "name": "Customer 1", "phoneNumber": "{あなたの電話番号}", "deliveryTime": "17-19" } ] 
このデータのphoneNumberをもとに、配達先に電話を発信します。{あなたの電話番号}に、発信先に指定したい電話番号をE.164形式で入力してください。
delivery-data.jsonファイルを保存してください。

発信の設定をする

まずは、環境変数をインポートし、依存パッケージをインスタンス化します。
make-call.jsを開き、以下のコードをファイルの1行目にコピーしてください。
require("dotenv").config() // Twilioの認証情報を環境変数として設定 const accountSid = process.env.TWILIO_ACCOUNT_SID const authToken = process.env.TWILIO_AUTH_TOKEN const outgoingPhoneNumber = process.env.TWILIO_PHONE_NUMBER const callBackDomain = process.env.NGROK_URL // 依存パッケージやダミーデータをインポート const express = require("express") const client = require("twilio")(accountSid, authToken) const deliveryData = require("./delivery-data.json") // グローバルappオブジェクトとポートを作成 const app = express() const port = 3000 // 受信したリクエストをJSONペイロードで解析するためのExpressの設定 app.use(express.json()) app.use(express.urlencoded({ extended: true })) // グローバル変数 let earliestDeliveryTime = "" let latestDeliveryTime = "" app.listen(port, () => { console.log(`Example app listening on port ${port}!`) }) 
この時点で、一度サーバーが正しく実行されるかを試してみましょう。ファイルを保存し、一つ目のターミナルのウィンドウを開き、amd-node-delivery-reminderのルートディレクトリで以下を実行してください。
node make-call.js 
問題なくサーバーが実行されると「Example app listening on port 3000!」が表示されます。メッセージが表示されたら、一度サーバーを終了してください。
次に、発信処理を実装します。Twilioで電話を発信するには、Callリソースに対してPOSTリクエストを送ります。以下のコードで、Callリソースにリクエストを送信し、発信設定を行うmakeCall関数を定義します。
グローバル変数のブロックと、app.listenのブロックの間に、以下のコードをペーストしてください。
// 発信処理 const makeCall = (data) => { earliestDeliveryTime = deliveryData[0].deliveryTime.split("-")[0] latestDeliveryTime = deliveryData[0].deliveryTime.split("-")[1] console.log("Making a call to:", data.phoneNumber) client.calls .create({ machineDetection: "DetectMessageEnd", asyncAmd: true, asyncAmdStatusCallback: callBackDomain + "/amd-callback", asyncAmdStatusCallbackMethod: "POST", twiml: "<Response><Say language='ja-JP'>Twilio Logisticsです。メッセージを取得しています。</Say><Pause length='10'/></Response>", to: data.phoneNumber, from: outgoingPhoneNumber, statusCallback: callBackDomain + "/status-callback", statusCallbackEvent: ["initiated", "ringing", "answered", "completed"], statusCallbackMethod: "POST" }) .catch(error => {console.log(error)}) } 
上記のコードを詳しく解説します。

machineDetection

machineDetectionではAMDの処理方法を指定します。machineDetectionで指定できるAMDの処理方法はEnableDetectMessageEndの二種類があります。AMDは人間の通話もしくは留守番電話を判別すると、検知の結果をAnsweredByパラメータとして返します。このAnsweredByパラメータが返されるタイミングが、指定するAMDの処理方法により異なります。
Enableは人間の通話もしくは留守番電話を特定したら、すぐにAnsweredByの値を返します。この設定は人間が電話に出た場合すぐに特定の処理を行いたい時に有効です。たとえば、人間が出た場合は直ちにエージェントに接続し、留守番電話だった場合は通話を終了するシナリオに適しています。
DetectMessageEndは、人間の通話を検知した場合はすぐにAnsweredByの値を返しますが、留守番電話を検知した場合、応答メッセージが終わってからAnsweredByパラメータで判別結果を返します。この設定は留守番電話にメッセージを残したい場合に有効です。このチュートリアルでは、DetectMessageEndを使用します。

asyncAmdasyncAmdStatusCallback

AMDでは、検知を非同期で処理できます。asyncAmdをtrueに設定すると、電話に人間、留守番電話、どちらが出たかに関わらず通話が継続します。これにより、人間が電話に出た場合、無音状態なしで通話を継続できます。falseの場合、TwilioがAMDが完了するまで通話の実行をブロックします。
このチュートリアルでは、asyncAmdtrueに設定します。asyncAmdtrueに設定する場合、AsyncAmdStatusCallbackも設定する必要があります。AsyncAmdStatusCallbackには、検知結果の送信先のコールバックURLを指定します。このチュートリアルでは、/amd-callbackというエンドポイントをプログラム内に作成し、指定します。

twiml

twimlで、AMDの検知をしている間に行う処理をTwiMLを定義します。このチュートリアルでは、「Twilio Logisticsです。メッセージを取得しています。」のメッセージを流すTwiMLを指定します。ja-JPで再生する言語を日本語に指定します。

statusCallbackstatusCallbackEvent

statusCallbackを使用すると、通話のステータスを知ることができます。このチュートリアルでは、/status-callbackというエンドポイントをプログラム内に作成し、指定します。statusCallbackを使用する場合、受け取りたいステータスをstatusCallbackEventに指定する必要があります。このチュートリアルでは、受け取り可能なすべてのステータスである、initiatedringingansweredcompletedを配列で指定します。

通話ステータスを受け取るためのエンドポイントを作成する

次に、statusCallbackから送信される通話ステータスを受け取り、出力するエンドポイントを作ります。
makeCall関数のブロックの下に、以下のコードをペーストしてください。
// 通話ステータスを出力する app.post("/status-callback", function (req, res) { console.log(`Call status changed: ${req.body.CallStatus}`) res.sendStatus(200) }) 
このコードでは、statusCallbackから送信される通話ステータスを受け取る /status-callbackエンドポイントを作成します。これにより、ステータスが変わる度に、initiatedringingansweredcompletedのどれかが出力されます。

留守番電話検知後の処理の設定するエンドポイントを作成する

次に、留守番電話を検知した際のプログラムの動作を設定します。/status-callbackエンドポイントのブロックの下に、以下のコードをペーストしてください。
// AMD判定後の処理 app.post("/amd-callback", function (req, res) { const callAnsweredByHuman = req.body.AnsweredBy === "human" const deliveryId = deliveryData[0].id const deliveryMessage = `Twilio Logisticsよりお荷物お届けのお知らせです。本日${earliestDeliveryTime}時から${latestDeliveryTime}時の間にお荷物をお届けいたします。` if (callAnsweredByHuman) { // 人間が電話に出た場合の処理 console.log("Call picked up by human") // 進行中の通話を更新し、配達リマインドメッセージを再生する client.calls(req.body.CallSid) .update({twiml: `<Response><Pause length="1"/><Say language="ja-JP">${deliveryMessage}</Say></Response>`}) .catch(err => console.log(err)) } else { // 留守番電話だった場合の処理 const smsReminder = `Twilio Logisticsよりお荷物お届けのお知らせです。本日注文番号${deliveryId}のお荷物を${earliestDeliveryTime}時から${latestDeliveryTime}時の間にお届けいたします。` console.log("Call picked up by machine") // 進行中の通話を更新し、配達リマインドメッセージを留守番電話に残す client.calls(req.body.CallSid) .update({twiml: `<Response><Pause length="1"/><Say language="ja-JP">${deliveryMessage}SMSでリマインドをお送りいたします。</Say><Pause length="1"/></Response> `}) // SMSでリマインダーを送信する .then(call => client.messages .create({body: smsReminder, from: call.from, to: call.to }) .then(message => console.log(message.sid))) .catch(err => console.log(err)) } res.sendStatus(200) }) 
上記のコードでは、/amd-callbackエンドポイントを作成し、エンドポイントで受け取ったAMDの検知結果をもとに処理を定義しています。電話に人間(human)が出た場合は、配達のリマインド音声メッセージを流します。また、それ以外の場合は留守番電話に配達のリマインド音声メッセージを残し、配達IDを含むリマインドメッセージをSMSで送ります。
最後に、app.listenブロックの下に、以下のコードをペーストしてください。
// 発信を開始 makeCall(deliveryData[0]) 
このコードで、プログラムを実行し、発信を開始します。ファイルを保存してください。

これでプログラムが完成しました!
この時点でこれまでのmake_call.jsのコードをGithubのコードと照らし合わせてみてください。
プログラムを動作検証する

人間が電話に出た場合の動作を検証する

ターミナルで、amd-node-delivery-reminderのルートディレクトリで以下を実行してください。
node make-call.js 
問題なくプログラムが動作すると、delivery-data.jsonに登録した電話番号に電話がかかってきます。
日本国外の番号を使用すると、非通知で着信する場合があります。
電話に出て、「もしもし」など発声してください。人間が電話に出たことが検知されると、「Twilio Logisticsです。メッセージを取得しています。」のメッセージの後に、「Twilio Logisticsよりお荷物お届けのお知らせです。本日17時から19時の間にお荷物をお届けいたします。」のメッセージが流れます。
ターミナルには、通話のステータスと、「Call picked up by human」が表示されます。
一度電話を切り、ターミナルでプログラムを終了させてください。

留守番電話だった場合の動作を検証する

再度ターミナルでnode make-call.jsを実行してください。
着信しても、今度は電話に出ず、着信が終わるまで待ってください。
留守番電話が検知されると、以下のSMSメッセージが送信されます。
留守番電話に残されたメッセージを再生すると、「Twilio Logisticsよりお荷物お届けのお知らせです。本日17時から19時の間にお荷物をお届けいたします。」が流れます。
いかがだったでしょうか?留守番電話検知機能は、電話の受信者の状況によって異なる対応をしたい時に便利な機能です。このプログラムをさらに拡張したい方は、TwiMLの<Gather>動詞を使って、人間が電話に出た場合はキーパッドを使って、エージェントに接続するなど、顧客に希望する対応方法を指定してもらうようにしてみてはいかがでしょうか?
Twilio Blogに投稿してみたい方や、フィードバック、登壇、勉強会のお誘いなど気軽にsnakajima[at]twilio.comまでご連絡ください。開発中のプロジェクトに関してはGithub(smwilk)を覗いてみて下さい。