もがもがしいSEブログ

もがもがしく生きる

Tropo API を利用した自動音声応答装置(IVR)の構築サンプル

あけましておめでとうございます。@moga_shiです。
去年は初配属の年で、あまり業務外のことに時間を割けなかったのですが、最近社内失職気味に
なってきたので、やる気さえあればいろいろ活動できそうな感じです。やる気さえあれば。
というわけで新年最初の記事です。クラウド電話APITropo』をご紹介します。

はじめに

クラウド電話APIといえば『Twilio』を思い浮かべる人は多いのではないでしょうか。
率直に言って、国内でクラウド電話APIを利用したサービスを開発したいのであれば、
日本語ドキュメントやサポートが充実している Twilio を利用するのが現状では最適解だと思います。
ただ、Twilio を検証目的で利用する場合、以下の点で少し困ります。

  • 月額基本料金490円 + 従量料金がかかる
  • 無料のトライアルアカウントだと冒頭でデモ用メッセージが流れる

まぁ、月々にわずかな金額を支払えば済む話ではあるのですが、検証用に複数の電話番号を使いたい時や、
お遊びで一瞬だけ公開するアプリを作りたい時には少々困った話になります。
そんな時に役に立つのが Tropo です。
トライアルの Development Use に限り、基本料金や発着信の料金がかかりません。*1*2
電話番号は、東京の03番号を始めとする数十カ国の番号を5つ程度まで持つことができます。*3


使い方

開発するサービスへの接続方法により、Scripting API と Web API の2通りの使い方があります。
前者がホスティング、後者が自前のサーバを使う方法ですが、図で描くと以下のようになります。

  • Scripting API

f:id:moga_shi:20140106231841p:plain

f:id:moga_shi:20140106231848p:plain
データベースや他サービスと連携したい場合は Web API の方を使うことが多くなると思いますので
以下のサンプルでは Web API を利用した事例で説明します。

サンプル

運送会社の再配達受付サービスを考えます。
荷物の配達時に家の人が不在だった場合、不在票を入れておき、後日そこに記載されている番号に
電話をかけると、伝票番号や再配達日時を入力するように求められ・・・というおなじみのシステムです。

サンプルコードは PHP ですが、他に Ruby, Node.js, Python が使えます。
コードがアレなのですが、いい書き方があればご教示ください。

[redeliver/config.php]

<?php
$DSN = array(
        'phptype' => 'mysql',
        'username' => 'YOUR_USERNAME',
        'password' => 'YOUR_PASSWORD',
        'hostspec' => 'localhost',
        'database' => 'YOUR_DATABASE',
);
$BASE_URL = 'http://[YOUR_SERVER]/redeliver/';
$VOICE_URL = $BASE_URL . 'voice/';    // 音声ディレクトリ(音声は CeVIO Creative Studio で作成)
?>


[redeliver/index.php]

<?php
require_once 'MDB2.php';
require_once 'tropo.class.php';    // https://github.com/tropo/tropo-webapi-php
require_once 'lib/limonade.php';    // http://limonade-php.github.io/

/**
 * 初回アクセス
 */
dispatch_post('/welcome', 'app_welcome');
function app_welcome() {

        require_once 'config.php';

        $tropo = new Tropo();
        $session = new Session();

        $from = $session->getFrom();
        $callerID = $from["id"];

        ### Connect to Database ###
        $mdb2 =& MDB2::connect($DSN);
        if (PEAR::isError($mdb2)) {
                die($mdb2->getMessage());
        }

        ### Access History ###
        $res =& $mdb2->exec("INSERT INTO history (tel_no) VALUES ('$callerID')");
        if (PEAR::isError($res)) {
                die($mdb2->getMessage());
        }

        $tropo->say($VOICE_URL . "welcome.mp3");    // "こちらは再配達受付センターです。これから流れるメッセージに従って入力してください。"
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_register"));

        $tropo->RenderJson();
}

/**
 * 伝票番号入力
 */
dispatch_post('/slip_register', 'app_slip_register');
function app_slip_register() {

        require_once 'config.php';

        $tropo = new Tropo();

        $options = array("choices" => "[12 DIGITS]", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->ask($VOICE_URL . "slip_register.mp3", $options);    // "伝票番号12桁を入力してください。" ※0~9の12桁の数字なら何でも OK にしてます。
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_confirm"));

        $tropo->RenderJson();
}

/**
 * 伝票番号確認
 */
dispatch_post('/slip_confirm', 'app_slip_confirm');
function app_slip_confirm() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        $slip_no = $result->getValue();
        $options = array("choices" => "1,3", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->say($VOICE_URL . "slip_confirm.mp3");    // "伝票番号は"
        for ($i=0; $i<12; $i++) {
                $tropo->say($VOICE_URL . "0" . substr($slip_no, $i, 1) . ".mp3");
        }
        $tropo->ask($VOICE_URL . "confirm.mp3", $options);      // "ですね? よろしければ 1 を、訂正する場合は 3 を入力してください。"
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_search&slip_no=$slip_no"));

        $tropo->RenderJson();
}

/**
 * 伝票番号検索(未実装)
 */
dispatch_post('/slip_search', 'app_slip_search');
function app_slip_search() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        $choice = $result->getValue();
        $slip_no = $_GET['slip_no'];

        switch ($choice) {
                case "1":
                        /**
                         * 本当ならここで伝票番号が正しいか判定する
                         */
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
                case "3":
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_register"));
                        break;
                default:
                        $tropo->say($VOICE_URL . "wrong.mp3");    // "入力に誤りがあります。"
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=slip_register"));
                        break;
        }

        $tropo->RenderJson();
}

/**
 * 再配達日入力
 */
dispatch_post('/date_register', 'app_date_register');
function app_date_register() {

        require_once 'config.php';

        $tropo = new Tropo();

        $slip_no = $_GET['slip_no'];

        $options = array("choices" => "[4 DIGITS]", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->ask($VOICE_URL . "date_register.mp3", $options);    // "再配達の希望日を4桁で入力してください。たとえば、1月23日なら 0123 と入力してください。"
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_confirm&slip_no=$slip_no"));

        $tropo->RenderJson();
}

/**
 * 再配達日確認
 */
dispatch_post('/date_confirm', 'app_date_confirm');
function app_date_confirm() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        // 日付チェックは省略...
        $date = $result->getValue();
        $month = substr("$date", 0, 2);
        $day = substr("$date", 2, 2);
        $slip_no = $_GET['slip_no'];
        $options = array("choices" => "1,3", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->say($VOICE_URL . "date_confirm.mp3");    // "ご希望の配達日は"
        $tropo->say($VOICE_URL . $month . ".mp3");
        $tropo->say($VOICE_URL . "gatsu.mp3");    // "月"
        $tropo->say($VOICE_URL . $day . ".mp3");
        $tropo->say($VOICE_URL . "nichi.mp3");    // "日"
        $tropo->ask($VOICE_URL . "confirm.mp3", $options);    // "ですね? よろしければ 1 を、訂正する場合は 3 を入力してください。"
        $tropo->on(array("event" => "continue", "next" => "index.php?uri=time_register&slip_no=$slip_no&date=$date"));

        $tropo->RenderJson();
}

/**
 * 再配達時間帯入力
 */
dispatch_post('/time_register', 'app_time_register');
function app_time_register() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        $choice = $result->getValue();
        $slip_no = $_GET['slip_no'];
        $date = $_GET['date'];
        $options = array("choices" => "0,1,2,3,4,5,6", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        switch ($choice) {
                case "1":
                        $tropo->say($VOICE_URL . "time_register01.mp3");    // "再配達の時間帯を入力してください。"
                        $tropo->ask($VOICE_URL . "time_register02.mp3", $options);    // "時間指定がない場合は 0、午前中は 1、12時から14時は 2、14時から16時は 3、16時から18時は 4、18時から20時は 5、20時から21時は 6 を入力してください。"
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=time_confirm&slip_no=$slip_no&date=$date"));
                        break;
                case "3":
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
                default:
                        $tropo->say($VOICE_URL . "wrong.mp3");    // "入力に誤りがあります。"
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
        }

        $tropo->RenderJson();
}

/**
 * 再配達時間帯確認
 */
dispatch_post('/time_confirm', 'app_time_confirm');
function app_time_confirm() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        // 時間帯チェックは省略...
        $time = $result->getValue();
        $slip_no = $_GET['slip_no'];
        $date = $_GET['date'];
        $month = substr("$date", 0, 2);
        $day = substr("$date", 2, 2);
        $options = array("choices" => "1,3", "name" => "digit", "timeout" => 30, "mode" => "dtmf");

        $tropo->say($VOICE_URL . "time_confirm.mp3");    // "ご希望の配達日時は"
        $tropo->say($VOICE_URL . $month . ".mp3");
        $tropo->say($VOICE_URL . "gatsu.mp3");    // "月"
        $tropo->say($VOICE_URL . $day . ".mp3");
        $tropo->say($VOICE_URL . "nichi.mp3");    // "日"
        $tropo->say($VOICE_URL . "time0" . $time . ".mp3");
        $tropo->ask($VOICE_URL . "confirm.mp3", $options);    // "ですね? よろしければ 1 を、訂正する場合は 3 を入力してください。"

        $tropo->on(array("event" => "continue", "next" => "index.php?uri=complete&slip_no=$slip_no&date=$date&time=$time"));

        $tropo->RenderJson();
}

/**
 * 受付完了
 */
dispatch_post('/complete', 'app_complete');
function app_complete() {

        require_once 'config.php';

        $tropo = new Tropo();
        @$result = new Result();

        $choice = $result->getValue();
        $slip_no = $_GET['slip_no'];
        $date = $_GET['date'];
        $time = $_GET['time'];

        switch ($choice) {
                case "1":
                        /**
                         * 本当ならここでデータベースを更新する
                         */
                        $tropo->say($VOICE_URL . "complete.mp3");    // "再配達のご依頼を承りました。ありがとうございました。"
                        break;

                case "3":
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
                default:
                        $tropo->say($VOICE_URL . "wrong.mp3");    // "入力に誤りがあります。"
                        $tropo->on(array("event" => "continue", "next" => "index.php?uri=date_register&slip_no=$slip_no"));
                        break;
        }

        $tropo->RenderJson();
}

run();
?>


以上のサンプルコードを実装した電話番号が 03-4578-2345 です。よろしければお試しください。

*1:もっとも、電話回線の料金は Tropo が負担しているため、検証が終わり次第 Production Account へ移行するか番号をリリースするのが筋なのですが。

*2:アウトバウンドコールにはアカウントの認証が必要です。https://www.tropo.com/docs/webapi/quickstarts/making-call

*3:今のところ明確な上限はないようです。9つまで電話番号を増やしたところ、検証目的を説明するか番号をリリースしてくださいとの連絡を受けました。