今回Twilioハッカソンに参加してやさしい呼び出しくんというプロダクトを開発し、最優秀賞を受賞することができました。
発表会の様子はこちらより確認できます
またプロダクトのソースコードはこちらにて公開しております。
TwilioHackathon2022
よろしければ参考にしてください。
以降ではこのプロダクトの開発で使用した技術や得られた知見について紹介します。
要件
やさしい呼び出しくんとはどういうプロダクトなのかざっくり解説しますと
「チャットツールでメンションしたのに反応が無い人に反応してもらえるように催促するシステム」
というものになります。
主に例えば緊急の事案に対して呼び出しを行いたい時に使用されることを想定しています。
このシステムの実現のために必要だった要件について以下の通りです。
- Slackからのメンションを受け取る
- 返事、リアクションがなかった時にのみ遅延実行させる
- Twilioを使って電話をかける
- Twilioを使ってSMSを送る
- 電話機からプッシュホンを受け取る
- 電話を転送する
- 通話内容を録音する
- 通話内容を文字に起こす
要件の実現のために実際に行なった処理の流れの詳細を以下に図示します。
以降ではこの要件(機能)を具体的にどうやって実装していったのか紹介していきます。
使用技術
一覧
- Node.js
- Typescript
- Serverless Framwork
- Express (serverless-express)
- Slack API
- AWS
- Lambda: Webhook受け取りなどのサーバとして使用
- API Gateway: Web APIとしての受け口
- SQS: 遅延実行を行うために使用
- S3: 録音ファイルから文字に起こすために使用
- Transcribe: 文字起こしに使用
- Twilio Node
- Kintone: データベース
基本的な構成
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 Events APIを使用してWebhookを受け取る
SlackのAPIが利用できるようになったら、Slack Events API が利用できる状態にしていきます。
Event Subscriptions
を選択して Request URL
にwebhookを受け取る場所のURLを入力します。
すでに上記にてサーバが立ち上がっている状態であるため、その中にある受け取りたい箇所での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.channels
と reaction_added
のEventを Bot
に対して追加しました。
これでSlackでの投稿、およびリアクションがあった時にWebhookイベントを受け取れるようになりました。
返事、リアクションがなかった時にのみ遅延実行する
SlackからのWebhookイベントを受け取れるようになったので、メンションをつけて投稿の投稿、本プロダクトの起動コマンドが入力されたのかどうか判別することができるようになりました。
メンションをつけられた人からのリアクションなどの反応があるまで一定時間待ってみて、リアクションがなかった電話をかけられるようにします。
反応があるまで待つ処理を実現させるために AWS SQS を使用することで処理の遅延実行が可能になります。
AWS SQSに遅延実行を追加する
AWS SQS にメッセージ(タスク)を追加する時に DelaySeconds
要素を指定することでことで指定した秒数の分だけ処理の遅延実行が可能になります。
まずはAWS SDK for JavaScript v3のAWS 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.handler
は src/sqs.ts の中の記述のexport const handler = async (event, context) => {}
(handler function) 以下にキューから取り出された時に関数が呼び出されることを指定しています。 -
${aws:region}
と${aws:accountId}
はそれぞれ変数が自動的に入力されます。serverless deploy
やserverless 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
参考
- キューへメッセージを送信する
- SQS Client - AWS SDK for JavaScript v3
- Using Lambda with Amazon SQS
- SQS Queues
- AWS LambdaのイベントソースにSQSを使う
Twilioを使って電話をかける
Twilioの設定
Twilioの利用を可能にするための各種設定をおこなっていきます。
まずはTwilioのNode.jsのライブラリであるTwilio Nodeをインストールするようにします。
yarn add twilio
次にTwilioの機能を使用するためにTwilioのコンソールにアクセスして ACCOUNT_SID
と AUTH_TOKEN
の値を確認する(後で使用します)
今回は電話をかけたりSMSを送ったりするのでTwilioで電話番号を購入します。
コンソールから Buy a number
タブを選択し、Buy
を選択して電話番号を購入します。
Twilioから電話をかける
ACCOUNT_SID
と AUTH_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',
});
参考
電話機からプッシュホンを受け取る
プッシュホンを受け取る場合は電話をかける時の twiml
に Gather
の要素を加えた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に投稿した入りなどの処理行います。
参考
- 音声を文字起こしする
- Transcribe Client - AWS SDK for JavaScript v3
- How to upload a stream to S3 with AWS SDK v3
- [Node.js] [AWS SDK v3] StreamでCSVファイルをS3からS3にコピーする
さいごに
実際のソースコードは疎結合ができるように細かいパーツ単位で分かれています。
またハッカソン内で作成したものであるので、ソースコードのりファクタなどを行う余地が十分にあるように感じています。リファクタによる改善にも取り組んでいきたいと思います。
また
においてはローカルでの開発環境を整えることができませんでした。
このためこれらのAWSサービスを使用する場合は環境構築の方法やノウハウの調査を進めて、ローカルでも開発ができるようにしていきたいと思います。
またはIAMの権限を制限して、その上で他のメンバーでもAWSのデプロイができるように情報の共有などを行えるようにしたいとおもいます。