Raspberry Pi でオレオレAmazonEcho(もしくはGoogleHomeもしくはSiri)を作ってみた
Raspberry piでAmazon Echo(AVS)を動かしてみる未遂 - Qiita
Raspberry piでAmazon Echo(AVS)を動かしてみる未遂 - Qiita... |
こちらで書いた記事は有力な情報が見つからず相変わらず同じ状態。 その状況を打破すべく、オレオレAmazonEchoを作ってみることにした。
オレオレAmazonEchoにやらせたいこと
とにかくラジオが好きだ。よく聞いている。朝はTBSのスタンバイを聞くことが最近の日課だ。 6:42から始まる歌のない歌謡曲は最高だ。 しかし寝起きにそのあたりに転がっているiPhoneを取り出し、radikoアプリを起動し、TBSを選択し、Airplayを設定するのはとてもめんどくさい。 となればラズパイをradikoサーバにでもしてcronで自動起動させればよいじゃないかと思うのだが、自動は嫌だ。
と言う事で話しかければradikoが鳴ることを最終目標にオレオレAmazonEchoを作ることにした。
環境
- Raspberry pi (Raspbian)
- git, python, node.js
- USBマイク
raspberry piでサウンド関係はちょっとめんどくさいので事前に調べて設定しておく。
音声認識
LINEBot SDK+Microsoft Speech API+Translate API でバリニーズと会話できるようにしてみる - Qiita
LINEBot SDK+Microsoft 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 piでradiko再生を行える環境を整える。 今回はこちらを利用させて頂いた。
Raspberry Piでradikoの再生、録音 - Muchuu
Raspberry Piでradikoの再生、録音 - Muchuu... |
プログラム
juliusが音声認識を行ってくれるが、それを受けるプログラムを作ることにする。 PHPで書きたかったが情報があまりなかったのでpythonを利用することにした。 ただし僕はpythonを書けないので何人かの方のサンプルを組合させていただいた。 (ちなみにシシマルとは昔飼ってた犬の名前。日常生活もしくはラジオ・テレビから発せられることはあまりないと思い、誤認識はしないと思ったから) 流れ
- pythonからjuliusを起動させる。
- シシマルという言葉を認識すると待機状態に入る(Hey SiriやAlexaのようなこと)
- 待機状態で辞書に登録された言葉を認識すると各々の動作を行う(今回はラジオを再生する)
#!/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
再生
— momochan_qiita (@momochan_qiita) 2017年1月13日
思った通りに動作しているようだ。 シシマルを認識したらポーンという音を鳴らすことにした。わかりやすいから。
停止
— momochan_qiita (@momochan_qiita) 2017年1月13日
停止には言葉ではなく以前ハックしたAmazon Dash Buttonを利用している。 理由はスピーカーの横にマイクを置いているため言葉を認識してくれなかったからだ。
念のため停止するスクリプト。 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); } } ) };
認識していないパターン
— momochan_qiita (@momochan_qiita) 2017年1月13日
以上で無事オレオレAmazonEchoが作れた。 あとはこれを昨日覚えたsupervisorでデーモン化すれば完了。
ラズパイの使い方としては満足なものができた。
【続編】Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう
昨日書いた以下記事。
Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう - Qiita
Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう - Qiita... |
とにかくSpotify APIを利用することに注力していたため全くレスポンスなどを考えていなかった。 昨日も書いたがとにかく素早くレスポンスを返すことを目指して、しかもPHPでできるのかわからなかったけど、とりあえず非同期で処理させることができたので記載する。
キーワード
- 非同期
- Que
- SQS
- Supervisor
フローチャート
メッセージをキューにためる(送信側)
今回サーバには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。勉強しなきゃ。
完成品
昨日と変化なし。
プレイリストを作るところは処理が多いのでレスポンスが遅い。 ただし非同期なので待ってたらたぶん返ってくるはず。
バックグラウンドは大きく変わったので記事にした。 あと、まだ工事中の部分もあるけれども晒してみる。
知らないうちにサービスやめてるかもだけど、その時はそっと友達外してください。
コメントにアドバイス頂けると非常にうれしいです。
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を適当に決めて登録する。
今回は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-phpの Authorizationを見に行くと、
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を取得するのか!
成功例
ということでまずは 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仕様書をじっくり眺めてみる。眺めてみて以下フローで出来そうな事を思いついた。
ソース
ぐわっと書いたのでかなり汚く、エラー処理なども無いが以下の通り。 これで無事思い描いた全自動プレイリストはできそうだ。
// 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); }
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); }
なかなか満足できるものだったので、公開したかったのだけれどもレスポンスに大きな課題があったのでできずorz
今後の課題
プレイリストを作る速度が遅いので以下を見て改良していくつもり。
大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ - Qiita
大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ - Qiita... |
追記
非同期で処理するように対応した。
【続編】Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう - Qiita
【続編】Spotify APIを使ってLINEBotであなたにおすすめプレイリストを全自動で作ってもらう - Qiita... |
Raspberry piでAmazon Echo(AVS)を動かしてみる未遂
前段
思うところあってRaspberry piを買ってみた。 買って早速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
別のターミナルを起動して
$ cd ~/Desktop/alexa-avs-sample-app/samples $ cd javaclient && mvn exec:exec
Login Register/Autehnitication youre Device で YESを押下するとブラウザが立ち上がるのでproceed to localhost(unsafe)を押下する。
Amazon.comサイトへリダイレクトされるのでログインして認証し、OKを押下するとAlexa Voice Serv..にTokenが自動的に入力される。
(ここからは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
困ったよー。 ググりまくった結果、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.
こなーい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にリプライするという言葉でいうと単純なもの。 方法としては二つあると考えた。
少し調べた結果、今回は上の一旦文字に変換して翻訳することにした。 理由としては音声から文字への変換レベルを検証したことが大きな理由だ。
利用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のソースはきれいではないがこちらも。 流れとしては
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)); }
結果
結果、見事に思うような結果が返ってきた。 これでバリに行っても安心である。
友達とBotとグループを組めばリアルタイム翻訳ちっくなこともできる。若干見づらいかもしれないけれど も。
フローチャート
今回のフローは以下通り。
全体ソース
<?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]; }
動作
精度や速度は全く問題なしだ。これはかなり使えそうな予感。
(追記) ちょうどこの記事を見て、 ニューラルネット翻訳に対応してみた。
Microsoft Translate API を用いた Slack ニューラルネット翻訳コマンドの実装 - Qiita
Microsoft Translate API を用いた Slack ニューラルネット翻訳コマンドの実装 - Qiita... |
LINEBOT+Open Jtalkで初音ミクに全力人力でしゃべらせてみた
逃げ恥を見ていて、ガッキーがペッパー君と楽しそうに話しているのを見て、ロボット欲しいなと思ったけれども、小遣いでは買えそうにないので、代わりになるものを作ってみた。
構想
入力した内容をそのまま話させる。INはなんでも良いが、一般人でもイメージしやすいようにLINEがベター。
LINEからの入力はSSL証明書が用意されたサーバが必要で、普段はVPSかAWSに無料の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
上記PHPをapacheなり、nginxなり、php -S localhost:8080なりでローカルサーバをたてて配置する。 このときhttpsの設定は必要なくhttpの事だけ考えてよい。
LINE Messaging APIをvagrantから利用する
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 を設定する。
動作
じんりきみーくの完成。
じんりきみーく pic.twitter.com/RJlIJc5qR3
— momochan_qiita (@momochan_qiita) 2016年12月19日