イノベーション エンジニアブログ


株式会社イノベーションのエンジニアたちの技術系ブログです。ITトレンド・List Finderの開発をベースに、業務外での技術研究などもブログとして発信していってます!


このエントリーをはてなブックマークに追加

EC2 + SpringBoot でLINE@のBotアプリを作る その2

こんにちは。 エンジニアのNew塚本です。

最近、秋らしくなり寒くなってきましたが、 皆さまは如何お過ごしでしょうか。

さて、今回のお題は、前回ギブアップした「LINE@のボイスデータを音声解析してみる」のリベンジ編です。

結論から申し上げますと、中ハマりでリベンジ成功です!

まずは、実行結果の確認からどうぞ。

実行結果

LINEで「バナナ」とお話して送信してみます!

banana.png

解析されてます!

他の人の音声ではどうなるか? 弊社KTNさんのダンディボイス?をゲットしました。

「しょがさんといえば」でチャレンジ!!

syogasan.png

解析されてますが、50点ですねー

次!!

「パンケーキ食いたいっす」でチャレンジ!!

pancake.png

こちらも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/s
      Stream #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.100
      Stream #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 List googleSpeech(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;
}
感想

音声データ(圧縮形式)は得意な領域ではないので、これが絡んだ問題にハマりました。今回、解決はしましたが、圧縮形式についてはイマイチ興味が湧きません。ハマって得た知識は少なかったのですが、新しい引き出しができたのは収穫です。

おわり