読者です 読者をやめる 読者になる 読者になる

Raspberry Pi でオレオレAmazonEcho(もしくはGoogleHomeもしくはSiri)を作ってみた

Raspberry piでAmazon Echo(AVS)を動かしてみる未遂 - Qiita
Raspberry piAmazon Echo(AVS)を動かしてみる未遂 - Qiita...

こちらで書いた記事は有力な情報が見つからず相変わらず同じ状態。 その状況を打破すべく、オレオレAmazonEchoを作ってみることにした。

オレオレAmazonEchoにやらせたいこと

とにかくラジオが好きだ。よく聞いている。朝はTBSのスタンバイを聞くことが最近の日課だ。 6:42から始まる歌のない歌謡曲は最高だ。 しかし寝起きにそのあたりに転がっているiPhoneを取り出し、radikoアプリを起動し、TBSを選択し、Airplayを設定するのはとてもめんどくさい。 となればラズパイをradikoサーバにでもしてcronで自動起動させればよいじゃないかと思うのだが、自動は嫌だ。

と言う事で話しかければradikoが鳴ることを最終目標にオレオレAmazonEchoを作ることにした。

環境

raspberry piでサウンド関係はちょっとめんどくさいので事前に調べて設定しておく。

音声認識

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

音声認識にはMicosoft Speech APIを利用しようかと思ったけれども、ローカルで動く Juliusというものを知ったので利用する事とした。

Juliusのインストール

Juliusをgithubからインストールし、ラズパイで音声認識できるまでもっていく。

# 依存ライブラリインストール
$ sudo apt-get install libasound2-dev
# Juliusインストール
$ git clone https://github.com/julius-speech/julius.git
$ cd julius
$ ./configure 
$ make
$ sudo make install

ディクテーションキットとディクショナリーキットのインストール

$ wget -O dictation-kit-v4.3.1-linux.tgz
 http://sourceforge.jp/frs/redir.php?m=jaist&f=%2Fjulius%2F60416%2Fdictation-kit-v4.3.1-linux.tgz'
$ wget -O grammar-kit-v4.1.tar.gz
 'http://sourceforge.jp/frs/redir.php?m=osdn&f=%2Fjulius%2F51159%2Fgrammar-kit-v4.1.tar.gz'

そのまま使ってもよいが自分で辞書を作成するとそこに記載の言葉しか認識しないので、今回の要件にマッチするため辞書を作成する。

辞書の作成

$ vi /home/pi/julius_sample/sample.yomi
$ cat /home/pi/julius_sample/sample.yomi                                                                                                                
シシマル ししまる
エヌエチケー えぬえちけー
JFM じぇーうぇーぶ
TBS てぃーびーえす
INT いんたーえふえむ
消して けして

辞書の変換

$ cd ~/julius-master
$ iconv -f utf8 -t eucjp ~/sample.yomi | ./yomi2voca.pl 
 > ~/julius-kits/dictation-kit-v4.3.1-linux/sample.dic

julius conf作成

$ vi /home/pi/julius_sample/sample.jconf
$ cat /home/pi/julius_sample/sample.jconf
-w sample.dic
-v model/lang_m/bccwj.60k.bingram
-h model/phone_m/jnas-tri-3k16-gid.binhmm
-hlist model/phone_m/logicalTri
-n 5
-output 1
-input mic
-rejectshort 500
-charconv euc-jp utf8
-lv 1000

juliusをモジュールモードで起動

$ julius -C /home/pi/julius-kits/dictation-kit-v4.4/sample.jconf -module

radiko

無事辞書にある言葉が認識したらraspberry piradiko再生を行える環境を整える。 今回はこちらを利用させて頂いた。

Raspberry Piでradikoの再生、録音 - Muchuu
Raspberry Piradikoの再生、録音 - Muchuu...

プログラム

juliusが音声認識を行ってくれるが、それを受けるプログラムを作ることにする。 PHPで書きたかったが情報があまりなかったのでpythonを利用することにした。 ただし僕はpythonを書けないので何人かの方のサンプルを組合させていただいた。 (ちなみにシシマルとは昔飼ってた犬の名前。日常生活もしくはラジオ・テレビから発せられることはあまりないと思い、誤認識はしないと思ったから) 流れ

  1. pythonからjuliusを起動させる。
  2. シシマルという言葉を認識すると待機状態に入る(Hey SiriやAlexaのようなこと)
  3. 待機状態で辞書に登録された言葉を認識すると各々の動作を行う(今回はラジオを再生する)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import socket
import requests
import re
import subprocess
import shlex
import time
import threading

julius_path = '/home/pi/src/julius-master/julius/julius'
jconf_path = '/home/pi/julius-kits/dictation-kit-v4.4/sample.jconf'
julius = None
julius_socket = None


def invoke_julius():
    print 'INFO : invoke julius'
    args = julius_path + ' -C ' + jconf_path + ' -module '
    p = subprocess.Popen(
            shlex.split(args),
            stdin=None,
            stdout=None,
            stderr=None
        )
    print 'INFO : invoke julius complete.'
    print 'INFO : wait 2 seconds.'
    time.sleep(3.0)
    print 'INFO : invoke julius complete'
    return p


def kill_julius(julius):
    print 'INFO : terminate julius'
    julius.kill()
    while julius.poll() is None:
        print 'INFO : wait for 0.1 sec julius\' termination'
        time.sleep(0.1)
    print 'INFO : terminate julius complete'


def get_OS_PID(process):
    psef = 'ps -ef | grep ' + process + ' | grep -ve grep -vie python |head -1|awk \'{print($2)}\''
    if sys.version_info.major == 3:
        PID = str(subprocess.check_output(psef, shell=True), encoding='utf-8').rstrip ()
    else:
        PID = subprocess.check_output(psef, shell=True).rstrip ()
    return PID


def create_socket():
    print 'INFO : create a socket to connect julius'
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('localhost', 10500))
    print 'INFO : create a socket to connect julius complete'
    return s


def delete_socket(s):
    print 'INFO : delete a socket'
    s.close()
    print 'INFO : delete a socket complete'
    return True


def invoke_julius_set():
    julius = invoke_julius()
    julius_socket = create_socket()
    sf = julius_socket.makefile('rb')
    return (julius, julius_socket, sf)

STATUS_WAITING   = 0
STATUS_READY     = 1
STATUS_EXECUTING = 2
STATUS_FINISH    = 3
 
status = STATUS_WAITING
 
def status_change(new_status):
  global status
  if status == STATUS_WAITING:
    if new_status == STATUS_READY:
      status = new_status
      print "Status is READY." 
      threading.Timer(15, status_change, args=[STATUS_WAITING]).start()
  elif status == STATUS_READY:
    if new_status == STATUS_WAITING:
      status = new_status
      print "Status is WAITING." 
    elif new_status == STATUS_EXECUTING:
      status = new_status
      print "Status is EXECUTING." 
      threading.Timer(30, status_change, args=[STATUS_FINISH]).start()
  elif status == STATUS_EXECUTING:
    if new_status == STATUS_WAITING:
      print "Command is executed now, so I do NOT change status."
    if new_status == STATUS_FINISH:
      status = STATUS_WAITING
      print "Status is WAITING." 

      
def play_mp3(sound_file):
    args =  "mplayer --quiet " + sound_file
    print args
    p = subprocess.Popen(
            shlex.split(args),
            stdin=None,
            stdout=None,
            stderr=None
        )
    time.sleep(0.5)
    return True

def call_jsay(text):
    cmd = "/usr/local/bin/jsay %s" % text
    subprocess.call(cmd, shell=True)

def radio_on(channel):
    cmd ="/home/pi/radiko/radiko.sh -p %s >/dev/null 2>&1 &" %channel
    subprocess.call(cmd, shell=True)

def nhk_on(channel):
    cmd ="/home/pi/radiko/nhk.sh %s >/dev/null 2>&1 &" %channel
    subprocess.call(cmd, shell=True)


def radio_off():
    # off
    cmd ="killall mplayer"
    subprocess.call(cmd, shell=True)

def main():
    global julius
    global julius_socket
    julius, julius_socket, sf = invoke_julius_set()

    while True:
        if julius.poll() is not None:   # means , julius dead
            print 'ERROR : julius dead'
            delete_socket(julius_socket)
            julius, julius_socket, sf = invoke_julius_set()
        else:
            line = sf.readline().decode('utf-8')
            if line.find('WHYPO') != -1:
               if line.find(u'シシマル') != -1:
                     if status == STATUS_WAITING:
                           print "Julius is ready to your command."
                           status_change(STATUS_READY)
                           play_mp3("/home/pi/julius_sample/sound/ok.mp3") 
                           print "Shishimaru is wating."
               elif line.find(u'TBS') != -1:
                        if status == STATUS_READY:
                           call_jsay('ティービーエスを流すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           radio_on('TBS')
                           print "Call TBS"
               elif line.find(u'INT') != -1:
                        if status == STATUS_READY:
                           call_jsay('インターエフエムを流すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           radio_on('INT')
                           print "Call INTERFM"
               elif line.find(u'JFM') != -1:
                        if status == STATUS_READY:
                           call_jsay('ジェーウェーブを流すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           radio_on('FMJ')
                           print "Call JWAVE"
               elif line.find(u'NHK') != -1:
                        if status == STATUS_READY:
                           call_jsay('エヌエチケーを流すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           nhk_on('fm')
                           print "Call NHK"
               elif line.find(u'消して') != -1:
                        if status == STATUS_READY:
                           call_jsay('ラジオを消すよ')
                           time.sleep(2.0)
                           status_change(STATUS_EXECUTING)
                           radio_off()
                           print "Call Radio Off"
               
    print 'WARN : while loop breaked'
    print 'INFO : exit'


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print 'Interrupted. Exit sequence start..'
        kill_julius(julius)
        delete_socket(julius_socket)
        print 'INFO : Exit sequence done.'
        sys.exit(0)()

動作確認

$ python sample_listner.py

再生

思った通りに動作しているようだ。 シシマルを認識したらポーンという音を鳴らすことにした。わかりやすいから。

停止

停止には言葉ではなく以前ハックしたAmazon Dash Buttonを利用している。 理由はスピーカーの横にマイクを置いているため言葉を認識してくれなかったからだ。 IMG_0482.JPG

念のため停止するスクリプトAmazon Dash Buttonが押されたら、killall mplayerしてるだけ。

var dash_button = require('node-dash-button');                                                                                                                 
var dash = dash_button("xx:xx:xx:xx:xx:xx", null, null, 'all'); //address from step above                                                                      
dash.on("detected", function (){                                                                                                                               
var exec = require('child_process').exec
, cmd;
 
cmd = 'killall mplayer';
 
killmplayer = function() {
    return exec(cmd, {timeout: 1000},
        function(error, stdout, stderr) {
            console.log('stdout: '+(stdout||'none'));
            console.log('stderr: '+(stderr||'none'));
            if(error !== null) {
                console.log('exec error: '+error);
            }
        }
    )
};
 

認識していないパターン

以上で無事オレオレAmazonEchoが作れた。 あとはこれを昨日覚えたsupervisorでデーモン化すれば完了。

ラズパイの使い方としては満足なものができた。

【続編】Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう

昨日書いた以下記事。

Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう - Qiita
Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう - Qiita...

とにかくSpotify APIを利用することに注力していたため全くレスポンスなどを考えていなかった。 昨日も書いたがとにかく素早くレスポンスを返すことを目指して、しかもPHPでできるのかわからなかったけど、とりあえず非同期で処理させることができたので記載する。

キーワード

  • 非同期
  • Que
  • SQS
  • Supervisor

フローチャート

Flowchart.png

メッセージをキューにためる(送信側)

今回サーバにはAWSを利用していたので、いろいろ調べたところ、非同期処理にはSQSを利用している情報が多かった。 またSQSを利用するときにはSNSを前に噛ますといいよと言う事で調べてみた。

PHPでもaws-sdkを利用すれば随分簡単に利用することができた。

require __DIR__."/vendor/autoload.php";
use Aws\Sns\SnsClient;

// object to array
$data = messageEventToJson($events);

// push SNS -> SQS
try{
    $sns = SnsClient::factory(array(
      'key'    => '{YOUR_KEY}',
      'secret' => '{YOUR_SECRET}',
      'region' => 'ap-northeast-1'
    ));
    $response = $sns->publish(array(
        'TopicArn' => '{YOUR_ARN}',
        'Message' => $data
    ));

}catch (Exception $e){
    //
    getLog($e->getMessage());
}

//
function messageEventToJson($events) {
  $eventArray = array();
  foreach($events as $event) {
    if($event instanceof TextMessage) {
      array_push($eventArray, array(
        'type' => $event->getMessageType(),
        'timestamp' => $event->getTimestamp(),
        'replyToken' => $event->getReplyToken(),
        'userId' => $event->getUserId(),
        'text' => $event->getText()
      ));
    }
    else if($event instanceof PostbackEvent) {
      array_push($eventArray, array(
        'type' => $event->getType(),
        'timestamp' => $event->getTimestamp(),
        'replyToken' => $event->getReplyToken(),
        'userId' => $event->getUserId(),
        'postBackData' => $event->getPostbackData()
      ));
    }
  }

  return json_encode($eventArray);
}

下の汚いコードはObjectをJson化できなかったので仕方なくこうやってみたが、たぶん間違っているんだろうと思う。ほんとはObjectそのままをシリアライズしてSQSのメッセージに放り込みたかった。

以上だ。とても簡単。AWS最高。

非同期でキューを取得(受信側)

こちらはSQSから直接取り出す。 非同期については全く知識がなかったのでいろいろ調べた。PHPじゃだめだ、Node.jsだ!Rubyだ!などの情報があって回り道をしたが、結局挫折してPHPでやってみた。 このソースは全く自信がないので真似しないほうがいいです。自分で調べてください。

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

use Aws\Sqs\SqsClient;

$sqs = SqsClient::factory(array(
      'key'    => '{YOUR_KEY}',
      'secret' => '{YOUR_SECRET}',
    'region'    => 'ap-northeast-1',
    'version' => 'latest',
));

$queueurl = $sqs->getQueueUrl(array(
    'QueueName' => '{YOUR_QUE_NAME}',
));


//メッセージを待機します
while(true){
  try {
    $result = $sqs->receiveMessage(array(
        'QueueUrl' => $queueurl['QueueUrl'],
    ));
    //
    if ($result['Messages'] == null) {
      sleep(1);
    }
    else {
        // Exec Something! # ここにSpotify連携とLINEへの返信処理を記述した


        // status ok is delete sqs message
        if($response->isSucceeded()) {
          try {
            $sqs->deleteMessage(array(
                'QueueUrl' => $queueurl['QueueUrl'],
                'ReceiptHandle' => $receiptHandle
            ));
          }
          catch(Exception $e) {
            echo $e->getMessage();
          }
        }
        else {
          // under construction
        }

      }

    }
  }
  catch(Exception $e) {
    echo $e->getMessage();
  }
}
exit(0);
$ php worker.php

なんと想定通りにうごいたよ!おかん!! といってもここで半日ほど躓いたのは内緒の方向で。 非同期はPythonやNode.jsを使ったほうが良いかもしれないとは思った。勉強しなきゃ。

受信のプロセス化(デーモン化)

さて、うまくいったのはいいが、黒い画面を一日中立ちあげて眺めるわけにはいかない。 普通にinit.dとかに登録すればよいの?と思ったけれども、どうもSupervisorってのを使うとうまくいくらしい。 このあたりは全くわからないのでこちらの記事を参考にしてもらったほうが良い。

CakePHPでSQSを使って非同期にメール送信し、送信プロセスの監視を行う - Qiita
CakePHPでSQSを使って非同期にメール送信し、送信プロセスの監視を行う - Qiita...

参考にさせてもらってどうやらうまくいった。ビバSupervisor。勉強しなきゃ。

完成品

昨日と変化なし。 Screenshot_2017-01-12-18-49-31-730_jp.naver.line.android.png

プレイリストを作るところは処理が多いのでレスポンスが遅い。 ただし非同期なので待ってたらたぶん返ってくるはず。

バックグラウンドは大きく変わったので記事にした。 あと、まだ工事中の部分もあるけれども晒してみる。

知らないうちにサービスやめてるかもだけど、その時はそっと友達外してください。

コメントにアドバイス頂けると非常にうれしいです。

Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう

9月末にとうとう日本へ上陸したSpotify。 当初はAmazon Prime Musicがあるからいらないやと思っていたけれども、音楽系には珍しくAPIがあるというので弄ってみたいと思い、早速無料プランで申し込んでみた。 APIについていろいろ調べて見るものの日本に入ったばかりが原因なのかあまり情報がなかったのでここに記事として残す。

Spotify API登録

資料が見つからなかったのでわからないが、Freeプランでも特に制限なくAPIは使えるよう。(違ったらごめんなさい) https://developer.spotify.com/ のメニューからMy Appsを選択し、CREATE AN APPを押下して、NameとDescriptionを適当に決めて登録する。 1.PNG

今回はLINEBotと連携するのでRediret URIsは適当に入れていたのだが、あとでドはまりしたのでそこで説明する。 Client IDと Client Secretをひかえておいて、SAVEを押せば登録が完了だ。

SDK取得

Spotify APIのPHPSDKがgithubで提供されているので利用させてもらう。 spotify-web-api-php インストール方法はgithubに記載の方法で全く問題なく完了した。

認証

ここが一番苦労した。というか今まで使ってきたAPIはIDとSECRETさえあれば認証できていたのだが... (実はきちんとAPI仕様書を読めば書いてあるのだ)

と言う事で、僕が認証までたどり着いた方法を書く。

失敗例

spotify-web-api-php のdocsは見ずに、海外サイトのサンプル例に従い以下のように認証をしていた。

require 'vendor/autoload.php';

$session = new SpotifyWebAPI\Session('CLIENT_ID', 'CLIENT_SECRET', 'REDIRECT_URI');
$api = new SpotifyWebAPI\SpotifyWebAPI();

// Request a access token with optional scopes
$scopes = array(
    'playlist-read-private',
    'playlist-modify-private', 
    'user-read-private',
    'playlist-modify'
);

$session->requestCredentialsToken($scopes);
$accessToken = $session->getAccessToken(); // We're good to go!

// Set the code on the API wrapper
$api->setAccessToken($accessToken);

getLog($api->me())

scopesは Web API: Using Scopes を参考に自分で変更した。 でLINEBotと連携させて認証をさせようとしても、$api->me()で401:認証エラーで落ちる。 なぜなぜ?と何度やっても同じ結果だった。 一時間ほどたった後、spotify-web-api-phpAuthorizationを見に行くと、

Client Credentials Flow This method doesn’t require any user interaction and no access to user information are therefore granted.

と書いてある。no access to user information are therefore granted. この方法ではユーザ情報にアクセスできない。Oh My God! しかも、きちんとAPI仕様書の Authorization を読めと書いてあるので読む。

何々、一度認証してaccess_tokenを取得して、それを利用してrefreshed access_tokenを取得するのか! Authorization-Code-Flow-Diagram.png

成功例

ということでまずは index.php をWebサーバにおいて認証をしてみることに。 この際、My AppのRediret URIsにはこのindex.phpを指定すること。

<?php

require 'vendor/autoload.php';

$session = new SpotifyWebAPI\Session('{YOUR_CLIENT_ID}', '{YOUR_CLIENT_SECRET}', '{YOUR_REDIRECt_URI}');
$api = new SpotifyWebAPI\SpotifyWebAPI();

if (isset($_GET['code'])) {
    $session->requestAccessToken($_GET['code']);
    $api->setAccessToken($session->getAccessToken());

    print_r($api->me());
    print_r($session->getAccessToken());
} else {
    header('Location: ' . $session->getAuthorizeUrl(array(
        'scope' => array(
          'playlist-read-private', // プレイリスト取得
          'playlist-modify-private', // プレイリスト変更
          'user-read-private',
          'playlist-modify'
        )
    )));
    die();
}
?>

おーー見事に自分の情報が出た。また無事にaccess_tokenも取得することができた。 と言う事でこちらを利用して、今度はrefresh access_token の発行とサーバからアクセスしてみる。

use SpotifyWebAPI\Session;
use SpotifyWebAPI\SpotifyWebAPI;

// spotify
$session = new Session('{YOUR_CLIENT_ID}', '{YOUR_CLIENT_SECRET}', '{YOUR_REDIRECt_URI}');
$api = new SpotifyWebAPI();

$refreshToken = '{YOUR_ACCESS_TOKEN}';

$session->refreshAccessToken($refreshToken);

$accessToken = $session->getAccessToken();

print_r($accessToken);

こちらも無事access_tokenが出た模様。と言う事でこの方法が正しいかはわからないが、現在も認証は出来ているのでこれで良しとしよう。

Spotify APIを使って何を作るか考える

Spotify APIを利用したサンプルがあまり見つからなかった。 いろいろ見ていて、お!Spotifyっぽいと思ったのがあったのでそのアイデアパクることにオマージュすることに。 ちなみに元ネタは Spotibot というページで、Bandやアーティスト名を入力すると、そのアーティストに似たアーティストのプレイリストを自動で作ってくれるものだ。 まずはこちらのページでイメージしてほしい。

あまりLINEBotとは親和性は良くなさそうではあるが(プレイリスト作成に時間がかかる)、LINEBotでもやってみたい事があったのでチャレンジしてみる。

全自動プレイリスト作成手順

フロー

Spotibotには当たり前だがソースや設計書が公開されていないので、どうやったら作れるか考えてみる。 認証の失敗を教訓に、まずはAPI仕様書をじっくり眺めてみる。眺めてみて以下フローで出来そうな事を思いついた。

Untitled.png

ソース

ぐわっと書いたのでかなり汚く、エラー処理なども無いが以下の通り。 これで無事思い描いた全自動プレイリストはできそうだ。

// Artist
function getArtist($text, $session, $api) {
  // get artist
  $results = $api->search($text, 'artist', array('limit' => 1));

  if(count($results->artists->items) == 0) return null;

  foreach($results->artists->items as $data) {

      $artist = array(
                      'id' => $data->id,
                      'name' => $data->name,
                      'image' => $data->images[0]->url,
                    );
  }


  return count($artist) == 0 ? 'Not Found.' : $artist;
}

function makePlayList($id, $session, $api) {

  // get related artist
  $results = $api->getArtistRelatedArtists($id)->artists;

  // get 15 artist
  $relatedArtists = array();

  $max = count($results) >= 20 ? 20 : count($results);

  for( $i = 0; $i < $max; $i++ ) {
    $related = array(
                   'id' => $results[$i]->id,
                   'name' => $results[$i]->name
    );
    array_push($relatedArtists, $related);
  }

  // get artist top tracks
  $top_tracks = array();

  foreach($relatedArtists as $related) {
    $top = $api->getArtistTopTracks($related['id'], array('country' => 'JP'))->tracks;

    if(count($top) != 0) array_push($top_tracks, $top[0]->id);
  }

  // create new playlist
  $user_id = $api->me()->id;

  $response =$api->createUserPlaylist($user_id, array(
    'name' => 'Your requested Related Artists'
  ));

  $playlist_id = $response->id;

  // add playlist
  $api->addUserPlaylistTracks($user_id, $playlist_id, $top_tracks);

  // get playlist
  $playlist = $api->getUserPlaylist($user_id, $playlist_id);

  $response = array(
    'url' => $playlist->external_urls->spotify,
    'image' => $playlist->images[0]->url,
  );


  return $response;
}

function getEtcArtist($name, $session, $api) {
  // get artist
  $results = $api->search($name, 'artist', array('offset' => 1, 'limit' => 5));

  //
  if(count($results->artists->items) == 0) return null;

  $artists = array();
  foreach($results->artists->items as $data) {

      array_push($artists, array(
                      'id' => $data->id,
                      'name' => $data->name,
                      'image' => count($data->images) != 0 ? $data->images[0]->url : "https://developer.spotify.com/wp-content/uploads/2016/07/icon3@2x.png",
                    )
      );
  }
  return $artists;

}

LINEBot

今まで作ってきたLINEBotはテキストを返すばかりであったが、せっかくインタラクティブなものが用意されているので、Template Message機能で返信してみる。

流れ的には、第一にアーティスト名がリクエストされたら、Spotify APIを利用してアーティスト名と画像を表示してユーザに表示する。OKならばプレイリストを作る、NOならばカルーセルで名称が似ているアーティストをサジェストする。 言葉で説明しにくいのでソースと画像を晒す。

getArtist

  if($event instanceof TextMessage) {
      $reply_token = $event->getReplyToken();

      // getArtist
      $artist = getArtist($event->getText(), $session, $api);

      if(count($artist) == null) $bot->replyText($reply_token, "Not Found.");

      $actions = array(
        new PostbackTemplateActionBuilder("YES", "confirm=1&id=". $artist['id']),
        new PostbackTemplateActionBuilder("NO", "confirm=-1&name=". $artist['name'])
      );

      $img_url = $artist['image'];

      $button = new ButtonTemplateBuilder($artist['name'], "Is your request ". $artist['name'] . "?", $img_url, $actions);

      $msg = new TemplateMessageBuilder("Your Request". $artist['name'], $button);

      $bot->replyMessage($reply_token, $msg);
    }

Screenshot_2017-01-11-11-16-46-136_jp.naver.line.android.png

makePlaylist

      $query = $event->getPostbackData();

      if ($query) {
          parse_str($query, $data);

          foreach($data as $key => $value) {
            if ($key == 'confirm' && isset($data['confirm'])) {
                $confirm = $value;
            }
            else if($key == 'id' && isset($data['id'])) {
                $id = $value;
            }
            else if($key == 'name' && isset($data['name'])) {
                $name = $value;
            }
          }
      }

      $reply_token = $event->getReplyToken();

      // YES
      if($confirm == 1) {
        $playlist = makePlayList($id, $session, $api);

        $actions = array(
          new UriTemplateActionBuilder("Open Spotify", $playlist['url']),
        );

        $img_url = $playlist['image'];

        $button = new ButtonTemplateBuilder('Playlist', 'Please click "Open Spotify".', $img_url, $actions);

        $msg = new TemplateMessageBuilder('Finish generate playlist', $button);

        $bot->replyMessage($reply_token, $msg);
      }

Screenshot_2017-01-11-11-14-29-462_jp.naver.line.android.png

Screenshot_2017-01-11-11-30-27-895_jp.naver.line.android.png

Screenshot_2017-01-11-11-30-35-925_jp.naver.line.android.png

なかなか満足できるものだったので、公開したかったのだけれどもレスポンスに大きな課題があったのでできずorz

今後の課題

プレイリストを作る速度が遅いので以下を見て改良していくつもり。

大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ - Qiita
大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ - Qiita...

追記

非同期で処理するように対応した。

【続編】Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう - Qiita
【続編】Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう - Qiita...

Raspberry piでAmazon Echo(AVS)を動かしてみる未遂

前段

思うところあってRaspberry piを買ってみた。 IMG_20161222_204815.jpg 買って早速OSを入れて、Airplay受信したり、Radiko録音したりやっていたが、一番やってみたかったAmazonEcho(AVS)を試してみたので記録に残す。 ただし題名にもある通り、2016.12.27現在、やりたかったことはできていない。

そもそもAVSとはなんであろう。英語ではあるが公式ページに書かれている。

Alexa Voice Service
Alexa Voice Service...

そもそもはAmazonEchoという米英独で発売されている製品の中で動ているソフトウェアだ。 その、AVSをAmazonがライブラリとして他ハードでも動くように用意してくれている。

alexa/alexa-avs-sample-app: This project demonstrates how to access and test the Alexa Voice Service using a Java client (running on a Raspberry Pi), and a Node.js server.
alexa/alexa-avs-sample-app: This project demonstrates how to access and test the Alexa Voice Service using a Java client (running on a Raspberry Pi), and a Node.js server....

インストール方法は英語ではあるがithubに丁寧に書かれており、Linuxを扱える人であれば問題なくインストールできると思う。 上にも書いた通り、うまくいってないのでそのあたりを中心に書き残しておく。

Raspberry Pi · alexa/alexa-avs-sample-app Wiki
Type ctrl-X and then Y, and then press Enter to save the changes to the file. ...

Amazon Developer アカウントの登録

https://developer.amazon.com/login.html

証明書の取得

Project, ClientID, ClientSecretの取得

AVSをraspberry piにgit clone

$ cd ~/Desktop
$ git clone https://github.com/alexa/alexa-avs-sample-app.git

インストール

$ cd ~/Desktop/alexa-avs-sample-app
$ vim automated_install.sh

304 #--------------------------------------------------------------------------------------------                                                              
305 # Checking if script has been updated by the user with ProductID, ClientID, and ClientSecret                                                               
306 #--------------------------------------------------------------------------------------------                                                              
307                                                                                                                                                            
308 if [ "$ ProductID" = "YOUR_PRODUCT_ID_HERE" ]; then                                                                                                         
309   ProductID="mypialexa"                                                                                                                                    
310 fi                                                                                                                                                         
311 if [ "$ ClientID" = "YOUR_CLIENT_ID_HERE" ]; then                                                                                                           
312   ClientID="amzn1.application-xxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"                                                                                 
313 fi                                                                                                                                                         
314 if [ "$ ClientSecret" = "YOUR_CLIENT_SECRET_HERE" ]; then                                                                                                   
315   ClientSecret="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"                                                                          316 fi          


$ ./automated_install.sh
※基本的にはプロンプトに対しては「y」で良い。

VNCのインストール)

GUIが必要なため、モニタをRaspberry pi に接続していない場合はVNCをインストールしておく。

(ここからGUI)

Webサービス開始

$ cd ~/Desktop/alexa-avs-sample-app/samples
$ cd companionService && npm start

1.PNG

別のターミナルを起動して

$ cd ~/Desktop/alexa-avs-sample-app/samples
$ cd javaclient && mvn exec:exec

2.PNG

Login Register/Autehnitication youre Device で YESを押下するとブラウザが立ち上がるのでproceed to localhost(unsafe)を押下する。 3.PNG

Amazon.comサイトへリダイレクトされるのでログインして認証し、OKを押下するとAlexa Voice Serv..にTokenが自動的に入力される。 4.PNG

(ここからはAlexaという声で反応させたい場合)

Alexa

別のターミナルを起動して

$ cd ~/Desktop/alexa-avs-sample-app/samples
$ cd wakeWordAgent/src && ./wakeWordAgent -e sensory

  ここで僕の環境で問題が発生!

$ cd wakeWordAgent/src && ./wakeWordAgent -e sensory
INFO:main: Starting Wake Word Agent
INFO:WakeWordAgent: State set to IDLE(2)
INFO:Initializing Sensory library | library name: TrulyHandsfree | library version: 5.0.0-avs.1 | model file: ../ext/resources/spot-alexa-rpi.snsr
WARNING:Library expires on: License expires on 27 Mar 2017 00:00:00 GMT
wakeWordAgent: pcm.c:694: snd_pcm_close: Assertion `pcm' failed.
Aborted

5.PNG

困ったよー。 ググりまくった結果、githubのコミュニティで良い情報を得た。

Sensory Wake Word engine · Issue #284 · alexa/alexa-avs-sample-app
Sensory Wake Word engine · Issue #284 · alexa/alexa-avs-sample-app...

ndphu commented on 12 Oct I got the same problem. There is a file under wakeWordAgent/sensory/alexa-rpi/config called asound.conf. I replaced my ~/.asoundrc by this file and it can fix the problem. (make sure you have >a backup for your current ~/.asoundrc)

どうやらwakeWordAgent/sensory/alexa-rpi/configを見ろと言う事だ。

$ cat asound.conf
pcm.!default
{
  type asym
  playback.pcm {
    type hw
    card 1
    device 0
  }
  capture.pcm {
    type plug
    slave {
      pcm {
        type hw
        card 0
        device 0
      }
    }
  }
}

ぼくの環境をチェックしてみる。

$ cat /proc/asound/modules
 0 snd_usb_audio
 1 snd_bcm2835

$ arecord -l
**** List of CAPTURE Hardware Devices ****
card 0: Device [USB PnP Audio Device], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 1: ALSA [bcm2835 ALSA], device 0: bcm2835 ALSA [bcm2835 ALSA]
  Subdevices: 8/8
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
  Subdevice #4: subdevice #4
  Subdevice #5: subdevice #5
  Subdevice #6: subdevice #6
  Subdevice #7: subdevice #7
card 1: ALSA [bcm2835 ALSA], device 1: bcm2835 ALSA [bcm2835 IEC958/HDMI]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

どうやら問題なさそうだ。 今度は~配下の .asoundrcを確認してみる。

$ cat .asoundrc
pcm.!default {
  type asym
   playback.pcm {
     type plug
     slave.pcm "hw:0,0"
   }
   capture.pcm {
     type plug
     slave.pcm "hw:1,0"
   }
}

pcm.!default {
        type hw
        card 1
}

ctl.!default {
        type hw
        card 1
}

うん?なんかおかしい。おかしいぞ!と言う事で変更してみた。

$ vi .asoundrc
$ cat .asoundrc
pcm.!default {
  type asym
   playback.pcm {
     type plug
     slave.pcm "hw:1,0"
   }
   capture.pcm {
     type plug
     slave.pcm "hw:0,0"
   }
}

再実行

$ ./wakeWordAgent -e sensory
INFO:main: Starting Wake Word Agent
INFO:WakeWordAgent: State set to IDLE(2)
INFO:Initializing Sensory library | library name: TrulyHandsfree | library version: 5.0.0-avs.1 | model file: ../ext/resources/spot-alexa-rpi.snsr
WARNING:Library expires on: License expires on 27 Mar 2017 00:00:00 GMT
INFO:SensoryWakeWordEngine: mainLoop thread started
INFO:WakeWordIPCSocket::mainLoop thread started
INFO:WakeWordIPCSocket: init socket on port:5123
INFO:WakeWordAgent: thread started
INFO:===> Connected to AVS client <===

キターーーーー!

別のターミナルを起動して

$ cd ~/Desktop/alexa-avs-sample-app/samples
$ cd wakeWordAgent/src && ./wakeWordAgent -e kitt_ai
INFO:main: Starting Wake Word Agent
INFO:WakeWordAgent: State set to IDLE(2)
ERROR:Failed to open PortAudio stream.Device unavailable
ERROR:KittAiSnowboyWakeWordEngine: Initialization error:Failed to open PortAudio stream.
ERROR:WakeWordAgent: exception in constructor: Failed to open PortAudio stream.
ERROR:main: Exception happened: Failed to open PortAudio stream.

6.PNG

こなーいorz 一応このあたりは見てみた。

wake word engine problem · Issue #277 · alexa/alexa-avs-sample-app
wake word engine problem · Issue #277 · alexa/alexa-avs-sample-app...

ググっても有力な情報が得られず、途方に暮れているところでQiitaを書いてみた。。

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

Microsoft Translator APIを使ってLINEから翻訳させてみた - Qiita
Microsoft Translator APIを使ってLINEから翻訳させてみた - Qiita...

先日以下記事をFacebookで投げたところ、友達から『音声入力はできないんだっけ?』というコメントが。 そもそもLINEでボイスメッセージを送ったことがなかったが、Messaging APIのドキュメントを見るとAudioも受信できるようであったので早速試してみた。 ※この記事は音声+翻訳の技術要素に寄った内容となるのでライブラリインストール・契約方法などは各自他で探ってください。

構想

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

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

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

利用API

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

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

環境

LINEBotでボイスメッセージを受信

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

LINEBot SDK導入

$composer require linecorp/line-bot-sdk

ボイスメッセージ受信

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

<?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仕様はここにあるので参考に。

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

  • m4aからwavに変換
  • Bing Speech APIAccess Token取得
  • APIで変換
      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);
}

翻訳

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

      $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

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

15589833_1308733472503027_4321714413467263664_n.jpg

フローチャート

今回のフローは以下通り。 Flowchart.png

全体ソース

<?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 にこれで応募してみた。

Microsoft Translator APIを使ってLINEから翻訳させてみた

引続き自分に使えるLINEBotシリーズをひたすら研究中。 海外輸入でサポートに問い合わせることが多いので翻訳できるLINEBotを作ってみた。

翻訳ライブラリ

Google Cloud Translation APIは有料のようなので、Microsoft Translator APIを利用することにした。 200百万文字/月までは無料で利用できるようだ、Microsoftありがとう。

登録方法などはこの記事が詳しい。

Microsoft Translator APIを使ってみる - Qiita
Microsoft Translator APIを使ってみる - Qiita...

プログラム

プログラムはこの記事をほぼ真似させてもらった。

Microsoft Translator APIを使ってみた - Qiita
Microsoft Translator APIを使ってみた - Qiita...

日本語が混じっていれば入力は日本語と判定し英語に翻訳、それ以外は逆とした。

<?php

require_once('./LINEBotTiny.php');

$channelAccessToken = 'YOUR_ACCESS_TOKEN';
$channelSecret = 'YOUR_SECRET';

$client = new LINEBotTiny($channelAccessToken, $channelSecret);

foreach ($client->parseEvents() as $event) {
    switch ($event['type']) {
        case 'message':
            $message = $event['message'];

            switch ($message['type']) {
                case 'text':

                    $client->replyMessage(array(
                        'replyToken' => $event['replyToken'],
                        'messages' => array(
                            array(
                                'type' => 'text',
                                'text' => translation($message['text'])
                            )
                        )
                    ));

                    break;
                default:
                    error_log("Unsupporeted message type: " . $message['type']);
                    break;
            }
            break;
        default:
            error_log("Unsupporeted event type: " . $event['type']);
            break;
    }
};

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

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

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


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

}


  function getAccessToken($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 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];
  }

動作

精度や速度は全く問題なしだ。これはかなり使えそうな予感。 cap.PNG

(追記) ちょうどこの記事を見て、 ニューラルネット翻訳に対応してみた。

Microsoft Translate API を用いた Slack ニューラルネット翻訳コマンドの実装 - Qiita
Microsoft Translate API を用いた Slack ニューラルネット翻訳コマンドの実装 - Qiita...

LINEBOT+Open Jtalkで初音ミクに全力人力でしゃべらせてみた

逃げ恥を見ていて、ガッキーがペッパー君と楽しそうに話しているのを見て、ロボット欲しいなと思ったけれども、小遣いでは買えそうにないので、代わりになるものを作ってみた。

構想

入力した内容をそのまま話させる。INはなんでも良いが、一般人でもイメージしやすいようにLINEがベター。

LINEからの入力はSSL証明書が用意されたサーバが必要で、普段はVPSAWSに無料のLetsEncryptを設定して対応しているが、今回はその方法ではうまくいかない。 なぜならサーバから音声を出したいので、ローカルでサーバを運用する必要があるからだ。

環境は ノートPC(windows10)にのせたvirtualbox+vagrantで行うこととした。

キーワード

ググってみたところ、入力した文字を音声化するのとローカルサーバをSSLで外部公開できるライブラリが見つかった。 - Open Jtalk(入力した日本語を音声化する) - ngrok(ローカル環境をテンポラリで外部公開する)

Open Jtalk環境構築

centosにOpen Jtalkをインストールする。Oopen Jtalkを利用するには当該ライブラリ以外にhts_engine APIと辞書ファイル、音声ファイルが必要となってくるのでインストールする。

hts_engine APIのインストール

$wget http://downloads.sourceforge.net/hts-engine/hts_engine_API-1.09.tar.gz
...
$tar zxvf hts_engine_API-1.09.tar.gz
...
$cd hts_engine_API-1.09
$./configure
...
config.status: creating Makefile
$make
...
$make install
...
$which hts_engine
usr/local/bin/hts_engine

Open Jtalkのインストール

$wget http://downloads.sourceforge.net/open-jtalk/open_jtalk-1.09.tar.gz
...
$tar zxvf open_jtalk-1.09.tar.gz
...
$cd open_jtalk-1.09
$./configure
...
config.status: creating Makefile
$make
...
$make install
...
$which open_jtalk
/usr/local/bin/open_jtalk

辞書のインストール

Open Jtalkで使う辞書のインストールを行う。

$mkdir /usr/local/share/open_jtalk
$wget http://sourceforge.net/projects/open-jtalk/files/Dictionary/open_jtalk_dic-1.07/open_jtalk_dic_utf_8-1.07.tar.gz
...
$tar zxvf open_jtalk_dic_utf_8-1.07.tar.gz
...
$su -
$mv open_jtalk_dic_utf_8-1.07 /usr/local/share/open_jtalk/

初音ミク音声をOpen Jtalkで

Open Jtalkでは音声サンプルがいくつか用意されているが、今回はそれらを利用せず、個人が作っている初音ミクの音声を利用させていただく。 .htsvoice形式までの作り方はいかが参考になる。

Open JTalkで初音ミクの声でおしゃべりさせる@Mac/Linux/Raspberry Pi - karaage. [からあげ]
Voice.htsvoice...

上記でできたhtsvoiceファイルを音声ファイルとして適当な場所に保存する。

$mkdir /usr/local/share/hts_voice/miku
$mv Voice.htsvoice /usr/local/share/hts_voice/miku/miku.htsvoice

以上でOpen Jtalkの環境ができたのでテストをしてみよう。

Open Jtalkのテスト

$echo '今日は晴れです' | /usr/bin/tr -d '\n' | open_jtalk \
        -m /usr/local/share/hts_voice/miku/miku.htsvoice \
        -ow /tmp/output.wav -x /usr/local/share/open_jtalk/open_jtalk_dic_utf_8-1.09

$sudo aplay /tmp/output.wav

無事PCから音声が流れたらOKだ。出ない場合は僕も引っかかったがvirtualboxの設定でオーディオが無効になっている可能性があるので、一度vagrantを落としてオーディオを有効にしてから試してほしい。

プログラム

LINEからメッセージを受信して、Open Jtalkを話すプログラムを書いてみよう。 今回はPHPを選択する。LINEが提供するSDKからLINEBotTinyを利用する。

<?php

require_once('./LINEBotTiny.php');

$channelAccessToken = 'YOUR_ACCESS_TOKEN';
$channelSecret = 'YOUR_SECRET';

$client = new LINEBotTiny($channelAccessToken, $channelSecret);

foreach ($client->parseEvents() as $event) {
    switch ($event['type']) {
        case 'message':
            $message = $event['message'];

            switch ($message['type']) {
                case 'text':
                    $client->replyMessage(array(
                        'replyToken' => $event['replyToken'],
                        'messages' => array(
                            array(
                                'type' => 'text',
                                'text' => 'きちんと話したよ'
                            )
                        )
                    ));

                    // DB
                    $mongo = new Mongo();

                    $db = $mongo->selectDB("mikubot");

                    $coll = $db->selectCollection("messages");

                    $coll->insert( (array('message' => $message['text'], 'ts' => new MongoDate() )) );

                    // open_jtalk
                    exec("sudo /usr/local/bin/line_talk \"". $message['text']. "\"");

                    break;
                default:
                    error_log("Unsupporeted message type: " . $message['type']);
                    break;
            }
            break;
        default:
            error_log("Unsupporeted event type: " . $event['type']);
            break;
    }
};
?>

基本はecho_bot.phpを真似しただけだが、このPHPでは追加で2つの要素が入っているので追記しておく。 一つ目はmongdbに話したい内容をためることにした。今はLINEから入力した内容のみを話す人力知能だけれども、いつか人工知能になってくれることを祈って。 二つ目はexecの部分だ。 line_talk コマンドの中身を書いておく。

$ cat line_talk                                                                                                                  
/bin/sh

echo $1 | /usr/bin/tr -d '\n' | open_jtalk \
        -m /usr/local/share/hts_voice/miku/miku.htsvoice \
        -ow /tmp/output.wav -x /usr/local/share/open_jtalk/open_jtalk_dic_utf_8-1.09/

sudo aplay /tmp/output.wav

上記PHPapacheなり、nginxなり、php -S localhost:8080なりでローカルサーバをたてて配置する。 このときhttpsの設定は必要なくhttpの事だけ考えてよい。

LINE Messaging APIvagrantから利用する

LINE Messaging APIの設定

初めてだと少しわかりづらいがQiitaなどに沢山情報が落ちているので検索してみよう。

ngrokの設定

ngrokをインストールする。

$npm i -g ngrok

ngrokの起動

$nngrok http 80
ngrok by @inconshreveable                                                                                                                     (Ctrl+C to quit)
                                                                                                                                                              
Session Status                online                                                                                                                          
Version                       2.1.18                                                                                                                          
Region                        United States (us)                                                                                                              
Web Interface                 http://127.0.0.1:4040                                                                                                           
Forwarding                    http://xxxxxxx.ngrok.io -> localhost:80                                                                                        
Forwarding                    https://xxxxxxx.ngrok.io -> localhost:80  

https://xxxxxxx.ngrok.io をコピー。

webhook

LINE Messaging API のWebhook URLにhttps://xxxxxxx.ngrok.io/XXXX/callback.php を設定する。

動作

じんりきみーくの完成。