Programmable VoiceとAITalkのWeb APIを使って表現力豊かな自動音声で着信電話に応答してみた

Daizen Ikehara - Apr 20 '20 - - Dev Community

Twilioが提供するProgrammable Voiceを利用すると、電話の自動応答や発信をアプリケーションから制御することができます。 着信に応答する場合は、Twimlと呼ばれるマークアップ言語を用いることで、応答する言語や音声を設定することができます。 またこのProgrammable Voiceでは、2018年から標準の音声合成エンジン以外にもAmazon Pollyが使えるようになりました。

Amazon Pollyの合成音声は標準の合成音声に比べてとても滑らかなのですが、他の音声をリアルタイムで利用したいという声もよくいただきます。今回は株式会社エーアイ様が提供する日本語に特化した音声合成サービス、AITalk® Web API(以下 AITalk Web API)と連携してみました。

AITalk Web APIは 感情表現や自然な音声、豊富な話者ラインナップ を提供する音声合成エンジンAITalkをSaaS型で利用できるサービスです。サーバーを自前で構築する必要がないため、他のSaaSやクラウドサービスと素早く連携できます。これまでは評価版の申し込みから利用開始まで少し時間がかかっていましたが、最近、Webから評価用アカウントを即時取得し利用できるようになりました。

必要なもの

  • Node.jsおよびnpm
  • AITalk Web API評価アカウント
  • TwilioアカウントとTwilio電話番号

今回の記事で解説するサンプルプロジェクトはこちらのGitHubリポジトリから取得できます。

GitHub - Neri78: Twilio-Voice-AiTalkWebAPI

AITalk Web API評価アカウントの作成方法

冒頭でも記載しましたが、AITalk Web APIでは評価を目的としてサービスを2週間利用できる評価アカウントをWebサイトから申し込めるようになりました。AITalk WebAPI 評価アカウント申込みページで必要な情報を入力し利用規約に同意すると、評価用APIのエンドポイントや接続情報、APIの仕様に関する情報がメールで送られてきます。

AiTalk-WebAPI-Trial-SignUp

Twilioアカウントのサインアップと電話番号の取得

Twilioアカウントのサインアップと電話番号の取得方法についてはこちらで確認できます。TwilioQuest 3というゲームチュートリアルを題材にしていますが、サインアップと電話番号の購入方法は通常のフローと同じです。

Twilioアカウントの作成と電話番号の取得

AITalk Web APIを用いた音声応答の構築

サンプルでは、下記の図のようにTwilio電話番号に着信した際に、Webhookを経由し、 AITalk Web API にリクエストを送信し、合成された音声データをレスポンスとして受け取ります。

Prog-voice-AiTalk

サンプルプロジェクトをローカル開発環境に展開し、必要なパッケージをインストールします。

npm install
Enter fullscreen mode Exit fullscreen mode

次に、.env.sampleファイルをコピーし、.envとリネームします。このファイルにはアカウント登録時に送られてきたAITalk Web APIのエンドポイント、ユーザー名、パスワードを保存します。これらの情報はサンプルコード内で利用されます。

AI_TALK_BASE_URL=https://api.example.com
AI_TALK_USERNAME=username
AI_TALK_PASSWORD=password
Enter fullscreen mode Exit fullscreen mode

着信時の応答を返すAPIを実装

次にindex.jsを開いてください。必要なパッケージの宣言と、生成した音声ファイルを保存しておくフォルダを設定しています。

'use strict';
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const fsp = require('fs').promises;
const got = require('got');

//着信に対して応答を返すVoice用TwiMLを作成するクラス
const VoiceResponse = require('twilio').twiml.VoiceResponse;
const app = express();
const VOICE_FOLDER_NAME = 'voice\files';
app.use(bodyParser.urlencoded({ extended: true }));

// 生成したファイルを保存しておくフォルダを設定
app.use(express.static(path.join(__dirname, VOICE_FOLDER_NAME)));
Enter fullscreen mode Exit fullscreen mode

Twilioで取得した電話番号に着信があった際に呼び出すWebhookはこちらになります。内部で非同期メソッドを使用するためasyncキーワードを指定しています。

// Twilioから着信リクエストを受け取るWebhook
app.post('/incoming', async (req, res, next) => {//処理を実装});
Enter fullscreen mode Exit fullscreen mode

AITalk Web APIへのリクエストを送信し生成されたファイルを保存

さて、/incoming内部の実装は次のようになっています。

応答テキストと AITalk Web API に送信するリクエストパラメーターを構築します。さまざまな設定が可能ですが、最低限、ユーザー名、合成文字列、話者名が必要になります。このサンプルでは感情対応話者である「のぞみ」さん('speaker_name' : 'nozomi_emo')、最大限の喜びを('style' : { 'j': '1.0'})抑揚('range' : 2.00)をつけてmp3で出力('ext' : 'mp3')するように設定しました。

// 音声合成するテキストを設定。実際はAIエンジンなどから生成される。
const text = '今日はお電話ありがとう!エーアイトークとTwilioを連携した音声合成デモだよ! あなたの電話番号は' 
    + req.body.From 
    + 'だよね?ぜひ、両方のサービスを試してみてね。';
// AITalk Web API用のパラメーターを作成
const aiTalkOptions = {
    'username' : process.env.AI_TALK_USERNAME,
    'password' : process.env.AI_TALK_PASSWORD,
    'text' : text,
    'speaker\_name' : 'nozomi_emo',
    'style' : { 'j': '1.0'},
    'range' : 2.00,
    'ext' : 'mp3'}
Enter fullscreen mode Exit fullscreen mode

AITalk Web APIのパラメータを含んだリクエストを送信し、レスポンスに含まれるバイナリデータをmp3ファイルとして保存します。今回のサンプルではNode.js用のHTTPライブラリーgotを利用しています。

try {
    // リクエストオプション
    const options = {
        method: 'POST',
        form: aiTalkOptions,encoding: 'binary'
    };
    //AITalk Web APIにリクエスト
    const response = await got(process.env.AI_TALK_API_URL, options);
    if (response.statusCode === 200 ) {
        //現在のCallSidから出力ファイル名を生成
        const outputFileName = `${req.body.CallSid}.mp3`;
        // mp3ファイルを保存
        await fsp.writeFile(path.join(VOICE_FOLDER_NAME, outputFileName),
                                response.body, 'binary');
        // TwiMLを作成(次のコードブロック)}
} catch (error) {
    console.error(error);next(error);}
Enter fullscreen mode Exit fullscreen mode

保存したmp3ファイルを再生するTwiMLを作成

Twilio Programming Voiceは電話への着信時の応答を __TwiML__ と呼ばれているマークアップ言語でプログラミングできます。Play句を使用し、先ほど保存したmp3ファイルを再生するというTwiMLを生成します。このTwiMLをレスポンスとして返しています。

// TwiMLを初期化
const twiml = new VoiceResponse();
// 生成した音声を再生させる。
twiml.play(path.join(outputFileName));
//作成したTwiMLをレスポンスとして送信
res.status(200).send(twiml.toString());
Enter fullscreen mode Exit fullscreen mode

(追加)通話が終了した段階でmp3ファイルを削除

さて、このままでは通話終了後も生成された音声ファイルが残ってしまいます。不要なファイルを削除するため、通話状態を取得できるWebhookを実装します。

/statuschangedでは、リクエストのbodyに含まれているCallStatusで現在の通話状態を確認できます。今回のサンプルでは、通話が完了したcompletedとなった時点で、通話IDを表すCallSidを基にmp3ファイルを削除しています。

app.post('/statuschanged', async(req, res, next) => {
    try {
        // 通話が完了したかどうかを確認
        if (req.body.CallStatus === "completed") {
            // CallSidからファイルパスを構築し、削除
            const filePath = path.join(
                VOICE_FOLDER_NAME, `${req.body.CallSid}.mp3`);
            await fsp.unlink(filePath);
            console.info(`${filePath}を削除しました`);
        }
        res.sendStatus(200);
    } catch (error) {
        console.error(error);
        next (error);
    }
});
Enter fullscreen mode Exit fullscreen mode

全ての実装を終え、Port3000でサーバーを起動します。

app.listen(3000, () => console.log('Listening on port 3000'));
Enter fullscreen mode Exit fullscreen mode

これで実装は完了です。

ローカル環境でサンプルを実行する場合

ローカル環境でサンプルを実行する場合は、localhostのポート3000番に外部からアクセスできるようにします。

その場合は、ngrokなどのツールが利用できます。

ngrokをインストール後、別のターミナルを開き、下記のコマンドを実行します。

ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

ngrok - daizen

ここで生成されたURLをメモしておきます。

Twilioコンソールで着信応答と、通話ステータスのWebhookをそれぞれ設定

最後にTwilioコンソールで取得した電話番号の設定画面を表示し、作成したNode.jsアプリケーションのUrlを着信応答と通話ステータスのWebhookをそれぞれ指定します。

この例ではhttps://2c2a973d.ngrok.ioなので下記のスクリーンショットのようになります。

Twilio Console Incoming Status Webhook

ターミナルからアプリケーションを起動し動作を確認

ローカル環境の場合は、ターミナルからアプリケーションを起動します。

npm start
Enter fullscreen mode Exit fullscreen mode

Twilioから取得した番号に電話をかけてみてください。前回同様に電話番号を数字として解釈してしまっているので少し変ですが、__動的にリアルタイムで感情と抑揚が設定された__ 合成音声を応答として利用することを確認できます。

まとめ

今回はTwilio Programmable VoiceとAITalk Web APIを連携してみました。AITalk Web APIでは、感情表現対応話者だけでなく、関西弁 対応話者も設定されているので話者を('speaker_name' : 'miyabi_west')に変更して違いをくらべてみるのも面白いかもしれません。

質問についてはこちらまで

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .