[Amazon Lex] HTML+JavaScriptでLexクライアントを作ってみました(音声対応)

2019.03.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

こんにちは、AIソリューション部の平内(SIN)です。

前回、AWS SDK for JavaScriptを使用して、ブラウザで動作するAmazon Lex(以下、Lex)クライアントを作ってみました。

上記は、テキスト入力・テキスト出力のボットでしたが、今回は、マイクとスピーカで利用できるようにしてみたいと思います。

最初に動作しているようすです。

2 AWS SDK for JavaScript

AWS SDK for JavaScriptをブラウザで使用する場合、下記のようにscriptタグで利用可能です。

<script src="https://sdk.amazonaws.com/js/aws-sdk-2.283.1.min.js"></script>

後は、Cognitoで作成したプールID(後述)で初期化します。

AWS.config.region = 'us-east-1';
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: 'us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx',
});

PoolIdは、CognitoでLexへアクセスできる最低限のパーミッションを付与して発行しますが。、詳しくは、下記をご参照下さい。

[Amazon Lex] HTML+JavaScriptでLexクライアントを作ってみました

3 Lex

前回は、テキストの送受だったので postText()を使用しましたが、今回は、PostContent()を使用します。

利用の要領は、概ね以下のとおりです。

var lexruntime = new AWS.LexRuntime();
var params = {
  botAlias: botAlias, // エリアス
  botName: botName, // ボット名
  contentType: 'audio/x-l16; sample-rate=16000',// レスポンスのコンテンツタイプ  
  inputStream: blob, // 送信する音声データ
  userId: userId, // セッションのID
  accept: ’audio/mpeg’, // Acceptヘッダ 
  sessionAttributes: sessionAttributes // 属性情報
};

lexruntime.postContent(params, function (err, data) {
    if (err) {
        //エラー
    }
    if (data) {
        // Lexからのレスポンス
    }
});

Lexに関連する一連の処理は、Lexクラスに纏めました。詳しくは、下記をご参照下さい。

参考 https://github.com/furuya02/LexClient/blob/master/html/lex.js

4 録音

ブラウザでマイクからの音声を録音する主なコードは、以下のようになります。

const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const context = new AudioContext();
const source = context.createMediaStreamSource(stream);
const recorder = context.createScriptProcessor(4096, 1, 1);

recorder.onaudioprocess = ( e => {
    const sample = e.inputBuffer.getChannelData(0);
    buffers.append(sample);
});

NavigatorオブジェクトのmediaDevicesクラスからgetUserMedia()利用して、MediaStreamを取得します。この時点で、ブラウザでは、「マイクの利用の許可」を求めるポップアップが表示されます。引数でaudioを指定しているため、許可が必要なのはマイクだけです。

AudioContext()では、すべての音声の再生を管理します。 createMediaStreamSource()で先に作成したストリームをソースとして設定しています。

createScriptProcessor()では、直接音声データを扱うためのrecorderオブジェクトを生成します。ここで指定できるパラメータは、処理単位のバッファサイズ、入力チャンネル数及び、出力チャンネル数です。

recorderオブジェクトのonaudioprocessでは、ストリームから取得したデータで、バッファがいっぱいになった時のイベンントハンドラを設定できます。

ここでは、1チャンネルのみのデータを予め用意したデータ配列に、どんどん追加しているだけです。

録音終了時に、溜まったバッファからデータを取得するコードは、以下のとおりです。単純に連結して返しているだけです。

getData() {
    const buffer = new Float32Array(this._len);
    let offset = 0;
    this._buffers.forEach(buf =>{
      buffer.set(buf, offset);
      offset += buf.length;
    })
    return buffer;
}

録音に関連する一連の処理は、Recorder及びBufferクラスに纏めました。詳しくは、下記をご参照下さい。

参考 https://github.com/furuya02/LexClient/blob/master/html/recorder.js

実は、録音データの取得は、createScriptProcessor()でなくMediaRecorder()を使用すると、もっと簡単に取得できます。しかし、MediaRecorder()で取得できるデータは、ローデータでは無く、既にBlob形式となっているため、音の大きさなどを取得するのが逆に難しくなります。 今回は、録音入力が視覚的に分かりやすいように、インジケータ表示したかったので、あえて、ローデータを扱っています。

recorder = new MediaRecorder(stream);
let chunks = []; // Blob[] 
recorder.ondataavailable = function(e) {
    chunks.push(e.data);
};

5 データ変換

(1) サンプリング周波数の調整

ブラウザでマイク入力を録音する場合、ブラウザによってサンプルレートが決まってしまいます。 例えば、Chromeの場合、44.1kHzなのですが、Lexで処理できるのは、16KHzとなっているため、ダウンレートが必要です。

次のコードは、録音データ(ローデータ)のサンプリングレートを変換(ダウングレード)するものです。

static downRate(buffer, fromRate, toRate) {
    const rate = fromRate / toRate;
    const result = new Float32Array(Math.round(buffer.length / rate));
    let offsetResult = 0;
    let offsetBuffer = 0;
    while (offsetResult < result.length) {
    let nextOffsetBuffer = Math.round((offsetResult + 1) * rate);
    let accum = 0;
    let count = 0;
    for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
        accum += buffer[i];
        count++;
    }
    result[offsetResult] = accum / count;
    offsetResult++;
    offsetBuffer = nextOffsetBuffer;
    }
    return result;
}

(2) WAVファイルの生成

ローデータのままでは、Lexに送信出来ないため、WAVファイルへの変換が必要です。 下記では、ヘッダを追加して、WAVファイルを生成しています。

static createWav(samples, sampleRate) {
    const view = new DataView(new ArrayBuffer(44 + samples.length * 2));

    this._writeString(view, 0, 'RIFF');
    view.setUint32(4, 32 + samples.length * 2, true);
    this._writeString(view, 8, 'WAVE');
    this._writeString(view, 12, 'fmt ');
    view.setUint32(16, 16, true);
    view.setUint16(20, 1, true);
    view.setUint16(22, 1, true);
    view.setUint32(24, sampleRate, true);
    view.setUint32(28, sampleRate * 2, true);
    view.setUint16(32, 2, true);
    view.setUint16(34, 16, true);
    this._writeString(view, 36, 'data');
    view.setUint32(40, samples.length * 2, true);
    let offset = 44;
    for (var i = 0; i < samples.length; i++, offset += 2) {
        const s = Math.max(-1, Math.min(1, samples[i]));
        view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
    }
    return view;
}

static _writeString(view, offset, string) {
    for (var i = 0; i < string.length; i++) {
        view.setUint8(offset + i, string.charCodeAt(i));
    }
}

データ変換に関連する処理は、Converterクラスに纏めました詳しくは、下記をご参照下さい。

参考 https://github.com/furuya02/LexClient/blob/master/html/converter.js

6 インジケータ

今回作成したサンプルでは、マイク録音時にそのボリュームが分かりやすいように表示しています。

録音データのバッファが一杯になるたびに、その音量を元に表示しています。

recorder.onData = volume => {
  ringLight.level(volume);
}

インジケータに関する処理は、RingLightクラスで行っております。詳しくは、下記をご参照下さい。

参考 https://github.com/furuya02/LexClient/blob/master/html/ring-light.js

7 メイン

マウスが押されると(onStart)録音が開始さあれ、離すと(onStop)そのデータをLexに送ります。録音中は、データのバッファが一杯になるたびに、インジケータの表示を更新します。

一連の動作の主要な部分は、以下の通りです。

const recorder = new Recorder();
recorder.init();

recorder.onStart = () => {
  ringLight.start();
}

recorder.onStop = async (samples) => {
  ringLight.stop();
  const sampleRate = 16000; 
  const buffer = Converter.downRate(samples, recorder.sampleRate, sampleRate);
  const wav = Converter.createWav(buffer, sampleRate);
  // Lexへの送信
  const userId = 'UserId';
  const data = await lex.postContent(new Blob([wav]), userId);
  // テキストの表示
  console.log(data);
  appendLog(data.inputTranscript, 'req');
  appendLog(data.message, 'res');
  // レスポンスの再生
  var audio = new Audio();
  audio.src = window.URL.createObjectURL(new Blob([data.audioStream]));;
  audio.play();
}

recorder.onData = volume => {
  ringLight.level(volume);
}

メインの処理は、index.htmlのJavaScriptに記述されています。詳しくは、下記をご参照下さい。

参考 https://github.com/furuya02/LexClient/blob/master/html/index.html

8 最後に

今回は、HTML+JavaScriptを使用して、音声によるLexクライアントを作成してみました。

Lexとのやり取りは、全てSDKで処理されるため、ローデータを使用しなければ、非常に簡単に作成することができるでしょう。

「録音のインジケータを表示したい」など、ローデータを扱う必要が生じた場合に、本記事が何らかの参考になれば幸いです。

全てのコードは、下記に起きました。

github [GitHub] https://github.com/furuya02/LexClient/tree/master/html

9 参考リンク

ユーザーから音声データを取得する

[Amazon Lex] HTML+JavaScriptでLexクライアントを作ってみました

awslabs/aws-lex-browser-audio-capture -LexAudio

Amazon Lex ドキュメント

ブラウザ内の AWS SDK for JavaScript

WEB SOUNDER


弊社ではAmazon Connectのキャンペーンを行なっております。

3月に「無料Amazon Connectハンズオンセミナー」を開催致します。導入を検討されておられる方は、是非、お申し込み下さい。

また音声を中心とした各種ソリューションの開発支援も行なっております。