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} $_;
        };
    }
}

2014/01/11(土)Directional Formatting Characters に関するメモ

大部分の言語ではテキストを左から右へ表示するけど、アラビア語やヘブライ語は右から左に表示するらしいです。しかしながら、それらの言語のテキスト中に数字や英語などの左から右へ表示されるテキストが含まれると、テキストが「左から右」と「右から左」の双方向性を持つことになってしまいます。そのため、双方向性を持つテキストをどう表示したら良いか曖昧性が生じます。(恐らくレンダリングエンジンが)よしなに表示してくれることもあるけど、それだけでは十分でないケースがあるため、方向性を制御できるように「Directional Formatting Characters」が必要になるっちゅうこっちゃ。

「Directional Formatting Characters」の一覧は以下の通り。

U+061C  ARABIC LETTER MARK
U+2066  LEFT-TO-RIGHT ISOLATE
U+2067  RIGHT-TO-LEFT ISOLATE
U+2068  FIRST STRONG ISOLATE
U+2069  POP DIRECTIONAL ISOLATE
U+200E  LEFT-TO-RIGHT MARK
U+200F  RIGHT-TO-LEFT MARK
U+202A  LEFT-TO-RIGHT EMBEDDING
U+202B  RIGHT-TO-LEFT EMBEDDING
U+202C  POP DIRECTIONAL FORMATTING
U+202D  LEFT-TO-RIGHT OVERRIDE
U+202E  RIGHT-TO-LEFT OVERRIDE

恐らく、99.9%ぐらいの日本語がメインのテキストではこれらのフォーマット文字は不要であると考えられるので、拙作のPerlモジュール「Lingua::JA::NormalizeText」ではこれらの文字を削除するオプションを用意しています。(Webアプリケーションでこられの文字を埋め込んでイタズラされちゃうおそれがあるため)

さらに詳しい情報は↓を参照されたし。 Unicode Standard Annex #9

2014/01/01(水)PerlのUnicode::Numberによる数値漢数字,大字 変換メモ

コード:

#!/usr/bin/env perl

use strict;
use warnings;
use utf8;
use open qw/:utf8 :std/;
use feature qw/say/;
use Unicode::Number;

my $u = Unicode::Number->new;

my @number_system_list = qw/
Japanese_Regular_Simplified
Japanese_Regular_Traditional
Japanese_Legal_Simplified
Japanese_Legal_Traditional
Japanese_Western_Mixed
/;

for my $number_system (@number_system_list)
{
    say $u->number_to_string($number_system, '1234567890');
}

say "\n";

for my $number_system (@number_system_list)
{
    say $u->string_to_number($number_system, '壹拾貳億參仟肆百伍拾陸萬柒仟捌百玖拾')->to_string;
}

# segmentation fault が発生する
#say $u->string_to_number('Japanese_Regular_Simplified', '123');

say $u->number_to_string('Japanese_Regular_Simplified', '123');

say "Yay!";

実行結果:

十二億三千四百五十六万七千八百九十
十二億三千四百五十六萬七千八百九十
壱十弐億参仟四百五十六萬七仟八百九十
壹拾貳億參仟肆百伍拾陸萬柒仟捌百玖拾
12億3,456万7,890


1234567890
1234567890
1234567890
1234567890
1234567890
Not a decimal number: must contain 0-9 at /home/***/Unicode/Number.pm line 43.

という感じで想定外の入力を渡すと死ぬようです。(Unicode::Number Ver. 0.005)

ちなみに「Japanese_Regular_Simplified」みたいなのは「Unicode::Number::System」のSYNOPSISのコードを実行させると出ます。

2013/09/25(水)感情の分類を考える

趣味で人工知能のために会話文の感情分類がしたいなぁっと思ったので、ちょいと調べていました。そもそも感情は明確に分類できるものなのか怪しいのですが・・・。

ちょろっと自然言語処理関連の論文を調べた限りでは、

  • 「感情表現辞典」という辞典の分類:「喜」「怒」「哀」「怖」「恥」「好」「厭」「昂」「安」「驚」
  • Ekmanの分類:「怒り」「嫌悪」「恐れ」「幸福感」「悲しみ」「驚き」
  • Plutchikの分類:「喜び」「悲しみ」「受容」「嫌悪」「恐れ」「怒り」「驚き」「期待」とこれらの合成 (日本語訳は 山口大学工学部研究報告第53巻第1号pp.85-90 を参考にしました:http://memoirs.lib-e.yamaguchi-u.ac.jp/531/11.pdf

という分類がよく使われているようです。

自分がしたいのは会話文の感情分類。

Ekmanの分類は表情から読み取れる感情とのことなので、除外。残りは Plutchik の分類か「感情表現辞典」の分類。

Plutchik の混合感情の考え方とかはよく出来ていると思うんだけど、自分以外の人に混合感情を意識させて妥当な感情を選ばせるのはあまりに負担がかかり過ぎるように思えます。かといって混合感情を意識させない場合、ドンピシャに純粋感情を過不足なく複数選択させるのが困難。

一人で感情分類作業する場合は、Plutchik の分類でもいいけど、誰かに手伝ってもらう場合はPlutchikの分類はキツい。

というわけで、「感情表現辞典」の10分類をベースにすることにしました。

今日、注文していた「感情表現辞典」が来たのでちょろっと読みました。

この10分類は実際の表現例から「喜」「怒」「苛」「悲」「淋」「鬱」「悄(ショウ:しょんぼりする)」「苦」「安」「悔」「昂」「感動」「好」「嫌」「憎」「驚」「怖」「恥」「惑」の19感情を抽出して境界線の引きにくさを理由に以下のように圧縮したもののようです。

  • 「哀」={「悲」,「淋」}
  • 「厭」={「鬱」,「悄」,「苦」,「悔」,「嫌」,「憎」,「惑」}
  • 「昂」={「苛」,「昂」,「感動」}

これでほぼ自分の直感にも反しないのですが、いくらか拡張しました。

人工知能を考えると、

  • 友達としての好き
  • 恋人としての好き
  • 慕う(目上の人への)の好き
  • 憧れの好き はそれぞれ分けるべきと考えて、

「好」ー>「友情」,「恋愛」,「忠誠(忠義)」,「欲」

に拡張しました。

これで「喜」「怒」「哀」「怖」「恥」「厭」「昂」「安」「驚」「欲」「友情」「恋愛」「忠誠」の13感情。

これらの13種のうちのどれかか、これらのうちの組み合わせで会話文に表れるほとんどの感情は網羅できるかな?

-追記-
研究レベルなら、「An Argument For Basic Emotions」とか読んでおいたほうがいいです。これによると、愛はemotional attitudeで感情(情動)と区別されています。 Plutchikの3次元モデルは「The Nature of Emotions」を読んだけど根拠が不明でした。

2013/09/06(金)Perl製Wiki MojoMojo メモ #02 トラブルシューティング

最近同じPerl製Wikiの https://github.com/yuki-kimoto/ringowiki もコミットが盛んになってきましたが、今のMojoMojoを越えるにはまだまだ時間がかかりそうだというわけで本格的にMojoMojoを使うことにしました。

早速いろいろ問題が発生したので解決のメモです。

  • 「ユーザーの新規登録ができない!」 What is this wiki powered by (look at the bottom; 8 letters, all lowercase)? の入力欄でcatalystと何回入力してもパスできないと思ったら、「mojomojo」でした。 MojoMojoのGithubのレポジトリではJun 27, 2013 のコミットでこの問題(?)は解消されているっぽいです。

  • 「JPEGをアップロードするとサムネイルが作れない!」

[error] Caught exception in MojoMojo::Controller::Attachment->thumb "format 'jpeg' not supported - formats bmp, ico, pnm, raw, sgi, tga available for reading

というエラーが出る。

↓で解決。

yum install libjpeg-devel
yum install libpng-devel
yum install giflib-devel

mojomojo再起動 -> 同じエラー

cpanm Imager --reinstall

mojomojo再起動 -> OK
  • 「サムネイルをページに挿入するとエラーが出る!」
    ↓のようなエラー
[error] Caught exception in MojoMojo::Controller::PageAdmin->edit "Operation "ne": no method found,
        left argument in overloaded package Text::Balanced::ErrorMsg,
        right argument has no overloaded magic at hogehoge/DBIx/Class/Storage/TxnScopeGuard.pm line 26."

DBIx/Class/Storage/TxnScopeGuard.pm の26行目の

if (defined $@ and $@ ne '') {

を↓に書き換える。

if (defined $@ and "$@" ne '') {

(Text::Balanced::ErrorMsgが$@に入るがこれをオーバーライドされた「"」を使って文字列に変換する必要がある)

*「登録したユーザを消したい」

perl /script/util/delete_inactive_users.pl

を実行して注意書きをよく読んで適当な引数(本当になんでもいい)をつけて↓を実行

perl /script/util/delete_inactive_users.pl hoge

マークダウンも使えるし、プレビューも見ながら編集できるし、TWikiより高速に動くし、HTMLが綺麗し中々良いです。あとはMojoMojoを拡張するプラグインがあればというところ。

mojomojo