Tuesday, 31 March, 2020 UTC


Summary

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の仕様に関する情報がメールで送られてきます。

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

Twilioアカウントのサインアップと電話番号の取得方法についてはこちらで確認できます。TwilioQuest 3というゲームチュートリアルを題材にしていますが、サインアップと電話番号の購入方法は通常のフローと同じです。
Twilioアカウントの作成と電話番号の取得
AITalk Web APIを用いた音声応答の構築
サンプルでは、下記の図のようにTwilio電話番号に着信した際に、Webhookを経由し、AITalk Web APIにリクエストを送信し、合成された音声データをレスポンスとして受け取ります。
サンプルプロジェクトをローカル開発環境に展開し、必要なパッケージをインストールします。
npm install 
次に、.env.sampleファイルをコピーし、.envとリネームします。このファイルにはアカウント登録時に送られてきたAITalk Web APIのエンドポイント、ユーザー名、パスワードを保存します。これらの情報はサンプルコード内で利用されます。
AI_TALK_BASE_URL=https://api.example.com AI_TALK_USERNAME=username AI_TALK_PASSWORD=password 
着信時の応答を返す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))); 
Twilioで取得した電話番号に着信があった際に呼び出すWebhookはこちらになります。内部で非同期メソッドを使用するためasyncキーワードを指定しています。
// Twilioから着信リクエストを受け取るWebhook app.post('/incoming', async (req, res, next) => {  //処理を実装 }); 
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' } 
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); } 
保存した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()); 
(追加)通話が終了した段階で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);  } }); 
全ての実装を終え、Port3000でサーバーを起動します。
app.listen(3000, () => console.log('Listening on port 3000')); 
これで実装は完了です。

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

ローカル環境でサンプルを実行する場合は、localhostのポート3000番に外部からアクセスできるようにします。
その場合は、ngrokなどのツールが利用できます。
ngrokをインストール後、別のターミナルを開き、下記のコマンドを実行します。
ngrok http 3000 
ここで生成されたURLをメモしておきます。
Twilioコンソールで着信応答と、通話ステータスのWebhookをそれぞれ設定
最後にTwilioコンソールで取得した電話番号の設定画面を表示し、作成したNode.jsアプリケーションのUrlを着信応答と通話ステータスのWebhookをそれぞれ指定します。
この例ではhttps://2c2a973d.ngrok.ioなので下記のスクリーンショットのようになります。
ターミナルからアプリケーションを起動し動作を確認
ローカル環境の場合は、ターミナルからアプリケーションを起動します。
npm start 
Twilioから取得した番号に電話をかけてみてください。前回同様に電話番号を数字として解釈してしまっているので少し変ですが、__動的にリアルタイムで感情と抑揚が設定された__ 合成音声を応答として利用することを確認できます。
まとめ
今回はTwilio Programmable VoiceとAITalk Web APIを連携してみました。AITalk Web APIでは、感情表現対応話者だけでなく、__関西弁__ 対応話者も設定されているので話者を('speaker_name' : 'miyabi_west')に変更して違いをくらべてみるのも面白いかもしれません。
- AITalk Web API
- Github - Neri78: Twilio-Voice-AiTalkWebAPI
質問についてはこちらまで