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


## はじめに

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

[公開しているソースコードはこちらです](https://github.com/MitsuharuNakamura/twiml-server)
<br />
具体的には、

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

という 5 ステップの IVR を **Express.js** と **TwiML（Twilio Markup Language）** だけで実装します。

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

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

---

## システム概要

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



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

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

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

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

5. 通話を終了




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

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

1. **`<Gather>` で DTMF 入力を受け取る**  
   - `numDigits` と `timeout` を組み合わせて、ユーザーが入力しやすく、かつ無入力でハングしないスマートな IVR を実現します。
2. **`<Pause>` で “間” を演出**  
   - 挨拶のあとに 5 秒ポーズを入れてから `Gather` へ進むことで、ユーザーが「次に何をすればよいか」を落ち着いて理解できる自然な誘導が可能です。
3. **外部 API → 取得結果を即座に読み上げ**  
   - `async/await` で Numbers API を呼び出し、そのレスポンスを待ってから `response.say()` で読み上げ。API 呼び出しと音声合成を 1 リクエスト内で完結させることで、ユーザー待ち時間を最小限に抑えます。

これら 3 つのテクニックを組み合わせることで、シンプルながら「入力 → 外部処理 → 結果返却」までを電話 1 本で完結させるエンドツーエンド体験を実装できます。

---

## 必要な依存関係




まず、`package.json`を見てみましょう：




```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. 初期設定とミドルウェア

```javascript
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連携関数

```javascript
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）

```javascript
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）

```javascript
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(`入力された値は、${digits} です。`, { language, voice });
        // Numbers APIを呼び出し
        const apiResult = await checkExternalApi(digits);
        debug('API result: %O', apiResult);
        // 結果を読み上げ
        response.say(apiResult.data, { language, voice });
    }
    // 通話終了
    response.say('これで通話を終了します。', { language, voice });
    response.hangup();
    res.type('text/xml');
    res.send(response.toString());
});
```

ポイント：
- `req.body.Digits`にユーザーが入力した数字が格納される
- 非同期でAPIを呼び出し、結果を待ってから読み上げ
- APIの応答（英語）をそのまま日本語音声エンジンで読み上げ

### 5. サーバー起動
```javascript
app.listen(PORT, () => {
    console.log(`TwiML server running at http://localhost:${PORT}/voice`);
});
```

## TwiMLレスポンスの仕組み
TwiMLは、Twilioが理解できるXML形式の指示書です。例えば、初回ハンドラーは以下のような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>

```

## 実行方法

### 通常起動

```bash
npm start
```




### デバッグモード
```bash
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`要素を使った対話的な音声アプリケーションの実装方法です。ユーザーの入力を受け取り、それに基づいて動的にレスポンスを生成することで、インタラクティブな音声サービスを構築できます。

このコードをベースに、様々な音声アプリケーションを作ることができます。例えば：
- 音声による予約システム
- 電話での問い合わせ対応
- 音声認証システム
- インタラクティブな音声ゲーム

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