Raspberry Piとslackを使って、赤ちゃんの泣き声を検知しリモートから子守ができる仕組みを考えてみました。実践のメモになります。

サービスのイメージとしてはこんな感じ

2016-02-06_154321_R_R
使用技術はこんな感じ。

2016-02-06_203258_R_R

泣いたらJuliusが検知してslack(パパ)に連絡、パパはslackからメッセージを送り、家にあるRaspberry Piから音声合成してメッセージで読ませるという仕組みです。音声合成のほかに音楽も鳴らしてみます。

ラズベリーパイ本体以外は全部無料で作れるところがすばらしいですね。家で簡単にIOTが実現できます。

赤ちゃんの泣き声を検知、通達

実は今回これが一番難易度が高いです。まず赤ちゃんの泣き声ってどうやって検知するの?っという課題にぶつかります。

Juliusによる音声認識

過去に卒論にてJuliusを使って泣き声の検知を試みた方がいたようです。

参考 (知能情報システム学) 山本翔太さんの卒論

この記事を読むと赤ちゃんの泣き声は周波数に特徴があり、非言語の連続音であることがわかります。Juliusでは周波数の特徴はわかりませんが、単語や言語なのか判断がスコアによってある程度特定ができますのでこれを使ってみます。

実は・・・申し訳ありません!

この部分はまだ研究中でまともなものができておらず、公開できません。なので仮にえーんと言ったものとして、話を先に進めます。

え!?それじゃ意味ないだろ。えーん なんて単語でしゃべらないだろ!!

。。。。過去の記事を参考に泣いたらslackに投稿するようにします。

まずはJuliusをモジュールモードで立ち上げて、次のようなプログラムを実行します。

#!/usr/bin/perl
use strict;
use IO::Socket;
use IO::Select;

my $host = "localhost";
my $port = 10500;

print STDERR "$host($port) に接続します\n";

# Socketを生成して接続
my $socket;
while(!$socket){
  $socket = IO::Socket::INET->new(PeerAddr => $host,
  PeerPort => $port,
  Proto  => 'tcp',
  );
if (!$socket){
  printf STDERR "$host($port) の接続に失敗しました\n";
  printf STDERR "再接続を試みます\n";
  sleep 10;
  }
}

print STDERR "$host($port) に接続しました\n";

# バッファリングをしない
$| = 1;
my($old) = select($socket); $| = 1; select($old);

# Selecterを生成
my $selecter = IO::Select->new;
$selecter->add($socket);
$selecter->add(\*STDIN);

# 入力待ち
my $command_cry_start = '~/aquestalkpi/AquesTalkPi  "えーーん、僕が泣いてます" | aplay';
my $command_slack_message = 'perl ~/program/slackbot/postMessge.pl MSG';

my $musicPlay = 0;#音楽再生フラグ。2重起動防止

while(1){
  my ($active_socks) = IO::Select->select($selecter, undef, undef, undef);

  foreach my $sock (@{$active_socks}){
    # Juliusからの出力を表示
    if ($sock == $socket){
      while(<$socket>){
        #print $_;
        if ($_ =~/.*<WHYPO WORD=\"([^\"]{1,})\"/) {
          print "$1\n";
          my $callName = $1;
          #print "$callName\n";
          my $result = "";
          my $exeCom;
          
          if ( $callName eq "えーん" ) {
            $result  = system ($command_cry_start);
            $exeCom = $command_slack_message;
            $exeCom =~ s/MSG/"僕が泣いてますTT"/;
            $result  = system ($exeCom);
            print "slackに投稿しました\n";
            
          } else {
            print "音声認識失敗";
            #$result  = system ($exeCom);
          }
        }
        
        last if(/^\./);
      }
    # 標準入力をJuliusに送信
    }else{
      my $input = <STDIN>;
      # 小文字を大文字に変換
      $input =~ tr/a-z/A-Z/d;

      print $socket $input;
    }
  }
}

汚いコードですね。すみません。えーんという単語を検知した場合にslack投稿用のプログラムを呼び出します。

slackによる投稿分

slackの投稿部分はこんな感じ。slackapiのchat.postMessageを使えば簡単に投稿できます。

use strict;
use LWP::UserAgent;
use HTTP::Request::Common;

my $inputText = $ARGV[0];

# POST準備
my $url = 'https://slack.com/api/chat.postMessage';
my %postdata = ( 
 'token' => 'TOKEN',
 'channel' => '#general',
 'text' => $inputText,
 'as_user' => 'true'
 );
my $request = POST( $url, \%postdata );

# 送信
my $ua = LWP::UserAgent -> new;
my $res = $ua -> request( $request ) -> as_string;

#print $res;

※TOKENのところはslackapiにて申請したtokenを使えば投稿できます。

 Slackで確認、リモートから命令を実行

泣き声を確認

slackに投稿されるとこんな感じで泣いたことを確認できます。今回のプログラムだと「僕が泣いてます」と表示されます。

無題

リモートから命令を出します。ここでは

「話す」コマンドと「音楽」コマンドを用意し、命令を出すものとします。

slackにて入力します。

無題

命令を検知して音声合成、音楽再生

最後はラズベリーパイ側でslackに接続して特定の命令が出ていないかチェックをします。

本当はラズベリーパイにhubotを使ってslackから命令を送るのが一般的ですが、家にあるラズベリーパイは外部公開をしたくないので、ラズベリーパイからslackにメッセージを取りに行って命令がないか確認するようにします。オンラインで命令が実行ができないデメリットはありますが、外部公開しない方法としては簡単で良いでしょう。

slackからメッセージを取得

slackapiを使って特定のチャネルに該当の命令がないかチェックします。slackapiのchannels.historyを使用します。

#!/usr/bin/perl
use strict;
use Encode;
use LWP::UserAgent;
use HTTP::Request::Common;
use JSON;
use Data::Dumper;

my @youbi = ('日', '月', '火', '水', '木', '金', '土');
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$year += 1900;
$mon += 1;


#チェック対象時間 n分以内のメッセージか
my $chkmin = 10;
my $adminUser = "USER_ID";#user id

#現在時刻
my $startTime = sprintf("%04d%02d%02d%02d%02d%02d", $year ,$mon, $mday, $hour, $min, $sec);
#print "$startTime \n";

($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time - 60 * $chkmin);
$year += 1900;
$mon += 1;
my $chkTime = sprintf("%04d%02d%02d%02d%02d%02d", $year ,$mon, $mday, $hour, $min, $sec);
#print "$chkTime \n";

# POST準備
my $url = 'https://slack.com/api/channels.history';
my %postdata = ( 
 'token' => 'TOKEN',
 'channel' => 'CHANEL'
 );
my $request = POST( $url, \%postdata );

# 送信
my $ua = LWP::UserAgent -> new;
my $response = $ua -> request( $request );

#print $response->content;

my $items = JSON->new()->decode($response->content);
#print Dumper($items);

my @data = $items->{"messages"};

my $command_talk_space = '~/aquestalkpi/AquesTalkPi  "E" | aplay';
my $command_talk_msg = '~/aquestalkpi/AquesTalkPi  "MSG" | aplay';
my $command_start = '~/aquestalkpi/AquesTalkPi  "みゅうじっく 、スタート!" | aplay';
my $command_stop = '~/aquestalkpi/AquesTalkPi  "みゅうじっく 、ストップ!" | aplay';
my $command_speak = '~/aquestalkpi/AquesTalkPi  "hogehoge、を再生します。" | aplay';
my $command_play_mp3 = 'mpg321 /home/pi/work/music/musicname.mp3 &';
my $command_kill_mp3 = 'perl /home/pi/julius-4.3.1/jclient-perl/killmp3.pl';

my $result;
$result  = system ($command_talk_space);

foreach my $item (@data) {
  foreach my $item_text (@$item) {
  	  if ($item_text->{"type"} eq 'message') {

  	  	my $text = encode('utf-8',$item_text->{"text"});
		my $user = $item_text->{"user"};
		my $ts = $item_text->{"ts"};

		$ts =~ "s/\.[0-9]{1,}$//gc";
		($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($ts);
		$year += 1900;
		$mon += 1;
		my $textTime = sprintf("%04d%02d%02d%02d%02d%02d", $year ,$mon, $mday, $hour, $min, $sec);
		if ($textTime >= $chkTime && $user eq $adminUser) {
			
			my $exeCom;
			my $exeMusic;
			 
			#コマンドテキストをそのまましゃべらせる
			my $comvalue = $text;
			if ($comvalue =~ /^話す .*/) {
				$comvalue =~ s/話す //gc;
				print "$textTime msg command  " . $comvalue  . "\n";
				$exeCom =$command_talk_msg;
				$exeCom =~ s/MSG/$comvalue/;
				$result  = system ($exeCom);
				last;
				
			#該当の曲を再生させる
			} elsif ($comvalue =~ /^音楽 .*/) {
				$comvalue =~ s/音楽 //gc;
				print "$textTime music command  " . $comvalue  ."\n";
				$exeCom = $command_speak;
                $exeMusic = $command_play_mp3;
                $exeCom =~ s/hogehoge/$comvalue/;
                $exeMusic =~ s/musicname/$comvalue/;
                $result  = system ($command_kill_mp3);
                $result  = system ($exeCom);
                $result  = system ($command_start);
                $result  = system ($exeMusic);
                last;
				
			}
		}
  	  }
	  
  }
}

これまた汚いプログラムですね。USER_IDのところは、監視対象のユーザIDを。TOKENのところはapiのtokenを。CHANELのところはCHANELIDをそれぞれ入れれば動作します。

このプログラムでチャネルのメッセージを取得して監視できますので、cron等で定期的に動かすか、常駐プログラムにすれば定期的にslackを巡回して命令があれば実行という流れが出来ます。

まとめ

このサービスで一番難しいのは赤ちゃんの泣き声検知です。完全なものは商用サービスでもあまり見たことがないですね。これが開発できればかなりすごいと思います。自分は挫折気味です。。。

あとはラズベリーパイに命令を送るところはhubotとかを使ったほうがオンラインで命令を送れるため、よりIOTっぽい感じで作ることができます。

slackは専用のツールなどは開発せずに家とリモートを簡単に情報連携をする場を作ってくれますので本当に便利です。今回は泣いてます!とかのメッセージでしたが、画像のスナップショット投稿や、音声投稿も簡単に出来るのでいろんな応用ができると思います。

また機会があれば他のIOTも作ってみたいです。