LINEBot SDK＋Microsoft Speech API+Translate API でバリニーズと会話できるようにしてみる

<TABLE ALIGN=CENTER><TR><TD WIDTH=400px>
 <A HREF="http://qiita.com/momochan/items/3448febc260947226fbd" TARGET="_blank"><STRONG>Microsoft Translator APIを使ってLINEから翻訳させてみた - Qiita</STRONG></A>
 <A HREF="http://b.hatena.ne.jp/entry/http://qiita.com/momochan/items/3448febc260947226fbd" TARGET="_blank">
 <IMG BORDER="0" SRC="http://b.hatena.ne.jp/entry/image/http://qiita.com/momochan/items/3448febc260947226fbd" BORDER="0" ALT="" /></a>
 <BR>
 Microsoft Translator APIを使ってLINEから翻訳させてみた - Qiita... 
 </TD>
 <TD WIDTH=150px>
 <IMG STYLE="float:left;margin-right:.5em;" SRC="http://capture.heartrails.com/120x120/cool?http://qiita.com/momochan/items/3448febc260947226fbd" ALT="" WIDTH="120" HEIGHT="120" />
 </TD></TR>
</TABLE>
先日以下記事をFacebookで投げたところ、友達から『音声入力はできないんだっけ？』というコメントが。
そもそもLINEでボイスメッセージを送ったことがなかったが、Messaging APIのドキュメントを見るとAudioも受信できるようであったので早速試してみた。
※この記事は音声＋翻訳の技術要素に寄った内容となるのでライブラリインストール・契約方法などは各自他で探ってください。

# 構想
LINEから日本語でボイスメッセージを受信し、翻訳された文字列をLINEにリプライするという言葉でいうと単純なもの。
方法としては二つあると考えた。

- ボイスメッセージを文字に変換し、前回利用したMicrosoft Translate API経由で翻訳する
- ボイスメッセージを直接翻訳できるAPI経由で翻訳する

少し調べた結果、今回は上の一旦文字に変換して翻訳することにした。
理由としては音声から文字への変換レベルを検証したことが大きな理由だ。

# 利用API
翻訳APIはTranslate APIを利用した。
音声から文字列への変換はいろいろと調べた結果、Microsoft Bing Speech APIを選択した。
候補としては他に

- [Julius](http://julius.osdn.jp/)
- [Google Specch API](https://cloud.google.com/speech/)
- [docomo 音声認識 API](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=speech_recognition&p_name=sdk)

あたりを探った。音声は日本語と言う事でdocomo 音声認識APIが第一候補であったが利用期間やプラットフォームの制限で諦めた。
Microsoftのは5,000/月まで無料利用ができるというのも大きかった。

# 環境
- AWS EC2
- PHP5.6
- LINEBot SDK

# LINEBotでボイスメッセージを受信
LINEBot Messaging APIの記事は数あれど、なかなかボイスメッセージの記事はなかった。
しかしLINEがSDKでexampleを提供してくれていたのでそれを見ながら試してみた。

# LINEBot SDK導入

```bash
$composer require linecorp/line-bot-sdk
```

# ボイスメッセージ受信
LINEからボイスメッセージを受信する部分を切り出してみた。
注意してほしいのはBodyで取得できる音声形式はm4aということだ。これで少しはまった。

```php:callback.php
<?php

define("LINE_MESSAGING_API_CHANNEL_SECRET", '<YOUR_SECRET>');
define("LINE_MESSAGING_API_CHANNEL_TOKEN", '<YOUR_ACCESS_TOKEN>');

require __DIR__."/vendor/autoload.php";


$bot = new \LINE\LINEBot(
    new \LINE\LINEBot\HTTPClient\CurlHTTPClient(LINE_MESSAGING_API_CHANNEL_TOKEN),
    ['channelSecret' => LINE_MESSAGING_API_CHANNEL_SECRET]
);


$signature = $_SERVER["HTTP_".\LINE\LINEBot\Constant\HTTPHeader::LINE_SIGNATURE];
$body = file_get_contents("php://input");

$events = $bot->parseEventRequest($body, $signature);

foreach ($events as $event) {
    if ($event instanceof \LINE\LINEBot\Event\MessageEvent\AudioMessage) {

      $contentId = $event->getMessageId();
      $audio = $bot->getMessageContent($contentId)->getRawBody();

      $tmpfilePath = tempnam('/tmp', 'audio-');
      unlink($tmpfilePath);
      $filePath = $tmpfilePath . '.m4a';
      $filename = basename($filePath);

      $fh = fopen($filePath, 'x');
      fwrite($fh, $audio);
      fclose($fh);

（以下略）
```

# Speech To Text
tmpに保存した音声ファイルをBing Speech APIに投げてテキスト変換を行う。
APIに投げる際の音声ファイル形式はwavということに注意してほしい。
今回はEC2にインストールしたffmpegコマンドでm4aからwavファイルに変換した。
API仕様は[ここ](https://www.microsoft.com/cognitive-services/en-us/speech-api/documentation/overview)にあるので参考に。

STTのソースはきれいではないがこちらも。
流れとしては

- m4aからwavに変換
- Bing Speech APIのAccess Token取得
- APIで変換

```php:callback.php
      exec("ffmpeg -i /tmp/".$filename." -vn -acodec pcm_s16le -ac 2 -ar 44100 -f wav /tmp/".basename( $filename, ".m4a").".wav");

      $data = "/tmp/".basename( $filename, ".m4a").".wav";
      $text = speechToText($data, 'ja-JP');


（中略）



function speechToText($filepath, $lang) {
  $access_token = getSpeechAccessToken();

  $sttServiceUri = "https://speech.platform.bing.com/recognize/query";
           $sttServiceUri .= @"?scenarios=ulm";                                  // websearch is the other main option.
           $sttServiceUri .= @"&appid=D4D52672-91D7-4C74-8AD8-42B1D98141A5";     // You must use this ID.
           $sttServiceUri .= @"&locale=".$lang;                                   // We support several other languages.  Refer to README file.
           $sttServiceUri .= @"&device.os=wp7";
           $sttServiceUri .= @"&version=3.0";
           $sttServiceUri .= @"&format=json";
           $sttServiceUri .= @"&instanceid=565D69FF-E928-4B7E-87DA-9A750B96D9E3";
           $sttServiceUri .= @"&requestid=".guid();
   $ctx = stream_context_create(array(
       'http' => array(
           'timeout' => 1
           )
       )
   );
  $data = file_get_contents($filepath, 0, $ctx);  //your wave file path here

  $options = array(
   'http' => array(
       'header'  => "Content-type: audio/wav; samplerate=8000\r\n" .
                   "Authorization: "."Bearer ".$access_token."\r\n",
       'method'  => 'POST',
       'content' => $data,
       ),
   );

   $context  = stream_context_create($options);

 // get the response result
   $result = file_get_contents($sttServiceUri, false, $context);
   if (!$result) {
        return "tranlate missed!";
     }
   else{
       $arr = json_decode($result);

       return $arr->results[0]->lexical;
   }

}

function guid(){
  if (function_exists('com_create_guid')){
      return com_create_guid();
  }else{
      mt_srand((double)microtime()*10000);
      $charid = strtoupper(md5(uniqid(rand(), true)));
      $hyphen = chr(45);// "-"
      $uuid = substr($charid, 0, 8).$hyphen
              .substr($charid, 8, 4).$hyphen
              .substr($charid,12, 4).$hyphen
              .substr($charid,16, 4).$hyphen
              .substr($charid,20,12);
      return $uuid;
  }
}

// Bing Speech API用のAccessToken
function getSpeechAccessToken() {
  $AccessTokenUri = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken";

  $apiKey = "<YOUR_KEY>";
  $ttsHost = "https://speech.platform.bing.com";

  $options = array(
      'http' => array(
          'header'  => "Ocp-Apim-Subscription-Key: ".$apiKey."\r\n" .
          "content-length: 0\r\n",
          'method'  => 'POST',
      ),
  );
  $context  = stream_context_create($options);

  //get the Access Token
  return file_get_contents($AccessTokenUri, false, $context);
}
```

# 翻訳
今回はバリニーズと話すことを目的としているので（なぜ？）インドネシア語に変換してみた。
前回書いたソースと同じ。

```php:callback.php

      $reply_token = $event->getReplyToken();
      $bot->replyText($reply_token, translation($text));

    }
    else if($event instanceof \LINE\LINEBot\Event\MessageEvent\TextMessage) {
      $reply_token = $event->getReplyToken();
      $bot->replyText($reply_token, translation($event->getText()));
    }
    else {
      $reply_token = $event->getReplyToken();
      $bot->replyText($reply_token, 'ありがとう！　"Terima Kashi!"');
    }
}

function translation($text) {
  $client_id = "<YOUR_ID>";
  $client_secret = "<YOUR_SECRET>";
  $access_token = getTranslateAccessToken($client_id, $client_secret)->access_token;

  $text = mb_convert_kana($text, 'KVas');

  $from = '';
  $to = '';
  if (preg_match('/[ぁ-んァ-ヶー一-龠、。]/u',$text)) {
    $from = 'ja'; //日本語
    $to = 'id'; //インドネシア語
  }
  else{
    $from = 'id';
    $to = 'ja';
  }


  return tranlator($access_token, array(
          'text' => $text,
          'to' => $to,
          'from' => $from));
}

// 翻訳実行
function tranlator($access_token, $params){
      $ch = curl_init();
      curl_setopt_array($ch, array(
          CURLOPT_URL => "https://api.microsofttranslator.com/v2/Http.svc/Translate?".http_build_query($params),
          CURLOPT_SSL_VERIFYPEER => false,
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HEADER => true,
          CURLOPT_HTTPHEADER => array(
              "Authorization: Bearer ". $access_token),
          ));
      preg_match('/>(.+?)<\/string>/',curl_exec($ch), $m);
      return $m[1];
}

// Microsoft Translate API用のAccessToken
function getTranslateAccessToken($client_id, $client_secret, $grant_type = "client_credentials", $scope = "http://api.microsofttranslator.com"){
  $ch = curl_init();
  curl_setopt_array($ch, array(
      CURLOPT_URL => "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13/",
      CURLOPT_SSL_VERIFYPEER => false,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => http_build_query(array(
          "grant_type" => $grant_type,
          "scope" => $scope,
          "client_id" => $client_id,
          "client_secret" => $client_secret
          ))
      ));
  return json_decode(curl_exec($ch));
}
```

# 結果
結果、見事に思うような結果が返ってきた。
これでバリに行っても安心である。

![15578549_1308510622525312_12722929064009997_n.jpg](https://qiita-image-store.s3.amazonaws.com/0/120983/32c9ec90-ce7c-2f30-19d4-c512e365e1e9.jpeg)

友達とBotとグループを組めばリアルタイム翻訳ちっくなこともできる。若干見づらいかもしれないけれど
も。

![15589833_1308733472503027_4321714413467263664_n.jpg](https://qiita-image-store.s3.amazonaws.com/0/120983/454ea767-e462-55e5-5677-3dc02cf7f33a.jpeg)



# フローチャート
今回のフローは以下通り。
![Flowchart.png](https://qiita-image-store.s3.amazonaws.com/0/120983/b54e22df-5bdd-4a09-941a-24a61814c25b.png)


# 全体ソース
```php:callback.php
<?php

define("LINE_MESSAGING_API_CHANNEL_SECRET", '<YOUR_SECREt>');
define("LINE_MESSAGING_API_CHANNEL_TOKEN", '<YOUR_TOKEN>');

require __DIR__."/vendor/autoload.php";


$bot = new \LINE\LINEBot(
    new \LINE\LINEBot\HTTPClient\CurlHTTPClient(LINE_MESSAGING_API_CHANNEL_TOKEN),
    ['channelSecret' => LINE_MESSAGING_API_CHANNEL_SECRET]
);


$signature = $_SERVER["HTTP_".\LINE\LINEBot\Constant\HTTPHeader::LINE_SIGNATURE];
$body = file_get_contents("php://input");

$events = $bot->parseEventRequest($body, $signature);

foreach ($events as $event) {
    if ($event instanceof \LINE\LINEBot\Event\MessageEvent\AudioMessage) {

      $contentId = $event->getMessageId();
      $audio = $bot->getMessageContent($contentId)->getRawBody();

      $tmpfilePath = tempnam('/tmp', 'audio-');
      unlink($tmpfilePath);
      $filePath = $tmpfilePath . '.m4a';
      $filename = basename($filePath);

      $fh = fopen($filePath, 'x');
      fwrite($fh, $audio);
      fclose($fh);


      //$data = '/tmp/'.$filename;
      exec("ffmpeg -i /tmp/".$filename." -vn -acodec pcm_s16le -ac 2 -ar 44100 -f wav /tmp/".basename( $filename, ".m4a").".wav");

      $data = "/tmp/".basename( $filename, ".m4a").".wav";
      $text = speechToText($data, 'ja-JP');

      $reply_token = $event->getReplyToken();
      $bot->replyText($reply_token, translation($text));

    }
    else if($event instanceof \LINE\LINEBot\Event\MessageEvent\TextMessage) {
      $reply_token = $event->getReplyToken();
      $bot->replyText($reply_token, translation($event->getText()));
    }
    else {
      $reply_token = $event->getReplyToken();
      $bot->replyText($reply_token, 'ありがとう！　"Terima Kashi!"');
    }
}

// Bing Speech API用のAccessToken
function getSpeechAccessToken() {
  $AccessTokenUri = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken";

  $apiKey = "<YOUR_KEY>";
  $ttsHost = "https://speech.platform.bing.com";

  $options = array(
      'http' => array(
          'header'  => "Ocp-Apim-Subscription-Key: ".$apiKey."\r\n" .
          "content-length: 0\r\n",
          'method'  => 'POST',
      ),
  );
  $context  = stream_context_create($options);

  //get the Access Token
  return file_get_contents($AccessTokenUri, false, $context);
}

// Microsoft Translate API用のAccessToken
function getTranslateAccessToken($client_id, $client_secret, $grant_type = "client_credentials", $scope = "http://api.microsofttranslator.com"){
  $ch = curl_init();
  curl_setopt_array($ch, array(
      CURLOPT_URL => "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13/",
      CURLOPT_SSL_VERIFYPEER => false,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => http_build_query(array(
          "grant_type" => $grant_type,
          "scope" => $scope,
          "client_id" => $client_id,
          "client_secret" => $client_secret
          ))
      ));
  return json_decode(curl_exec($ch));
}

function speechToText($filepath, $lang) {
  $access_token = getSpeechAccessToken();

  $sttServiceUri = "https://speech.platform.bing.com/recognize/query";
           $sttServiceUri .= @"?scenarios=ulm";                                  // websearch is the other main option.
           $sttServiceUri .= @"&appid=D4D52672-91D7-4C74-8AD8-42B1D98141A5";     // You must use this ID.
           $sttServiceUri .= @"&locale=".$lang;                                   // We support several other languages.  Refer to README file.
           $sttServiceUri .= @"&device.os=wp7";
           $sttServiceUri .= @"&version=3.0";
           $sttServiceUri .= @"&format=json";
           $sttServiceUri .= @"&instanceid=565D69FF-E928-4B7E-87DA-9A750B96D9E3";
           $sttServiceUri .= @"&requestid=".guid();
   $ctx = stream_context_create(array(
       'http' => array(
           'timeout' => 1
           )
       )
   );
  $data = file_get_contents($filepath, 0, $ctx);  //your wave file path here

  $options = array(
   'http' => array(
       'header'  => "Content-type: audio/wav; samplerate=8000\r\n" .
                   "Authorization: "."Bearer ".$access_token."\r\n",
       'method'  => 'POST',
       'content' => $data,
       ),
   );

   $context  = stream_context_create($options);

 // get the response result
   $result = file_get_contents($sttServiceUri, false, $context);
   if (!$result) {
        return "tranlate missed!";
     }
   else{
       $arr = json_decode($result);

       return $arr->results[0]->lexical;
   }

}

function guid(){
  if (function_exists('com_create_guid')){
      return com_create_guid();
  }else{
      mt_srand((double)microtime()*10000);
      $charid = strtoupper(md5(uniqid(rand(), true)));
      $hyphen = chr(45);// "-"
      $uuid = substr($charid, 0, 8).$hyphen
              .substr($charid, 8, 4).$hyphen
              .substr($charid,12, 4).$hyphen
              .substr($charid,16, 4).$hyphen
              .substr($charid,20,12);
      return $uuid;
  }
}

function translation($text) {
  $client_id = "<YOUR_ID>";
  $client_secret = "<YOUR_SECRET>";
  $access_token = getTranslateAccessToken($client_id, $client_secret)->access_token;

  $text = mb_convert_kana($text, 'KVas');

  $from = '';
  $to = '';
  if (preg_match('/[ぁ-んァ-ヶー一-龠、。]/u',$text)) {
    $from = 'ja';
    $to = 'id';
  }
  else{
    $from = 'id';
    $to = 'ja';
  }


  return tranlator($access_token, array(
          'text' => $text,
          'to' => $to,
          'from' => $from));
}

// 翻訳実行
function tranlator($access_token, $params){
      $ch = curl_init();
      curl_setopt_array($ch, array(
          CURLOPT_URL => "https://api.microsofttranslator.com/v2/Http.svc/Translate?".http_build_query($params),
          CURLOPT_SSL_VERIFYPEER => false,
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HEADER => true,
          CURLOPT_HTTPHEADER => array(
              "Authorization: Bearer ". $access_token),
          ));
      preg_match('/>(.+?)<\/string>/',curl_exec($ch), $m);
      return $m[1];
}
```

事前にQiitaで晒して大丈夫かわからないが、LINE BOT AWARDS にこれで応募してみた。