はじめに
Twilio をまだ触ったことがない人でも 「Twilio で購入した電話番号に着信したら、自動音声(IVR)が動く」 体験ができるように、できるだけシンプルなサンプルを用意しました。
公開しているソースコードはこちらです
具体的には、
- Twilio で取得した電話番号に着信があると Webhook が呼び出される
- 電話口で日本語の挨拶を再生
- プッシュボタン(DTMF)で 2 桁の数字を入力してもらう
- Numbers API からその数字のトリビアを取得
- 結果を音声合成で読み上げて通話を終了
という 5 ステップの IVR を Express.js と TwiML(Twilio Markup Language) だけで実装します。
「IVR って難しそう…」という方も、この記事を読みながらコピペすれば 30 分以内に動くところまで行けるはずです! TwilioでIVRを試すだけなら、GUIだけで構築できるStuidoもありますが、コードベースの方が用途的に向いているケースもあります。また昨今では、AIが電話応対するということを実施されている方も多いですが、意外と既存のIVRでもやりたいことが十分実現できる可能性はあります。そんな方々の後押しになればと思っています。
それでは見ていきましょう!
システム概要
このシステムは以下のような流れで動作します:
-
着信を受けて日本語で挨拶
-
ユーザーに数字の入力を促す
-
入力された数字をNumbers APIに問い合わせ
-
取得したトリビアを日本語で読み上げる
-
通話を終了
コードのキモになる 3 ポイント
このサンプルでぜひ押さえておきたいのは以下の 3 つです:
-
<Gather>
で DTMF 入力を受け取る-
numDigits
とtimeout
を組み合わせて、ユーザーが入力しやすく、かつ無入力でハングしないスマートな IVR を実現します。
-
-
<Pause>
で “間” を演出- 挨拶のあとに 5 秒ポーズを入れてから
Gather
へ進むことで、ユーザーが「次に何をすればよいか」を落ち着いて理解できる自然な誘導が可能です。
- 挨拶のあとに 5 秒ポーズを入れてから
-
外部 API → 取得結果を即座に読み上げ
-
async/await
で Numbers API を呼び出し、そのレスポンスを待ってからresponse.say()
で読み上げ。API 呼び出しと音声合成を 1 リクエスト内で完結させることで、ユーザー待ち時間を最小限に抑えます。
-
これら 3 つのテクニックを組み合わせることで、シンプルながら「入力 → 外部処理 → 結果返却」までを電話 1 本で完結させるエンドツーエンド体験を実装できます。
必要な依存関係
まず、package.json
を見てみましょう:
{
"name": "twiml-server",
"version": "1.0.0",
"dependencies": {
"axios": "^1.9.0",
"debug": "^4.4.1",
"express": "^5.1.0",
"twilio": "^5.7.0"
}
}
主要な依存関係は以下の通りです:
- express: Webサーバーフレームワーク
- twilio: TwiMLレスポンスを生成するためのSDK
- axios: Numbers APIへのHTTPリクエスト用
- debug: 開発時のデバッグログ出力用
コードの詳細解説
1. 初期設定とミドルウェア
const express = require('express');
const axios = require('axios');
const { twiml } = require('twilio');
const debug = require('debug')('twiml:server');
const app = express();
const PORT = 5555;
// Twilioが送ってくるPOSTデータをパース
app.use(express.urlencoded({ extended: true }));
ここでのポイント:
-
debug('twiml:server')
で名前空間付きのデバッグログを設定 -
express.urlencoded()
でTwilioからのapplication/x-www-form-urlencoded
形式のデータをパース
2. 外部API連携関数
async function checkExternalApi(digits) {
try {
const req_url = `http://numbersapi.com/${digits}`;
const res = await axios.get(req_url);
debug('API request succeeded');
return { success: true, data: res.data};
} catch (err) {
debug('API request failed: %s', err.message);
return { success: false, data: 'リクエストは失敗しました。' };
}
}
この関数の特徴:
- Numbers APIは
http://numbersapi.com/{数字}
の形式でアクセス - エラーハンドリングを実装し、失敗時は日本語のエラーメッセージを返す
- デバッグログで成功/失敗を記録
3. 初回通話ハンドラー(POST /voice)
app.post('/voice', async (req, res) => {
// デバッグ情報の出力
debug('--- Incoming Request ---');
debug('Headers: %O', req.headers);
debug('Body: %O', req.body);
debug('Query: %O', req.query);
debug('Params: %O', req.params);
debug('URL: %s', req.url);
debug('Method: %s', req.method);
debug('------------------------');
const response = new twiml.VoiceResponse();
const language = 'ja-JP';
const voice = 'Google.ja-JP-Chirp3-HD-Aoede';
const pause_length = 5;
// 挨拶
response.say( { language, voice }, 'こんにちは、これはテスト通話です。');
response.pause({ length: pause_length });
// 数字入力の収集
const gather = response.gather({
input: 'dtmf', // プッシュボタン入力
numDigits: 2, // 2桁まで
timeout: 5, // 5秒でタイムアウト
action: '/voice-2ndflow', // 入力後の遷移先
method: 'POST'
});
gather.say('ダイヤルパッドで好きな数字を入力してください。', { language, voice });
// タイムアウト時の処理
response.say( { language, voice }, '入力が確認できませんでした。');
response.say( { language, voice }, 'これで通話を終了します。');
response.hangup();
res.type('text/xml');
res.send(response.toString());
});
重要なポイント:
- デバッグログ: リクエストの全情報を記録(トラブルシューティング用)
-
音声設定:
Google.ja-JP-Chirp3-HD-Aoede
は高品質な日本語音声 - Gatherオブジェクト:
-
input: 'dtmf'
でプッシュボタン入力を指定 -
numDigits: 2
で最大2桁まで受け付け -
action
で次の処理エンドポイントを指定 -
フロー制御: Gatherがタイムアウトした場合、その後の
say
が実行される
4. 2次フローハンドラー(POST /voice-2ndflow)
app.post('/voice-2ndflow', async (req, res) => {
const response = new twiml.VoiceResponse();
const language = 'ja-JP';
const voice = 'Google.ja-JP-Chirp3-HD-Aoede';
const digits = req.body.Digits; // Twilioが送信する入力値
if(digits){
// 入力確認
response.say( { language, voice }, `入力された値は、${digits} です。`);
// Numbers APIを呼び出し
const apiResult = await checkExternalApi(digits);
debug('API result: %O', apiResult);
// 結果を読み上げ
response.say( { language, voice }, apiResult.data);
}
// 通話終了
response.say( { language, voice }, 'これで通話を終了します。');
response.hangup();
res.type('text/xml');
res.send(response.toString());
});
ポイント:
-
req.body.Digits
にユーザーが入力した数字が格納される - 非同期でAPIを呼び出し、結果を待ってから読み上げ
- APIの応答(英語)をそのまま日本語音声エンジンで読み上げ
5. サーバー起動
app.listen(PORT, () => {
console.log(`TwiML server running at http://localhost:${PORT}/voice`);
});
TwiMLレスポンスの仕組み
TwiMLは、Twilioが理解できるXML形式の指示書です。例えば、初回ハンドラーは以下のようなXMLを生成します:
<Response>
<Say language="ja-JP" voice="Google.ja-JP-Chirp3-HD-Aoede">
こんにちは、これはテスト通話です。
</Say>
<Pause length="5"/>
<Gather input="dtmf" numDigits="2" timeout="5" action="/voice-2ndflow" method="POST">
<Say language="ja-JP" voice="Google.ja-JP-Chirp3-HD-Aoede">
ダイヤルパッドで好きな数字を入力してください。
</Say>
</Gather>
<Say language="ja-JP" voice="Google.ja-JP-Chirp3-HD-Aoede">
入力が確認できませんでした。
</Say>
<Say language="ja-JP" voice="Google.ja-JP-Chirp3-HD-Aoede">
これで通話を終了します。
</Say>
<Hangup/>
</Response>
実行方法
通常起動
npm start
デバッグモード
npm run debug
# または
DEBUG=twiml:* node server.js
デバッグモードでは、すべてのリクエスト情報とAPI通信の詳細がコンソールに出力されます。
Twilioとの連携設定
- Twilioコンソールで電話番号を取得
- その番号の「Voice & Fax」設定で「A call comes in」のWebhook URLを設定
- ngrokなどを使ってローカルサーバーを公開し、
[https://xxx.ngrok.io/voice
を指定](https://xxx.ngrok.io/voice`を指定)
まとめ
このサンプルコードでは、以下の技術要素を組み合わせています:
- Express.js: シンプルなWebサーバー構築
- TwiML: 音声通話フローの制御
- async/await: 外部API呼び出しの非同期処理
- エラーハンドリング: API失敗時の適切な処理
- デバッグログ: 開発時の問題解決を支援
特に重要なのは、TwiMLのGather
要素を使った対話的な音声アプリケーションの実装方法です。ユーザーの入力を受け取り、それに基づいて動的にレスポンスを生成することで、インタラクティブな音声サービスを構築できます。
このコードをベースに、様々な音声アプリケーションを作ることができます。例えば:
- 音声による予約システム
- 電話での問い合わせ対応
- 音声認証システム
- インタラクティブな音声ゲーム
ぜひこのコードを参考に、独自の音声アプリケーションを作ってみてください!