16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

今回Twilioハッカソンに参加してやさしい呼び出しくんというプロダクトを開発し、最優秀賞を受賞することができました。
発表会の様子はこちらより確認できます

またプロダクトのソースコードはこちらにて公開しております。
TwilioHackathon2022
よろしければ参考にしてください。

以降ではこのプロダクトの開発で使用した技術や得られた知見について紹介します。

要件

やさしい呼び出しくんとはどういうプロダクトなのかざっくり解説しますと
「チャットツールでメンションしたのに反応が無い人に反応してもらえるように催促するシステム」
というものになります。
主に例えば緊急の事案に対して呼び出しを行いたい時に使用されることを想定しています。

このシステムの実現のために必要だった要件について以下の通りです。

  • Slackからのメンションを受け取る
  • 返事、リアクションがなかった時にのみ遅延実行させる
  • Twilioを使って電話をかける
  • Twilioを使ってSMSを送る
  • 電話機からプッシュホンを受け取る
  • 電話を転送する
  • 通話内容を録音する
  • 通話内容を文字に起こす

要件の実現のために実際に行なった処理の流れの詳細を以下に図示します。

以降ではこの要件(機能)を具体的にどうやって実装していったのか紹介していきます。

使用技術

一覧

基本的な構成

webhookの受け取り先(サーバ)はAPI Gateway + Lambdaで受けとれるようにしました。
Serverless Framwork, Node.js, Express (serverless-express)でサーバサイドの開発を進めていきました。

システムを動かしてみる

開発したプロジェクトの動かし方やデプロイの方法などについては詳しくは README.md に記述しましたのでそちらを参考にしてください。

簡単に動かしてみる解説

Serverless Framwork を使用していますので、Serverless Framworkがインストールされている状況下で以下のコマンドを実行するとlocalにてサーバーが立ち上がります。

serverless offline start

サーバーが立ち上がった後に http://localhost:3000/dev/test にアクセスすると該当のエンドポイントのAPIの実行結果を確認できます。
また Lambda へのデプロイは AWS Credentials を参考にAWSへの認証まわりの設定が完了した状態で

serverless deploy

コマンドを実行することでデプロイすることができます。

要件と実現した方法について

以降ではシステムを動かしてみることができている状態(サーバが立ち上がっている状態)でそれぞれの用件についてその実装方法やコードについての解説を行なっていきます。

Slackからのメンションを受け取る

Slackからのメンションおよびリアクションを受け取るにはSlack APIの中にあるSlack Events API を使用することでSlackにてメンションおよびリアクションがあった時にWebhookイベントが送られてくるようになります。

Slack APIを使用できるようにする

Slack APIを使用できるようにするためにはまずは「Create New App」を選択してAPI Keyなどの取得を行います。

Slack-Apps.png

Slack Events APIを使用してWebhookを受け取る

SlackのAPIが利用できるようになったら、Slack Events API が利用できる状態にしていきます。
Event Subscriptions を選択して Request URL にwebhookを受け取る場所のURLを入力します。

Slack-event-api.png

すでに上記にてサーバが立ち上がっている状態であるため、その中にある受け取りたい箇所でのURLを入力します。
またこの時にSlack Events APIにてWebhookが受取できるのかどうか確認するためにChallenge認証 が行われます。Challenge認証では入力したURLに対してPOSTでHTTPリクエストが送られてきます。そのリクエストの中に以下のようなJSONの情報がBodyに含まれて送られてきます。

この中の challenge の値を {challenge: "challengeの値"} となるJSONの形で返すことによって Challenge認証 が完了します。(上記のようなJSONが送られてきた場合は以下のようなJSONを返します)

Challenge認証 を行なっている部分の処理は以下のようなソースコードで実現しています。

import { NextFunction, Request, Response }, express from 'express';
import bodyParser from 'body-parser';

const slackWebhookRouter = express.Router();
// JSON bodyが送られてきた時に自動的にJSON parseを行うための設定
slackWebhookRouter.use(bodyParser.text({ type: "application/json" }));
slackWebhookRouter.use(bodyParser.urlencoded({ extended: false }));

slackWebhookRouter.post('/recieved_event', async (req: Request, res: Response, next: NextFunction) => {
  const webhookBody = req.body;
  // challengeが行われたときのresponse
  res.json({challenge: webhookBody.challenge});
});

作成したコードにおいての該当の部分はこちらになります。

参考

Webhookで受け取るイベントの登録

Slack Events APIによるWebhookを受け取るURLの登録が完了したら、Webhookで送ってほしいイベントの登録を行います。
このイベントの登録には Bot または user に追加することができます。

  • Botに追加した場合は Bot を追加した特定のチャンネルでのイベントにのみWebhookが実行されます。
  • user に追加した場合 user が参加しているチャンネル全てにおいてWebhookが実行されます。

今回はメンション(Slackへの投稿)とリアクション(投稿へのリアクション)が発生した時のイベントを取得できるようにしたいので、message.channelsreaction_added のEventを Bot に対して追加しました。

add-slack-event-bot.png

これでSlackでの投稿、およびリアクションがあった時にWebhookイベントを受け取れるようになりました。

返事、リアクションがなかった時にのみ遅延実行する

SlackからのWebhookイベントを受け取れるようになったので、メンションをつけて投稿の投稿、本プロダクトの起動コマンドが入力されたのかどうか判別することができるようになりました。
メンションをつけられた人からのリアクションなどの反応があるまで一定時間待ってみて、リアクションがなかった電話をかけられるようにします。
反応があるまで待つ処理を実現させるために AWS SQS を使用することで処理の遅延実行が可能になります。

AWS SQSに遅延実行を追加する

AWS SQS にメッセージ(タスク)を追加する時に DelaySeconds 要素を指定することでことで指定した秒数の分だけ処理の遅延実行が可能になります。
まずはAWS SDK for JavaScript v3AWS SQSを実行できるライブラリの導入を行います。

yarn add @aws-sdk/client-sqs

そして以下のような処理を実行することでキューに追加されて 900秒 遅れて処理が実行されるようになります。(なおDelaySeconds に指定できる秒数は 0 ~ 900の間。最大で遅延できる秒数は 900秒)

import { SQSClient, SendMessageCommand, SendMessageCommandInput } from '@aws-sdk/client-sqs';

const sqsClient = new SQSClient({ region: process.env.AWS_REGION });  // 'ap-northeast-1' と指定したら東京リージョン
const params: SendMessageCommandInput = {
  QueueUrl: process.env.QUEUE_URL,
  DelaySeconds: 900, // 900秒遅れて遅延実行
  MessageBody: "messsage text",
};
sqsClient.send(new SendMessageCommand(params));

上記の process.env.QUEUE_URL の値の確認と遅延実行されるときに実行される処理について serverless.ts に記述して serverless deploy を行うことでAWS上で実行されるようになります。それぞれどのように記述すると適用されるのか以下に記述していきます。

const serverlessConfiguration: AWS = {
  ...
  resources: {
    Resources: {
      // ここに指定することでserverless deploy時にSQSにキューを自動的に作成してくれるようになります
      Type: 'AWS::SQS::Queue',
      Properties: {
        QueueName: 'queueName',
      },
    },
  },
  provider: {
    ...
    // enviroment の項目に追加することによって process.env.QUEUE_URL にて設定された値を参照することができます
    environment: {
      QUEUE_URL: 'https://sqs.${aws:region}.amazonaws.com/${aws:accountId}/queueName';,
    },
  },
  functions: {
    ...
    queueevent: {
      // キューから取り出される時に遅延実行が行われる処理が記述されている場所(Lambdaにて実行される)
      handler: 'src/sqs.handler',
      events: [
        {
          sqs: {
            arn: 'arn:aws:sqs:${aws:region}:${aws:accountId}:queueName',
          },
        },
      ],
    },
  }
}

※ 上記で指定した変数の内容について

  • queueName はそれぞれ設定したいキューの名前を指定してください
  • src/sqs.handlersrc/sqs.ts の中の記述の export const handler = async (event, context) => {} (handler function) 以下にキューから取り出された時に関数が呼び出されることを指定しています。
  • ${aws:region}${aws:accountId} はそれぞれ変数が自動的に入力されます。 serverless deployserverless offline start を行う時にAWSから値を取得して自動的に入力されます。(AWS Credentials にて認証した値を基に参照が行われる)

キューから取り出された時に呼び出される関数は以下のように様になります。

import { SQSHandler, SQSEvent, Context } from 'aws-lambda';

export const handler: SQSHandler = async (event: SQSEvent, context: Context) => {
  for (const record of event.Records) {
    // record.bodyにMessageBodyにて指定した文字列
  }
};

record.body にてキューに追加したときにMessageBodyにて指定した文字列を取得することができます。MessageBodyにJSONを入れて追加した場合、ここで取得した時に JSON.parse() 行うことで変数を変数を取得することもできます。

詳しい処理はこちらにて記述しています。src/sqs.ts

参考

Twilioを使って電話をかける

Twilioの設定

Twilioの利用を可能にするための各種設定をおこなっていきます。
まずはTwilioのNode.jsのライブラリであるTwilio Nodeをインストールするようにします。

yarn add twilio

次にTwilioの機能を使用するためにTwilioのコンソールにアクセスして ACCOUNT_SIDAUTH_TOKEN の値を確認する(後で使用します)

Twilio-account-info.png

今回は電話をかけたりSMSを送ったりするのでTwilioで電話番号を購入します。
コンソールから Buy a number タブを選択し、Buy を選択して電話番号を購入します。

Twilio-buy-number.png

Twilioから電話をかける

ACCOUNT_SIDAUTH_TOKEN と 購入した番号を使用して以下のような処理を記述することでTwilioから電話をかけることができます。

import twilio from 'twilio';
const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
twilioClient.calls.create({
  twiml: twiml,
  from: process.env.TWILIO_US_PHONE_NUMBER,
  to: '+818012345678',
});
  • TWILIO_ACCOUNT_SID には ACCOUNT_SID
  • process.env.TWILIO_AUTH_TOKEN には AUTH_TOKEN
  • process.env.TWILIO_US_PHONE_NUMBER には購入した電話番号

の値をそれぞれ指定します。
上記の場合 +818012345678 に電話をかける処理となります(←の番号はサンプルの番号です)

上記のコードで指定している twiml とは Twilio Markup Language といってXMLの形式の文字列を挿入することでTwilioにおける電話の操作ができるようになります。

例えば

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say>Hello, world!</Say>
</Response>

このような twiml を指定して電話をかけると通話越しに Hello, world! としゃべってくれます。TypescriptでTwilio Nodeを使用しながら電話をかける処理と組み合わせて記述すると以下のようになります。

import twilio from 'twilio';
const VoiceResponse = twilio.twiml.VoiceResponse;
const response = new VoiceResponse();
response.say('Hello, world!');
const twiml = response.toString();

const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
twilioClient.calls.create({
  twiml: twiml,
  from: process.env.TWILIO_US_PHONE_NUMBER,
  to: '+818012345678',
});

電話をかけたけど、受け取ったかのかどうかを取得する

上記電話かけた時に電話を受けたのかどうか判別するには 電話をかける時に statusCallback を指定することによって、電話かけた時に電話を受けたのかどうか判別できるWebhookが送られてくるようになります。以下のように記述することで電話を受けたかどうか判別できます。

import twilio from 'twilio';
const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
twilioClient.calls.create({
  twiml: twiml,
  from: process.env.TWILIO_US_PHONE_NUMBER,
  to: '+818012345678',
  statusCallback: "https://aaa.co.jp/call_handler",
  statusCallbackMethod: 'POST',
});

以下のように記述することでstatusCallback で指定したURLにTwilioからのwebhookを受けとります。

import express from 'express';
const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/call_handler', async (req, res) => {
  const payload = req.body;
  const callStatus = payload.CallStatus;
  switch(callStatus) {
    case 'completed':
      // 電話を受け取った場合、何もしない
      break;
    case 'busy':
      // 電話を受け取らなかった場合、SMSに通知を送る
      break;
    default:
      break;
  }
  res.send('ok');
});

TwilioのwebhookはPOSTで受け取る場合application/x-www-form-urlencoded がヘッダーについてBodyには CallStatus=completed&AccountSid=AccountSid のような形式で黄倉れてきます。
上記の記述ではその内容を適切な形でParseしてその後の処理に活かしています。

参考

Twilioを使ってSMSを送る

Twilioを使って電話をかけることがすでにできる状態で以下のように記述することでSMSを送ることができます。

import twilio from 'twilio';
const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
twilioClient.messages.create({
  body: "メッセージを送りたい内容",
  from: process.env.TWILIO_US_PHONE_NUMBER,
  to: '+818012345678',
});

参考

電話機からプッシュホンを受け取る

プッシュホンを受け取る場合は電話をかける時の twimlGather の要素を加えたtwimlを作成することでプッシュホンを受け付けることができるようになります。
プッシュホンの内容を受け取る場合、Gather の要素の action の部分に受け取り先のURLを指定することで、プッシュホンが押された内容がTwilioからのWebhookで送られてきます。以下のように twiml を作成することでプッシュホンの内容を受け取ることができます

import twilio from 'twilio';

const VoiceResponse = twilio.twiml.VoiceResponse;
const response = new VoiceResponse();
// 番号をプッシュした時の受け取り先を指定
const gather = response.gather({
  // 番号を押した時の受け取り先
  action: "https://aaa.co.jp/gather_dtmf_handler",
  input: 'dtmf', // dtmf がいわゆる電話機の番号入植という意味 speech にしたら話している内容を文字に起こして入力される
  finishOnKey: '#', // 入力終了のKey defaultは'#' 文字を空を指定したら全ての記号が乳力終了になる
  method: 'POST',
  timeout: 30, // 入力をうけつけてくれる秒数
});
gather.say(
  {
    language: 'ja-JP',
    voice: 'woman',
  },
  '1を押したら電話をかけます。2を押したら要件の内容をメッセージに残してお伝えします。最後にシャープキーを押してください。';
);
const twiml = response.toString();
const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
twilioClient.calls.create({
  twiml: twiml,
  from: process.env.TWILIO_US_PHONE_NUMBER,
  to: '+818012345678',
  statusCallback: "https://aaa.co.jp/call_handler",
  statusCallbackMethod: 'POST',
});

※ 上記のコードで gather.say() とすることで以下のように <Gather></Gather>の要素の内部に<Say></Say>の要素が入ったtwiml が生成されます。

<Response>
    <Gather>
        <Say>1を押したら電話をかけます。2を押したら要件の内容をメッセージに残してお伝えします。最後にシャープキーを押してください。</Say>
    </Gather>
</Response>

このような形式の twiml を作成することで Say の内容をしゃべり終えてからプッシュホンの受付を待つようになります。それ以外の場所に Say の要素を入れるとしゃべっている最中にプッシュホンの受付が開始されてしまいます。

プッシュホンの受付は以下のように記述することでaction で指定したURLにTwilioからのwebhookを受けとります。

import express from 'express';
const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/gather_dtmf_handler', async (req, res) => {
  const payload = req.body;
  // 返答する内容のTwimlを設定する
  const response = new VoiceResponse();
  if (payload.Digits) {
    // 1が押された時の処理
    if (payload.Digits === '1') {
      response.say(
        {
          language: 'ja-JP',
          voice: 'woman',
        },
        '1が押されました',
      );
    // 2が押された時の処理
    } else if (payload.Digits === '2') {
      response.say(
        {
          language: 'ja-JP',
          voice: 'woman',
        },
        '2が押されました',
      );
    }
  }
  res.type('text/xml');
  res.send(response.toString());
});

Webhookの受け取り処理ではプッシュホンを押した後に続けての音声などの返答を返す必要があるためtwiml の形式で返答しています。
上記の処理の内容では 1 を押したら 1が押されました という返答が返ってきます。

参考

電話を転送する

Twilioを使って電話を転送するときは twiml をレスポンスに返す時に twiml の中にDialの要素を含めたtwimlを返すことで電話の転送が可能になります。
具体的には以下のように記述することで電話の転送が可能になります。

import express from 'express';
const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/gather_dtmf_handler', async (req, res) => {
  const payload = req.body;
  // 返答する内容のTwimlを設定する
  const response = new VoiceResponse();
  if (payload.Digits) {
    // 1が押された時の処理
    if (payload.Digits === '1') {
      // dialで電話を転送する
      response.say(
        {
          language: 'ja-JP',
          voice: 'woman',
        },
        '電話をかけます',
      );
      response.dial('+818087654321');
    // 2が押された時の処理
    } else if (payload.Digits === '2') {
      response.say(
        {
          language: 'ja-JP',
          voice: 'woman',
        },
        '2が押されました',
      );
    }
  }
  res.type('text/xml');
  res.send(response.toString());
});

上記の場合では Twilioから +818012345678 の電話番号にかけている電話を +818012345678 から +818087654321 の電話番号に電話をかける(転送する)るようにしています。
また上記のtwimlは以下のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>電話をかけます</Say>
  <Dial>+818087654321</Dial>
</Response>

<Say></Say> の要素が <Dial></Dial> の要素よりも上にあることで、<Say></Say> の内容をしゃべってから 電話の転送が行われます。
<Say></Say> の要素が <Dial></Dial> の要素よりも下にある場合、電話の転送が行われた後に<Say></Say> の内容をしゃべります。

また電話の転送は上記の +818012345678 から電話をかけているのではなく、Twilioから電話をかけているものとして扱われます。

参考

通話内容を録音する

Twilioで通話内容が録音されるようになる

Twilioを使って通話中の内容を録音するときは twiml をレスポンスに返す時に twiml の中に Record の要素を含めた twiml を返すことで通話の録音が可能になります。
具体的には以下のように記述することで電話の録音が可能になります。

import express from 'express';
const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/gather_dtmf_handler', async (req, res) => {
  const payload = req.body;
  // 返答する内容のTwimlを設定する
  const response = new VoiceResponse();
  if (payload.Digits) {
    // 1が押された時の処理
    if (payload.Digits === '1') {
      // dialで電話を転送する
      response.say(
        {
          language: 'ja-JP',
          voice: 'woman',
        },
        '電話をかけます',
      );
      response.dial('+818087654321');
    // 2が押された時の処理
    } else if (payload.Digits === '2') {
      const timeoutSecond = 30;
      // 録音するより前に言わせるにはrecordより前にsayの処理を書く
      response.say(
        {
          language: 'ja-JP',
          voice: 'woman',
        },
        'ピーとなったら' + timeoutSecond.toString() + '秒で要件をお話しください',
      );
      response.record({
        timeout: timeoutSecond,
        playBeep: true, // 録音を開始する前にピーという音を鳴らす
        recordingStatusCallbackMethod: 'POST',
        recordingStatusCallback: "https://aaa.co.jp/recording_status_handler",
      });
    }
  }
  res.type('text/xml');
  res.send(response.toString());
});

このとき recordingStatusCallback: の要素にURLを指定すると、録音が完了し、音声ファイルが出来上がった段階でTwilioから音声ファイルのダウンロードURLなどの情報が記載されたWebhookが送られてきます。
このWebhookは以下のように受け取ります。

import express from 'express';
const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/recording_status_handler', async (req, res) => {
  const payload = req.body;
});

このとき上記のWebhookで送られてきた上h上を受け取った payload には以下のような情報が送られてきます

音声ファイルをダウンロードする

上記のWebhookで受け取った情報の中にある RecordingUrl の要素が音声ファイルのダウンロード先のURLにあたります。
このダウンロード先のURLからダウンロードしようとした場合、BASIC認証がかかっており、ユーザ名とパスワードの入力を求められます。
ユーザ名は上記のTwilioの ACCOUNT_SID、パスワードは AUTH_TOKEN の値を入力することでBASIC認証を通り、音声ファイルのダウンロードを行うことができます。
またaxiosを使い、処理の中で音声ファイルのダウンロードを記述すると以下のようになります

import express from 'express';
const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/recording_status_handler', async (req, res) => {
  const payload = req.body;
  const downloadResponse = axios.get(`${payload.RecordingUrl}.wav`, {
    auth: {
      username: process.env.TWILIO_ACCOUNT_SID,
      password: process.env.TWILIO_AUTH_TOKEN,
    },
  });
});

上記のソースコードの中において downloadResponse.data と呼び出すことで音声ファイルのバイナリが取得できます。
この内容をファイルとして保存するなどすることで音ファイルとして保存することができます。

参考

通話内容を文字に起こす

Twilioを使って文字起こしをする

Twilioを使って通話中の内容を文字に起こすとき録音時と同様に twiml をレスポンスに返す時に twiml の中に Record の要素を含めて、transcribe の要素を true にし、transcribeCallback の要素に文字起こしの結果を受け取るWebhookのURLを指定した twiml を返すことで通話の録音と文字起こしが可能になります。
※ ただし現在Twilioで文字起こしに対応している言語は英語のみになります。

具体的には以下のように記述することで電話の録音および文字起こしが可能になります。

import express from 'express';
const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/gather_dtmf_handler', async (req, res) => {
  const payload = req.body;
  // 返答する内容のTwimlを設定する
  const response = new VoiceResponse();
  if (payload.Digits) {
    // 1が押された時の処理
    if (payload.Digits === '1') {
      // dialで電話を転送する
      response.say(
        {
          language: 'ja-JP',
          voice: 'woman',
        },
        '電話をかけます',
      );
      response.dial('+818087654321');
    // 2が押された時の処理
    } else if (payload.Digits === '2') {
      const timeoutSecond = 30;
      // 録音するより前に言わせるにはrecordより前にsayの処理を書く
      response.say(
        {
          language: 'ja-JP',
          voice: 'woman',
        },
        'ピーとなったら' + timeoutSecond.toString() + '秒で要件をお話しください',
      );
      response.record({
        timeout: timeoutSecond,
        playBeep: true, // 録音を開始する前にピーという音を鳴らす
        recordingStatusCallbackMethod: 'POST',
        recordingStatusCallback: "https://aaa.co.jp/recording_status_handler",
        transcribe: true,
        transcribeCallback: "https://aaa.co.jp/transcribe_handler",
      });
    }
  }
  res.type('text/xml');
  res.send(response.toString());
});

transcribeCallback: の要素に指定したURLに文字起こしが完了した段階でTwilioから文字起こしの結果などの情報を含んだWebhookが送られてきます。
このWebhookは以下のように受け取ります。

import express from 'express';
const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/transcribe_handler', async (req, res) => {
  const payload = req.body;
});

のとき上記のWebhookで送られてきた上h上を受け取った payload には以下のような情報が送られてきます

Webhookで受け取った情報の中にある TranscriptionText の要素を使用してSlackに投稿した入りなどの処理行います。

通話内容の日本語への文字起こし

Twilioでは日本語の文字起こしには対応していないので、日本語の文字起こしを行うために AWS Transcribe を使って日本語文字起こしを行います。
AWS Transcribeを使って文字起こしを行う場合は対象の音声ファイルはS3に置いてあるファイルを指定する必要があります。
そのため上記の音声ファイルのダウンロードし、そのままS3にアップロードするようにしました。
具体的には以下のように記述することで音声ファイルのダウンロードし、そのままS3にアップロードしました。

import express from 'express';
import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';

const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/recording_status_handler', async (req, res) => {
  const payload = req.body;
  const downloadResponse = axios.get(`${payload.RecordingUrl}.wav`, {
    responseType: 'stream',
    auth: {
      username: process.env.TWILIO_ACCOUNT_SID,
      password: process.env.TWILIO_AUTH_TOKEN,
    },
  });
  const s3Client = new S3Client({ region: process.env.AWS_REGION });
  const upload = new Upload({
    client: s3Client,
    params: {
      Bucket: process.env.S3_BUCKERT_NAME,
      Key: `RecordingFiles/${payload.RecordingSid}.wav`,
      Body: downloadResponse.data,
    },
  });
  await upload.done();
});

今回、axiosでダウンロードする時 responseType: 'stream' を指定してダウンロードを行い、S3へのアップロードにおいて @aws-sdk/client-s3 だけでなく、@aws-sdk/lib-storage のライブラリをもちいてS3にアップロードしています。
これはダウンロードしながら同時にアップロードを行うようにしているためです。
これによりダウンロード、アップロードの処理時間の短縮および少ないメモリでの実現が可能になりました。

S3へのアップロードが完了したらAWS Transcribeを起動させて、音声ファイルの文字起こしを行ないます。
具体的には以下のように記述することでAWS Transcribeを用いての日本語文字起こしを行なっています。

import express from 'express';
import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import {
  TranscribeClient,
  StartTranscriptionJobCommand,
  StartTranscriptionJobCommandInput,
} from '@aws-sdk/client-transcribe';

const app = express();
// Bodyの内容を文字列として受け取る
app.use(bodyParser.text({ type: '*/*' }));
app.use(bodyParser.urlencoded({ extended: false }));

app.post('/recording_status_handler', async (req, res) => {
  const payload = req.body;
  const downloadResponse = axios.get(`${payload.RecordingUrl}.wav`, {
    responseType: 'stream',
    auth: {
      username: process.env.TWILIO_ACCOUNT_SID,
      password: process.env.TWILIO_AUTH_TOKEN,
    },
  });
  const s3Client = new S3Client({ region: process.env.AWS_REGION });
  const upload = new Upload({
    client: s3Client,
    params: {
      Bucket: process.env.S3_BUCKERT_NAME,
      Key: `RecordingFiles/${payload.RecordingSid}.wav`,
      Body: downloadResponse.data,
    },
  });
  await upload.done();
  const transcribeClient = new TranscribeClient({ region: process.env.AWS_REGION });
  const params: StartTranscriptionJobCommandInput = {
    TranscriptionJobName: payload.RecordingSid,
    LanguageCode: 'ja-JP',
    MediaFormat: 'wav',
    Media: { MediaFileUri: `s3://${process.env.S3_BUCKERT_NAME}/RecordingFiles/${payload.RecordingSid}.wav` },
    OutputBucketName: process.env.S3_BUCKERT_NAME,
    OutputKey: `TranscribeResult/${payload.RecordingSid}.json`,
  };
  const command = new StartTranscriptionJobCommand(params);
  await transcribeClient.send(command);
});

これにより AWS Transcribeを用いての日本語文字起こしが行われるJobが実行されます。
文字起こしされた結果は OutputKey: の要素で指定したS3の場所に文字起こしされた結果のJSONファイルが生成されます。

AWS Transcribe ではJobが完了してもJobは消えることなく残り続けます。また、Job名が重複してしまうとエラーになってしまいます。そのためJob名が重複しないようにJobを実行する時にすでにJobが存在する場合は事前にJobを消した上で実行するようにする必要があります。
(実際にJob名が重複しないようにおこなっている処理はこちらを参考にしてください)

S3に新しいJSONファイルが作成されたということなので、そのタイミングでもってLambdaでイベントを実行することで、文字起こしの結果の内容を参照してSlackに投稿した入りなどの処理行います。

参考

さいごに

実際のソースコードは疎結合ができるように細かいパーツ単位で分かれています。
またハッカソン内で作成したものであるので、ソースコードのりファクタなどを行う余地が十分にあるように感じています。リファクタによる改善にも取り組んでいきたいと思います。

また

においてはローカルでの開発環境を整えることができませんでした。
このためこれらのAWSサービスを使用する場合は環境構築の方法やノウハウの調査を進めて、ローカルでも開発ができるようにしていきたいと思います。
またはIAMの権限を制限して、その上で他のメンバーでもAWSのデプロイができるように情報の共有などを行えるようにしたいとおもいます。

16
6
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
16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?