1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TwilioとExpress.jsで作る音声通話サーバー

Last updated at Posted at 2025-06-13

はじめに

Twilio をまだ触ったことがない人でも 「Twilio で購入した電話番号に着信したら、自動音声(IVR)が動く」 体験ができるように、できるだけシンプルなサンプルを用意しました。

公開しているソースコードはこちらです


具体的には、

  1. Twilio で取得した電話番号に着信があると Webhook が呼び出される
  2. 電話口で日本語の挨拶を再生
  3. プッシュボタン(DTMF)で 2 桁の数字を入力してもらう
  4. Numbers API からその数字のトリビアを取得
  5. 結果を音声合成で読み上げて通話を終了

という 5 ステップの IVR を Express.jsTwiML(Twilio Markup Language) だけで実装します。

「IVR って難しそう…」という方も、この記事を読みながらコピペすれば 30 分以内に動くところまで行けるはずです! TwilioでIVRを試すだけなら、GUIだけで構築できるStuidoもありますが、コードベースの方が用途的に向いているケースもあります。また昨今では、AIが電話応対するということを実施されている方も多いですが、意外と既存のIVRでもやりたいことが十分実現できる可能性はあります。そんな方々の後押しになればと思っています。

それでは見ていきましょう!


システム概要

このシステムは以下のような流れで動作します:

  1. 着信を受けて日本語で挨拶

  2. ユーザーに数字の入力を促す

  3. 入力された数字をNumbers APIに問い合わせ

  4. 取得したトリビアを日本語で読み上げる

  5. 通話を終了

コードのキモになる 3 ポイント

このサンプルでぜひ押さえておきたいのは以下の 3 つです:

  1. <Gather> で DTMF 入力を受け取る
    • numDigitstimeout を組み合わせて、ユーザーが入力しやすく、かつ無入力でハングしないスマートな IVR を実現します。
  2. <Pause> で “間” を演出
    • 挨拶のあとに 5 秒ポーズを入れてから Gather へ進むことで、ユーザーが「次に何をすればよいか」を落ち着いて理解できる自然な誘導が可能です。
  3. 外部 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との連携設定

  1. Twilioコンソールで電話番号を取得
  2. その番号の「Voice & Fax」設定で「A call comes in」のWebhook URLを設定
  3. ngrokなどを使ってローカルサーバーを公開し、[https://xxx.ngrok.io/voiceを指定](https://xxx.ngrok.io/voice`を指定)

まとめ

このサンプルコードでは、以下の技術要素を組み合わせています:

  • Express.js: シンプルなWebサーバー構築
  • TwiML: 音声通話フローの制御
  • async/await: 外部API呼び出しの非同期処理
  • エラーハンドリング: API失敗時の適切な処理
  • デバッグログ: 開発時の問題解決を支援

特に重要なのは、TwiMLのGather要素を使った対話的な音声アプリケーションの実装方法です。ユーザーの入力を受け取り、それに基づいて動的にレスポンスを生成することで、インタラクティブな音声サービスを構築できます。

このコードをベースに、様々な音声アプリケーションを作ることができます。例えば:

  • 音声による予約システム
  • 電話での問い合わせ対応
  • 音声認証システム
  • インタラクティブな音声ゲーム

ぜひこのコードを参考に、独自の音声アプリケーションを作ってみてください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?