2014/12/20(土)Web Speech API と Twitter n-gram を利用した英語発音矯正ゲーム
4月からは自分が研究室で唯一の日本人になってしまうので、英語の発音のトレーニングをひたすら楽しく積めるWebアプリケーションを研究の合間に作っていました。
「えいごのはつおんとれーにんぐ」 https://pron.chobitool.com/
開発は6日間ぐらいで、そのうち素材集めに3日ほど費やしました。
Web Speech API に音声認識と音声合成のインターフェースがあるので、これらをフル活用しました。出題される問題は Twitter n-gram の高頻度の表現から抽出しています。
いい練習になるので、マイクとChromeがあればどうぞ。
オペレーターズサイドという音声認識ゲームに触発されて「なんだとはなんだゲーム」も作りました。
学生寄宿舎の壁が薄すぎて小さい声でしか練習できないのが辛いところです。
2014/03/08(土)PerlでServer-Sent Events
サーバからPUSHされたイベントを受け取るやつ。(http://www.w3.org/TR/eventsource/)
最初リアルタイムで反映されなくて試行錯誤していたのですが、nginxの設定を変えたらリアルタイムで反映されるようになりました。(http://stackoverflow.com/questions/13672743/eventsource-server-sent-events-through-nginx)
コードは下の通りで「plackup」とかで立ち上げられます。
#!/usr/bin/env perl
use strict;
use warnings;
use AnyEvent;
use Time::Piece;
use HTTP::ServerEvent;
my $AFTER = 1;
my $INTERVAL = 1;
my $DURATION = 60 * 30; # 秒
my $html = do { local $/; <DATA> };
my $app = sub {
my $env = shift;
if ($env->{PATH_INFO} ne '/sse/events')
{
return [ 200, ['Content-Type', 'text/html'], [$html] ];
}
if ( ! $env->{"psgi.streaming"} )
{
my $err= "Server does not support streaming responses";
return [ 500, ['Content-Type', 'text/plain'], [$err] ];
}
return sub {
my $responder = shift;
my $writer = $responder->([ 200, [ 'Content-Type' => 'text/event-stream; charset=UTF-8' ] ]);
my $cnt = 0;
my $t; $t = AnyEvent->timer(
after => $AFTER,
interval => $INTERVAL,
cb => sub {
my $now = localtime->strftime('%Y-%m-%d %H:%M:%S');
my $event = HTTP::ServerEvent->as_string(
id => ++$cnt,
data => $now,
);
$writer->write($event);
undef $t if $cnt > $DURATION;
}
);
};
};
__DATA__
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Server-Sent Events</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
</head>
<body>
<h1>Server-Sent Events</h1>
<div id="msg"></div>
<script>
var eventSource = new EventSource('/sse/events');
var msg = $("#msg");
eventSource.onmessage = function(e)
{
console.log("message");
console.log(e.data);
msg.prepend("<p>" + e.data + "</p>");
};
eventSource.onopen = function(e)
{
console.log("open");
};
eventSource.onerror = function(e)
{
console.log("error");
};
</script>
</body>
2014/02/05(水)Web Speech API でツイート
Web Speech APIのためにマイク買ったので、音声でツイートできるAjaxなCGIを適当に書いてみました。(CGIモジュールなんて使うのもクソ久しぶり)
よく使うような単語やフレーズの認識精度はすげーです。
Web Speech APIの仕様は、「Web Speech API Specification」を参照。
完成したやつ↓(隣の部屋の人に聞こえないようマイクに近づいて小声でしゃべっています)
「index.html」
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>こえでついーと!</title>
<link rel="stylesheet" href="voice_tweet.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="voice_tweet.js"></script>
</head>
<body>
<h1>こえでついーと!</h1>
<div id="status">状態:<span id="desc"></span></div>
<div id="recog_result"></div>
<button>音声認識開始</button>
</body>
</html>
「voice_tweet.css」
@charset "UTF-8";
#recog_result
{
width: 600px;
height: 100px;
border: 1px solid #666;
padding: 10px;
margin-top: 20px;
}
button
{
margin-top: 10px;
}
「voice_tweet.js」
"use strict";
$(function()
{
var script_url = "voice_tweet.cgi";
var api_recog = $("#recog_result");
var api_status = $("#status #desc");
var api_button = $("button");
var is_recognizing = false; // 音声認識中か否か
var interim_result = "";
var error_en2ja = { "no-speech": "何か話して" };
var recognition = new webkitSpeechRecognition();
recognition.continuous = true; // 複数の連続した認識を有効にする
recognition.interimResults = true; // 途中結果を返す
recognition.lang = 'ja'; // 指定しない場合はドキュメントルートのlangが使われる(BCP 47 を参照)
recognition.start();
recognition.onresult = function(event)
{
for (var i = 0; i < event.results.length; i++)
{
api_recog.text(interim_result + " " + event.results[i][0].transcript);
}
var result = event.results[event.results.length - 1];
if (result.isFinal)
{
var last_spoken = result[0].transcript.trim();
console.log("音声認識結果:" + last_spoken);
if (last_spoken === "ストップ" || last_spoken === "stop")
{
recognition.stop();
}
else if (last_spoken === "クリア")
{
api_recog.text("");
}
else if (last_spoken === "削除")
{
api_recog.text( remove_trailing_word( remove_trailing_word( api_recog.text() ) ) );
}
else if (last_spoken === "ツイート" || last_spoken === "ツイード")
{
$.ajax
({
dataType: "text",
data:
{
"tweet_text": remove_trailing_word( api_recog.text() ),
},
url: script_url,
timeout: 10000, // 10秒
type: "POST",
success: function(res)
{
console.log(res);
},
error: function(XMLHttpRequest, textStatus, errorThrown)
{
console.log("ツイート失敗");
//console.log(XMLHttpRequest);
//console.log(XMLHttpRequest.status);
//console.log(textStatus);
//console.log(errorThrown);
}
});
api_recog.text("");
}
interim_result = api_recog.text();
}
};
recognition.onstart = function()
{
is_recognizing = true;
interim_result = "";
api_button.text("音声認識停止");
console.log("音声認識スタート!");
api_status.text("音声入力待ち");
};
recognition.onerror = function(event)
{
var error = error_en2ja[event.error] || event.error;
console.log("音声認識エラー:" + error);
api_status.text(error);
};
recognition.onend = function()
{
is_recognizing = false;
api_button.text("音声認識再開");
console.log("音声認識が終了しました");
api_status.text("音声認識が終了しました");
};
api_button.click(function()
{
is_recognizing ? recognition.stop() : recognition.start();
});
function remove_trailing_word(text)
{
var splited_text = text.split(/s+/);
splited_text.pop();
return splited_text.join(" ");
}
});
「voice_tweet.cgi」
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use open OUT => qw/:utf8 :std/;
use Encode qw/decode_utf8/;
use CGI;
#use CGI::Carp qw/fatalsToBrowser/;
use Net::Twitter::Lite::WithAPIv1_1;
use Text::Truncate;
use Try::Tiny;
my $MAX_TWEET_LEN = 140;
my @IP_WHITE_LIST = qw/150.65.110.57/;
my %WHITE_IP;
@WHITE_IP{ @IP_WHITE_LIST } = ();
my $q = CGI->new;
open(my $fh, '>', 'log.txt') or die $!; # 直前のリクエスト分のログを取る
if ($q->request_method eq 'POST')
{
my $ip = $q->remote_addr;
my $tweet_text = decode_utf8( $q->param('tweet_text') );
if (exists $WHITE_IP{$ip} && length $tweet_text)
{
print {$fh} "$tweet_text\n";
my $nt = Net::Twitter::Lite::WithAPIv1_1->new(
consumer_key => '***',
consumer_secret => '***',
access_token => '***',
access_token_secret => '***',
ssl => 1,
);
my $appendix = " (音声認識ツイート)";
$tweet_text = truncstr($tweet_text, $MAX_TWEET_LEN - length $appendix);
print "Content-Type: text/plain; charset=UTF-8\n\n";
try {
$nt->update("$tweet_text$appendix");
print "ツイート成功![$ip]";
}
catch {
print "ツイート失敗![$ip]";
print {$fh} $_;
};
}
}