EC2 + SpringBoot でLINE@のBotアプリを作る その2
こんにちは。 エンジニアのNew塚本です。
最近、秋らしくなり寒くなってきましたが、 皆さまは如何お過ごしでしょうか。
さて、今回のお題は、前回ギブアップした「LINE@のボイスデータを音声解析してみる」のリベンジ編です。
結論から申し上げますと、中ハマりでリベンジ成功です!
まずは、実行結果の確認からどうぞ。
実行結果
LINEで「バナナ」とお話して送信してみます!

解析されてます!
他の人の音声ではどうなるか? 弊社KTNさんのダンディボイス?をゲットしました。
「しょがさんといえば」でチャレンジ!!

解析されてますが、50点ですねー
次!!
「パンケーキ食いたいっす」でチャレンジ!!

こちらも50点かなー
取り敢えず、ボイスデータ送信(LINE) → ボイスデータ取得 → Google Speech APIで音声認識 → 結果解析 → メッセージ送信(LINE)という一連のフローは完成しました。
では、前回はどこがイケてなかったのでしょうか??
【問題①】音声データの形式が悪かった
LINEから取得するボイスデータはmp4a形式ですが、Google Speech APIが解析できる音声データ形式とは異なっていました。そのため、ffmpegを使用して音声データ形式を、mp4a形式からflac形式に変換することにします。ffmpegは動画や音声の再生や変換ができるフリーソフトです。
$ffmpeg -i voice_1908.aac send.flac ffmpeg version 3.3.4 Copyright (c) 2000-2017 the FFmpeg developers Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'voice_1908.aac': Metadata: major_brand : mp42 minor_version : 0 compatible_brands: isommp42 creation_time : 2017-09-22T13:22:16.000000Z com.android.version: 6.0.1 Duration: 00:00:02.88, start: 0.000000, bitrate: 78 kb/sStream #0:0(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 16 kb/s# (default) Metadata: creation_time : 2017-09-22T13:22:16.000000Z handler_name : SoundHandle Stream mapping: Stream #0:0 -> #0:0 (aac (native) -> flac (native)) Press [q] to stop, [?] for help [flac @ 0x7f9c0e801800] encoding as 24 bits-per-sample Output #0, flac, to 'send.flac': Metadata: major_brand : mp42 minor_version : 0 compatible_brands: isommp42 com.android.version: 6.0.1 encoder : Lavf57.71.100Stream #0:0(eng): Audio: flac, 16000 Hz, mono, s32 (24 bit), 128 kb/s (default) Metadata: creation_time : 2017-09-22T13:22:16.000000Z handler_name : SoundHandle encoder : Lavc57.89.100 flac size=103kB time=00:00:02.88 bitrate= 293.3kbits/s speed= 269x video:0kB audio:95kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 8.625886%
flac形式に変換にした音声データを使ってAPIを実行すると、解析結果が返ってきました。
curl -X POST --data-binary @'./send.flac' --header 'Content-Type: audio/x-flac; rate=16000;' 'https://www.google.com/speech-api/v2/recognize?output=json&lang=ja-JP&key=登録されているAPIキー' {"result":[]} {"result":[{"alternative":[{"transcript":"おはようございます","confidence":0.99787414}],"final":true}],"result_index":0}
ちょっと前進。
【問題②】Google Speech APIとの通信方法
次に、前回のプログラムを少し変え、flac形式に変換した音声データを読み込み実行してみますが、解析されません。
まだ問題がありますね。今回は、ここからハマりました・・・
Google Speech API仕様には、「base64でエンコーディングした音声データをcontentに設定して送る」とありますが解析されません。他の設定パラメータを変更して試してみるとエラーとなるため、設定値は問題なさそうです。
音声データを読み込む時に壊してるか?と疑ってみます。
読み込んだ音声データ(byte配列)をもう一度ファイルに保存して、音声ファイルを実行してみます。
問題ありません。
うーん。
実質、APIには音声データしか送ってないし・・・
使用するクラスをURLConnectionとDataOutputStreamに変更し、Google Speech APIとのコネクションに読み込んだファイルのbyte配列を設定して送信する様に変更してみます。
・・・できた。
音声データの送り方?腑に落ちませんが成功です。
改修したプログラム①
LINEからボイスデータを取得し、一時ディレクトリに保存するメソッド。
private String getContents(String msgId) { String fileName = null; CloseableHttpClient httpClient = HttpClients.createDefault(); String targetUrl = appConfig.getContentsUrl().replace("{messageId}", msgId); HttpGet request = new HttpGet(targetUrl); request.addHeader("Authorization", "Bearer {%s}".replace("%s", appConfig.getChannelAccessToken())); CloseableHttpResponse response = null; try { // LINEGWからボイスデータを取得する response = httpClient.execute(request); HttpEntity entity = response.getEntity(); fileName = RandomStringUtils.randomAlphabetic(10); // 英数字10桁の乱数をファイル名にしてボイスデータを/tmpに出力 String filePath = "/tmp/" + fileName + ".aac"; Files.write(Paths.get(filePath), EntityUtils.toByteArray(entity)); httpClient.close(); EntityUtils.consume(entity); } catch (Exception ex) { ex.printStackTrace(); } return fileName; }
改修したプログラム②
音声データをffmpegでflac形式に変換、音声解析API実行し返却値を解析するメソッド。
private ListgoogleSpeech(String fileName) { StringBuilder urlBuff = new StringBuilder(); urlBuff.append(appConfig.getGoogleCloudSpeechApi()); urlBuff.append(appConfig.getGoogleApiKey()); List speechList = null; URL url; try { // LINEボイスデータ(aac形式)をflac形式に変換 Path filePath = Paths.get("/tmp/" + fileName + ".flac"); String cmd = "ffmpeg -i /tmp/" + fileName + ".aac" + " /tmp/" + fileName + ".flac"; Runtime runtime = Runtime.getRuntime(); Process process = runtime.exec(cmd); process.waitFor(); // GoogleCloudSpeechApiからの返却値を取得url = new URL(urlBuff.toString()); URLConnection urlConnection = url.openConnection(); HttpsURLConnection httpConnection = (HttpsURLConnection) urlConnection; httpConnection.setRequestMethod("POST"); httpConnection.setRequestProperty("Content-Type", "audio/x-flac; rate=16000"); httpConnection.setDoOutput(true); DataOutputStream outStream = new DataOutputStream(httpConnection.getOutputStream()); outStream.write(Files.readAllBytes(filePath)); outStream.flush(); outStream.close(); BufferedReader in = new BufferedReader(new InputStreamReader(httpConnection.getInputStream())); String inputLine; speechList = new ArrayList(); // 音声解析データの変換 while ((inputLine = in.readLine()) != null) { // 返却されたjsonの中に解析結果がなければスルー if (!inputLine.contains("alternative")) { continue; } // jsonデータのデシリアライズ RecieveData receivedata = new ObjectMapper().readValue(inputLine, RecieveData.class); for (Result result : receivedata.getResult()) { int limit = 1; for (Alternative msg : result.getAlternative()) { // 解析されたテキストデータを取得 speechList.add(msg.getTranscript()); limit++; // 1回にLINEへ送信するメッセージの最大値 if (5 < limit) { break; } } } } in.close(); } catch (Exception e) { e.printStackTrace(); } return speechList; }
感想
音声データ(圧縮形式)は得意な領域ではないので、これが絡んだ問題にハマりました。今回、解決はしましたが、圧縮形式についてはイマイチ興味が湧きません。ハマって得た知識は少なかったのですが、新しい引き出しができたのは収穫です。
おわり