> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cartesia.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Twilio と統合する

> Twilio と Cartesia を統合してテキストから音声を生成し、音声通話として送信する方法。

**Twilio Programmable Voice** を **Media Streams** と組み合わせて使用することで、電話通話で **Cartesia TTS** によって生成された音声を WebSocket 経由で受信できます。このウォークスルーでは **Node.js** を使用し、Twilio のストリームを Cartesia にブリッジして TTS 音声を発信先の回線で再生する小さなサーバーを実装します。

## 前提条件

始める前に、以下のものが揃っていることを確認してください:

1. [Node.js](https://nodejs.org/en/download) がインストールされていること。
2. [Twilio アカウント](https://www.twilio.com/en-us/try-twilio)。Account SID と Auth Token が必要です。
3. [Cartesia API キー](https://play.cartesia.ai/keys)。
4. 発信先の電話番号。
5. 発信元となる Twilio の電話番号。
6. [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) (無料アカウントで構いません)。

## はじめに

<Steps>
  <Step title="プロジェクトをセットアップする">
    1. プロジェクト用に新しいディレクトリを作成し、ターミナルでそのディレクトリに移動します。
    2. 新しい Node.js プロジェクトを初期化します:
       ```bash lines theme={null}
       npm init -y
       ```
    3. 必要な依存関係をインストールします:
       ```bash lines theme={null}
       npm install twilio ws http @ngrok/ngrok dotenv
       ```
  </Step>

  <Step title="環境変数を設定する">
    プロジェクトのルートに `.env` ファイルを作成し、次の内容を追加します:

    ```sh lines theme={null}
    TWILIO_ACCOUNT_SID="your_twilio_account_sid"
    TWILIO_AUTH_TOKEN="your_twilio_auth_token"
    CARTESIA_API_KEY="your_cartesia_api_key"
    NGROK_AUTHTOKEN="your_ngrok_authtoken"
    ```

    プレースホルダーの値を実際の認証情報に置き換えてください。
  </Step>

  <Step title="メインスクリプトを作成する">
    `app.js` (または任意の名前) というファイルを作成し、次のコードを追加します:

    ```javascript lines theme={null}
    const twilio = require('twilio');
    const WebSocket = require('ws');
    const http = require('http');
    const ngrok = require('@ngrok/ngrok');
    const dotenv = require('dotenv');
    const crypto = require('crypto');

    // Load environment variables
    dotenv.config({ override: true });

    // Function to get a value from environment variable or command line argument
    function getConfig(key, defaultValue = undefined) {
      return process.env[key] || process.argv.find(arg => arg.startsWith(`${key}=`))?.split('=')[1] || defaultValue;
    }

    // Configuration
    const config = {
        TWILIO_ACCOUNT_SID: getConfig('TWILIO_ACCOUNT_SID'),
        TWILIO_AUTH_TOKEN: getConfig('TWILIO_AUTH_TOKEN'),
        CARTESIA_API_KEY: getConfig('CARTESIA_API_KEY'),
        NGROK_AUTHTOKEN: getConfig('NGROK_AUTHTOKEN'),
    };

    // Validate required configuration
    const requiredConfig = ['TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'CARTESIA_API_KEY', 'NGROK_AUTHTOKEN'];
    for (const key of requiredConfig) {
        if (!config[key]) {
            console.error(`Missing required configuration: ${key}`);
            process.exit(1);
        }
    }

    const client = twilio(config.TWILIO_ACCOUNT_SID, config.TWILIO_AUTH_TOKEN);
    ```
  </Step>

  <Step title="Cartesia TTS を設定する">
    スクリプト内には、Cartesia TTS の設定セクションがあります。必要に応じて以下の変数を設定してください:

    ```javascript lines theme={null}
    const TTS_WEBSOCKET_URL = `wss://api.cartesia.ai/tts/websocket?cartesia_version=2026-03-01`;
    const modelId = 'sonic-3';
    const voice = {
        'mode': 'id',
        // You can check available voices using the Cartesia API or at https://play.cartesia.ai
        'id': "e07c00bc-4134-4eae-9ea4-1a55fb45746b"
    };
    const partialResponse = 'Hi there, my name is Cartesia. I hope youre having a great day!';
    ```
  </Step>

  <Step title="Twilio の発信設定をする">
    Twilio のアウトバウンド/インバウンド番号を設定します:

    ```javascript lines theme={null}
    const outbound = "+1234567890"; // Replace with the number you want to call
    const inbound = "+1234567890";  // Replace with your Twilio number
    ```
  </Step>

  <Step title="メインロジックを実装する">
    `main()` 関数が全プロセスをオーケストレーションします:

    1. Cartesia TTS WebSocket に接続する
    2. TTS WebSocket をテストする
    3. Twilio WebSocket サーバーをセットアップする
    4. Twilio WebSocket 用の ngrok トンネルを作成する
    5. Twilio を使って通話を発信する

    ```javascript expandable lines  theme={null}
    let ttsWebSocket;
    let callSid;
    let messageComplete = false;
    let audioChunksReceived = 0;

    function log(message) {
      console.log(`[${new Date().toISOString()}] ${message}`);
    }

    function connectToTTSWebSocket() {
      return new Promise((resolve, reject) => {
        log('Attempting to connect to TTS WebSocket');
        ttsWebSocket = new WebSocket(TTS_WEBSOCKET_URL, {
          headers: { 'X-Api-Key': config.CARTESIA_API_KEY }
        });

        ttsWebSocket.on('open', () => {
          log('Connected to TTS WebSocket');
          resolve(ttsWebSocket);
        });

        ttsWebSocket.on('error', (error) => {
          log(`TTS WebSocket error: ${error.message}`);
          reject(error);
        });

        ttsWebSocket.on('close', (code, reason) => {
          log(`TTS WebSocket closed. Code: ${code}, Reason: ${reason}`);
          reject(new Error('TTS WebSocket closed unexpectedly'));
        });
      });
    }

    function sendTTSMessage(message) {
      const textMessage = {
        'model_id': modelId,
        'transcript': message,
        'voice': voice,
        'output_format': {
          'container': 'raw',
          'encoding': 'pcm_mulaw',
          'sample_rate': 8000
        },
        // create a new context for each message since each is a complete transcript
        'context_id': crypto.randomUUID()
      };

      log(`Sending message to TTS WebSocket: ${message}`);
      ttsWebSocket.send(JSON.stringify(textMessage));
    }

    function testTTSWebSocket() {
      return new Promise((resolve, reject) => {
        const testMessage = 'This is a test message';
        let receivedAudio = false;

        sendTTSMessage(testMessage);

        const timeout = setTimeout(() => {
          if (!receivedAudio) {
            reject(new Error('Timeout: No audio received from TTS WebSocket'));
          }
        }, 10000); // 10 second timeout

        ttsWebSocket.on('message', (audioChunk) => {
          if (!receivedAudio) {
            log(audioChunk);
            log('Received audio chunk from TTS for test message');
            receivedAudio = true;
            clearTimeout(timeout);
            resolve();
          }
        });
      });
    }

    async function startCall(twilioWebsocketUrl) {
      try {
        log(`Initiating call with WebSocket URL: ${twilioWebsocketUrl}`);
        const call = await client.calls.create({
          twiml: `<Response><Connect><Stream url="${twilioWebsocketUrl}"/></Connect></Response>`,
          to: outbound,  // Replace with the phone number you want to call
          from: inbound  // Replace with your Twilio phone number
        });

        callSid = call.sid;
        log(`Call initiated. SID: ${callSid}`);
      } catch (error) {
        log(`Error initiating call: ${error.message}`);
        throw error;
      }
    }

    async function hangupCall() {
      try {
        log(`Attempting to hang up call: ${callSid}`);
        await client.calls(callSid).update({status: 'completed'});
        log('Call hung up successfully');
      } catch (error) {
        log(`Error hanging up call: ${error.message}`);
      }
    }

    function setupTwilioWebSocket() {
        return new Promise((resolve, reject) => {
          const server = http.createServer((req, res) => {
            log(`Received HTTP request: ${req.method} ${req.url}`);
            res.writeHead(200);
            res.end('WebSocket server is running');
          });

          const wss = new WebSocket.Server({ server });

          log('WebSocket server created');

          wss.on('connection', (twilioWs, request) => {
            log(`Twilio WebSocket connection attempt from ${request.socket.remoteAddress}`);

            let streamSid = null;

            twilioWs.on('message', (message) => {
              try {
                const msg = JSON.parse(message);
                log(`Received message from Twilio: ${JSON.stringify(msg)}`);

                if (msg.event === 'start') {
                  log('Media stream started');
                  streamSid = msg.start.streamSid;
                  log(`Stream SID: ${streamSid}`);
                  sendTTSMessage(partialResponse);
                } else if (msg.event === 'media' && !messageComplete) {
                  log('Received media event');
                } else if (msg.event === 'stop') {
                  log('Media stream stopped');
                  hangupCall();
                }
              } catch (error) {
                log(`Error processing Twilio message: ${error.message}`);
              }
            });

            twilioWs.on('close', (code, reason) => {
              log(`Twilio WebSocket disconnected. Code: ${code}, Reason: ${reason}`);
            });

            twilioWs.on('error', (error) => {
              log(`Twilio WebSocket error: ${error.message}`);
            });

            // Handle incoming audio chunks from TTS WebSocket
            ttsWebSocket.on('message', (audioChunk) => {
              log('Received audio chunk from TTS');
              try {
                if (streamSid) {
                  twilioWs.send(JSON.stringify({
                    event: 'media',
                    streamSid: streamSid,
                    media: {
                      payload: JSON.parse(audioChunk)['data']
                    }
                  }));

                  audioChunksReceived++;
                  log(`Audio chunks received: ${audioChunksReceived}`);

                  if (audioChunksReceived >= 50) {
                    messageComplete = true;
                    log('Message complete, preparing to hang up');
                    setTimeout(hangupCall, 2000);
                  }
                } else {
                  log('Warning: Received audio chunk but streamSid is not set');
                }
              } catch (error) {
                log(`Error sending audio chunk to Twilio: ${error.message}`);
              }
            });

            log('Twilio WebSocket connected and handlers set up');
          });

          wss.on('error', (error) => {
            log(`WebSocket server error: ${error.message}`);
          });

          server.listen(0, () => {
            const port = server.address().port;
            log(`Twilio WebSocket server is running on port ${port}`);
            resolve(port);
          });

          server.on('error', (error) => {
            log(`HTTP server error: ${error.message}`);
            reject(error);
          });
        });
      }

    async function setupNgrokTunnel(port) {
        try {
          const listener = await ngrok.forward({
            addr: port,
            authtoken: config.NGROK_AUTHTOKEN,
          });
          const wssUrl = listener.url().replace('https://', 'wss://');
          log(`ngrok tunnel established: ${wssUrl}`);
          return wssUrl;
        } catch (error) {
          log(`Error setting up ngrok tunnel: ${error.message}`);
          throw error;
        }
      }

    async function main() {
      try {
        log('Starting application');

        await connectToTTSWebSocket();
        log('TTS WebSocket connected successfully');

        await testTTSWebSocket();
        log('TTS WebSocket test passed successfully');

        const twilioWebsocketPort = await setupTwilioWebSocket();
        log(`Twilio WebSocket server set up on port ${twilioWebsocketPort}`);

        const twilioWebsocketUrl = await setupNgrokTunnel(twilioWebsocketPort);

        await startCall(twilioWebsocketUrl);
      } catch (error) {
        log(`Error in main function: ${error.message}`);
      }
    }

    // Run the script
    main();
    ```
  </Step>

  <Step title="アプリケーションを実行する">
    アプリケーションを実行するには、次のコマンドを使用します:

    ```bash lines theme={null}
    node app.js
    ```
  </Step>
</Steps>

## 仕組み

1. スクリプトは Cartesia の TTS WebSocket への接続を確立します。
2. Twilio と通信するためのローカル WebSocket サーバーをセットアップします。
3. ローカルの WebSocket サーバーをインターネットに公開するための ngrok トンネルを作成します。
4. Twilio を使って通話を発信し、ngrok トンネルに接続します。
5. 通話が接続されると、スクリプトは事前に定義したメッセージを Cartesia の TTS に送信します。
6. Cartesia はテキストを音声に変換し、音声チャンクを返します。
7. スクリプトはこれらの音声チャンクを Twilio に転送し、Twilio が通話上で再生します。

## カスタマイズ

* 読み上げるメッセージを変更するには、`partialResponse` 変数を変更します。
* TTS のボイス特性を変えるには、`voice` オブジェクトのボイスパラメーターを調整します。
* 通話を終了するタイミングを制御するには、`audioChunksReceived` のしきい値を変更します。

## トラブルシューティング

* 問題が発生した場合は、コンソールログで詳細なエラーメッセージを確認してください。
* 必要な環境変数がすべて正しく設定されていることを確認してください。
* `invalid tunnel configuration` と表示される場合は、`ngrok` ではなく、よりサポートが充実した `@ngrok/ngrok` パッケージを使用していることを確認してください。
