[Amazon Connect] 期限の制限に縛られることなく、録音データを検索・再生する仕組みを作ってみました

2022.01.10

1 はじめに

IoT事業部の平内(SIN)です。

Amazon Connect(以下、Connect)では、顧客とエージェントの会話を簡単に録音して残すことができます。また、通話の詳細情報は、「コンタクト追跡レコード」として保持されており、検索したり、紐ずく録音を簡単再生したりする事ができます。

しかし、この機能は、残念ながら保持期間に制限があり、2年以上前の記録を閲覧することができません。

今回は、この期間の制限に縛られることなく、過去の録音も再生できる仕組みを作ってみました。

最初に、ブラウザで検索・再生している様子です。

S3に保存されたCTR「コンタクト追跡レコード」は、Athenaで検索していますが、日付及び時間がパーティションとして設定されており、比較的高速に検索できていると思います。

2 構成

構成は、以下の通りです。

  • ① CTR「コンタクト追跡レコード」は、Kinesis Firehose経由でS3に保存します
  • ② 録音データは、指定したS3バケットに保存します
  • ③ Amplifyで構成されたVueによるページを使用して、検索及び再生を行います
  • ④ 日付や条件を指定して、AthenaでCTRを検索して返します
  • ⑤ 再生データは、S3から期限つきURLを発行します
  • ⑥ ページの閲覧は、Amplifyの機能でCognito認証が必要になります(今回実装していません)
  • ⑦ 認証情報に基づいた閲覧記録は、DynamoDBに保存されます(今回実装していません)

⑥及び、⑦は、事後の拡張として設計(考慮)だけ行ってますが、今回未実装となっています。

3 構築手順

構築の手順は、以下の通りです。

(1) S3

「CTR保存」、「録音保存」及び、Athenaの作業領域」として、3つのバケットを用意しました。

  • amazon-connect-ctr-20220111(CTR用)
  • amazon-connect-call-recordings-20220111(録音データ用)
  • amazon-connect-ctr-athena-query-results-20220111(Athenaテンポラリ)

(2) Kinesis Data Firehose

CTRを保存用に、入力をDirect PUT、出力をS3バケットとして、Kinesis Data Firehoseのストリームを作成します。

比較的早く検索可能になるように、バッファは、1分1Mとしました。

(3) Amazon Connect

インスタンスの設定で、データストリーミング有効にし、作成したKinesis Data Firehoseへ送ります。

Data Strage では、録音データの保存場所を指定できます。

コンタクトフローでは、「記録と分析の動作を設定」ブロックを置いて、エージェントと顧客の両方を保存します。

(4) 保存確認

何回か電話して、保存の状況を確認しています。

  • WAV

バケット(amazon-connect-call-recordings-20220111)の中に録音ファイルが保存されています。

  • CTR(1M 60秒ごとに保存なので、ちょっと待ちます)

バケット(amazon-connect-ctr-20220111)の中にCTRファイルが保存されています。

保存されたCTRファイルには、以下のようなイメージですが、詳しくは、問い合わせレコードデータモデルをご参照ください。

{
    "AWSAccountId": "439028474478",
    "AWSContactTraceRecordFormatVersion": "2017-03-10",
    "Agent": {
        "ARN": "arn:aws:connect:xxxxxx",
        (略)
        "Username": "agent001"
    },
        (略)
    "Channel": "VOICE",
    "ConnectedToSystemTimestamp": "2022-01-06T18:15:57Z",
    "CustomerEndpoint": {
        "Address": "+8111112222",
        "Type": "TELEPHONE_NUMBER"
    },
    "DisconnectReason": "AGENT_DISCONNECT",
    "DisconnectTimestamp": "2022-01-06T18:16:17Z",
    "InitiationMethod": "INBOUND",
        (略)
    "Queue": {
        (略)
        "Name": "BasicQueue"
    },
    "Recording": {
        "Location": "amazon-connect-call-recordings-20220111/xxxxxx/xxxx.wav",
        "Type": "AUDIO"
    },
        (略)
    "SystemEndpoint": {
        "Address": "+815011112222",
        "Type": "TELEPHONE_NUMBER"
    },
        (略)
}

(5) Athena

CTRを検索するためのテーブルを作成します。

問い合わせレコードデータモデルからキーとなる名前を全部定義しています。

また、費用及び高速化のため、Kinesis Firehoseがデフォルトで作成する年月日及び時刻をパーティションキーとして設定しました。

CREATE EXTERNAL TABLE IF NOT EXISTS `default`.`ctr_table` (
  Agent string,
  AgentConnectionAttempts int,
  Attributes string,
  AWSAccountId string,
  AWSContactTraceRecordFormatVersion string,
  Channel string,
  ConnectedToSystemTimestamp string,
  ContactId string,
  CustomerEndpoint string,
  DisconnectTimestamp string,
  DisconnectReason string,
  InitialContactId string,
  InitiationMethod string,
  InitiationTimestamp string,
  InstanceARN string,
  LastUpdateTimestamp string,
  MediaStreams string,
  NextContactId string,
  PreviousContactId string,
  Queue string,
  Recording string,
  Recordings string,
  SystemEndpoint string,
  TransferCompletedTimestamp string,
  TransferredToEndpoint string
)
PARTITIONED BY (
  year int,
  month int,
  day int,
  hour int
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' 
WITH SERDEPROPERTIES (
  'serialization.format' = '1'
) LOCATION 's3://amazon-connect-ctr-20220111/'
TBLPROPERTIES (
  'has_encrypted_data'='false',

  'projection.enabled' = 'true',

  'projection.year.type' = 'integer',
  'projection.year.range' = '2021,2100',
  'projection.year.digits' = '4',

  'projection.month.type' = 'integer',
  'projection.month.range' = '1,12',
  'projection.month.digits' = '2',

  'projection.day.type' = 'integer',
  'projection.day.range' = '1,31',
  'projection.day.digits' = '2',
   
  'projection.hour.type' = 'integer',
  'projection.hour.range' = '0,23',
  'projection.hour.digits' = '2',
  
  'storage.location.template' = 's3://amazon-connect-ctr-20220111/${year}/${month}/${day}/${hour}'
);

作成したテーブルでクエリしてみた様子です。

SELECT 
    ConnectedToSystemTimestamp,
    DisconnectTimestamp,
    Channel,
    ContactId,
    json_extract(Queue, '$.name') AS Queue,
    json_extract(Agent, '$.username') AS AgentName,
    json_extract(Recording, '$.location') AS RecordingLocation,
    json_extract(CustomerEndpoint, '$.address') AS CustomerEndpoint,
    json_extract(SystemEndpoint, '$.address') AS SystemEndpoint
FROM "default"."ctr_table"
WHERE year = 2022 and month=1 and day=7
limit 10

(6) Amplify

Vueのプロジェクト作成後に、Amplifyを初期化しています。

% vue create recording-search
% cd recording-search
% amplify init

apiを追加して、 RESTを選択し、バックエンドには、Hello WorldLambdaを置くようにしました。

% amplify add api
? Please select from one of the below mentioned services: REST
? Provide a friendly name for your resource to be used as a label for this category in the project: RecordingSearchApi
? Provide a path (e.g., /book/{isbn}): /recording
? Choose a Lambda source Create a new Lambda function
? Provide an AWS Lambda function name: RecordingSearchFunction
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World

? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? No
? Restrict API access No
? Do you want to add another path? No

% amplify push

REST API endpoint: https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev

以下は、Lambdaの主要なコードです。

const Ctr = require('./ctr');
const s3 = require('./s3');

function getParams(bodyString){
  const body = JSON.parse(bodyString);
  const params = {};
  const keys = ['date', 'hour', 'initiationMethod', 'channel', 'queue', 'agentName', 'url', 'user'];
  keys.forEach(key =>{
    params[key] = (body[key]=='')?undefined:body[key];
  })

  if(params['date']){
    const [year, month, day] = params['date'].split('-');
    params['year'] = parseInt(year);
    params['month'] = parseInt(month);
    params['day'] = parseInt(day);
    if(params['hour']){
      params['hour'] = parseInt(params['hour']);
    }
  }

  return params;
}

exports.handler = async (event) => {

  console.log(JSON.stringify(event));
  
  const params = getParams(event['body']);
  console.log(params);

  let body = {};
  let statusCode = 403;

  if(event['httpMethod'] === 'POST'){
    if(event["path"] == "/recording/list"){
      const ctr = new Ctr();
      body = await ctr.query(params);
      statusCode = 200;
    }else if(event["path"] == "/recording/audio"){
      statusCode = 200;
      const audioSrc = s3.getSignedUrl(params.url)
      body = {
        "audioSrc": audioSrc
      }
    }
  }

  console.log(`body: ${JSON.stringify(body)}`)
  const response = {
    statusCode: statusCode,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': '*'
    }, 
    body: JSON.stringify(body),
  };
  return response;
};

Vue側のコードなど、すべてをここで紹介しきれませんが、詳しくは、下記をご参照ください。
https://github.com/furuya02/recording-search

4 最後に

今回作成した仕組みでは、CTRも録音データ(wav)も、S3上に置かれ、自己管理となっていますので。運用に合わせて、保存期間を決定できます。

閲覧ページの認証は、Amplifyの機能を使用すれば、比較的簡単に実装できると思います。また、認証情報と共に、閲覧(録音を聞いた)記録などを保管してするようにすれば、なんとか実用レベルに持っていくことも可能かもしれません。