Raspberry Pi と Arduino でつくったロボットと会話してみました。
RaspberryPi-Robot talk me - YouTube
まずは挨拶から。ロボットに"ポンボット"という名前をつけたので、こんな風に挨拶してみます。
「ポンボット、こんにちは。」
形態素解析エンジン"mecab"を利用して、ここでは「感動詞」を抽出してオウムがえしをさせています。
続いて好きなものを伝えてみます。
「ジバニャンが大好き。」
"mecab"のユーザー辞書に"mecab-ipadic-neologd" を利用させてもらいました。
こちらは素晴らしく語彙が豊富な辞書なので、Wikipediaに記載されているような様々な固有名詞を抽出することが可能になります。
こんな風にアニメのキャラクターなどの固有名詞も抽出できます。
続いて感想を伝えてみます。
「ジバニャンってかわいい。」
ここでは「自立形容詞」を抽出しています。抽出した形容詞の語尾に「よね。」をつけて相づちを打つようにしています。
「そうだよね。」とか返してみます。
抽出対象外の品詞の場合は、適当な返事を返します。ちなみに声は"Open JTalk"のMeiちゃんを利用させて頂きました。
最後に「ポンボット、おやすみ。」と挨拶してみます。
横にiPhoneが写っていますが、これはロボットの頭脳部分になるRaspberryPiのOS、Raspbian Jessie PIXELをリモートデスクトップで表示させています。
よく見るとChromiumブラウザを開いているのですが、このChromiumの音声認識APIを利用して、こちらの会話を読み取っています。
音声認識の許可の為にブラウザのリモートデスクトップを使用しますが、ロボットのマイクから音声入力、スピーカーから音声出力が出来ます。
ロボットはNode.jsで動いています。
使用ハード
- Raspberry Pi2 ModelB
- ラズパイ2用の充電池・マイク・スピーカー・WiFiコネクタ
- Arduino UNO
- GROVE - ベースシールド
- GROVE - I2C モータードライバ
- Arduino工作用の中国製ロボットキット(アクリルボード・モーター・タイヤ・単4電池ケース・超音波センサ・サーボモーター用アームなど)
- Wio Node
- GROVE - サーボモータ ×2
- Wio Node用の充電池
結構色々詰め込んでしまって、安物のモーターシャフトが少したわんでいます。
環境
- RASPBIAN JESSIE WITH PIXEL 4.4
- node.js v6.8.1
- mecab 0.996
- mecab-ipadic-neologd 2.0
Node.jsのライブラリ
"dependencies": {
"express": "^4.14.0",
"johnny-five": "^0.10.3",
"mecab-async": "^0.1.0",
"openjtalk": "^0.1.6",
"request": "^2.76.0",
"socket.io": "^1.5.1"
}
仕組み
ざっくり仕組みを述べます。Node.jsのexpressでローカルサーバーを立て、socket.ioを使いクライアントサイドとサーバーサイドを接続します。
音声認識はWeb Speech Rcognition APIを利用するのですが、今回もライブラリannyang.jsを利用させてもらいます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>PonBot</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="style.css">
</head>
<body>
<article>
<h1>PonBot</h1>
<article>
<!-- 略 -->
</article>
<script src="https://cdn.socket.io/socket.io-1.2.0.js"></script>
<script src="http://code.jquery.com/jquery-1.11.1.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/annyang/2.4.0/annyang.min.js"></script>
<script>
$(document).ready(function() {
var socket = io.connect('http://192.168.0.11');
if (annyang) {
var commands = {'*i': function() {console.log("実行")}};
annyang.setLanguage('ja-JP')
annyang.addCommands(commands);
annyang.start();
console.log('録音開始');
annyang.addCallback('resultMatch', function(userSaid) {
console.log('録音: ' + userSaid);
socket.emit('chat_message', userSaid);
});
};
socket.on('msg', function(msg) {
console.log('アクション');
if(msg === 'action'){
/* ロボットのアクション */
};
});
});
</script>
</body>
</html>
var express = require("express");
var app = express();
var io = require('socket.io')(server);
server.listen(3000, function () {
console.log('listening on *:3000');
});
app.use(express.static(__dirname + '/public'));
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname + '/index.html'));
});
io.sockets.on('connection', function(socket) {
socket.on('chat_message', function(data) {
/*様々な処理*/
});
});
音声認識した言葉を、mecabを利用して形態素解析を行います。これはnode-mecab-asyncを利用させて頂きます。
「感動詞」「固有名詞」「自立形容詞」の抽出はこのように行いました。
var MeCab = new require('mecab-async')
var mecab = new MeCab();
var array = [];
var reply = [];
io.sockets.on('connection', function(socket) {
socket.on('chat_message', function(data) {
MeCab.command = "mecab -d /usr/lib/mecab/dic/mecab-ipadic-neologd"
MeCab.parseFormat(data, function(err, morphs) {
if (err) throw err;
morphs.map(function(morph) {
if (morph.lexical === '感動詞') {
array.push(morph.original);
reply = '!'
console.log(array);
}
if (morph.compound === '固有名詞') {
array.push(morph.original);
reply = 'っていいね。'
console.log(array);
}
if (morph.lexical === '形容詞' | morph.compound === '自立') {
array.push(morph.original);
console.log(array);
reply = 'よね。'
}
});
if(array.length <= 0){
bot_reply = 'そうなんだ。' ;
}else{
bot_reply = array[Math.floor(Math.random() * array.length)] + reply;
}
io.emit('msg','action');
mei.talk(bot_reply);
array = [];
reply = [];
});
});
});
音声発話はnode-openjtalkを使わせてもらいます。
var OpenJTalk = require('openjtalk');
var mei = new OpenJTalk({ htsvoice: '/home/pi/ponbot/node_modules/openjtalk/voice/mei/mei_happy.htsvoice' });
mei.talk(bot_reply);
ローカルサーバーを立て、ラズパイにマイクとスピーカーを接続すればお喋りを楽しむことが出来ます。
ここではArduinoを使いモーター2台を動かし、Wio nodeを使いサーボモーターを2台動かします。
(注:Arduinoで両方動かせれば良かったのですが、RaspPi経由でサーボモータをNodeSerialPortで制御しようとすると、電圧の変化に耐えられずRasPiがショートする可能性があるみたいです。今回モーターとサーボモーター両方使いたかったので、サーボモータはWio Nodeを使い、WebAPI経由で制御してみました。)
Groveシステムを利用するので細かい配線は必要ありません。ArduinoをRasPiから動かすのにはjohnny-fiveを利用させてもらいました。
var five = require('johnny-five');
board = new five.Board();
board.on("ready", function() {
var a = new five.Motor({
controller: "GROVE_I2C_MOTOR_DRIVER",
pin: "A",
});
var b = new five.Motor({
controller: "GROVE_I2C_MOTOR_DRIVER",
pin: "B",
});
io.sockets.on('connection', function (socket) {
socket.on('click_motor_right_turn', function () {
a.rev(381);
b.rev(381);
});
socket.on('click_motor_left_turn', function () {
a.fwd(381);
b.fwd(381);
});
});
socket.ioにイベントが送信されたタイミングでモーターを動かします。
Wio node に Groveシステムを差し込み、iOS(Android)アプリを使用すれば、本当に5分ほどで対応するモーターやセンサーを利用することが出来ます。Wio link - Getting Started
アプリを利用してサーボモーターを操作するAPIとアクセストークンが取得出来ます。今回はロボットのアーム(首?)として利用するので、イベントが送信されたタイミングでrequestモジュールを使ってWio NodeにAPIリクエストを送信させてみました。
var request = require('request');
io.sockets.on('connection', function (socket) {
socket.on('click_servo_yes', function (degree_y) {
console.log('サーボ1: ' + degree_y);
var options_y = {
uri: 'https://us.wio.seeed.io/v1/node/GroveServoD1/angle/' + degree_y,
form: { access_token: 'アクセストークン' },
json: true
};
request.post(options_y, function(error, response, body){
if (!error && response.statusCode == 200) {
} else {
console.log('error: '+ response.statusCode);
}
});
});
socket.on('click_servo_no', function (degree_n) {
console.log('サーボ0: ' + degree_n);
var options_n = {
uri: 'https://us.wio.seeed.io/v1/node/GroveServoD0/angle/' + degree_n,
form: { access_token: 'アクセストークン' },
json: true
};
request.post(options_n, function(error, response, body){
if (!error && response.statusCode == 200) {
console.log(body);
} else {
console.log('error: '+ response.statusCode);
}
});
});
これらを組み合わせて完成です。
まとめ
とりあえず、以前から「あんなこといいな。出来たらいいな。」と思ってたことが出来たんで、色々雑なんですがまあ良しとしたいと思います。
せっかくカメラ用のアームがあるので、カメラをつけて顔認識とか、物体認識とか、感情認識とか…もっと色々出来ると良いなと思います。ではまた。