2019/06/05(水)【雨量観測】XRAINのスクリーンショットを会社Slackに投稿するPerlスクリプト

天気情報が流れるチャンネルが会社のSlackにあるのですが、XRAINのスクリーンショットも流れてきてほしくなったので作りました。

ちなみに大学生の頃は大学院の研究室訪問や大学院試験の費用を稼ぐために気象会社でアルバイトしていました。

SlackのtokenとチャンネルIDの取得情報はググってください。

$logo は会社のロゴと矢印が描かれた画像で、矢印の先にオフィスがあるという具合になっています。適当な画像を用意して置き換えてください。

CPANモジュール以外は何か特別にインストール必要なものはありませんでした。

#!/usr/bin/evn perl

use strict;
use warnings;
use utf8;
use Log::Log4perl qw(:easy);
use WWW::Mechanize::Chrome;
use File::Spec;
use File::Basename 'dirname';
use HTTP::Request::Common qw//;
use Encode;
use Furl qw//;
use JSON;
use Imager;
use DDP;

# TODO:
#  * 会社付近の色を取得して雨が降っているか判定

my $dirname = File::Spec->rel2abs(dirname($0));
my $rain_url = 'http://www.river.go.jp/x/krd0207010.php?lon=139.50406730175018&lat=35.674031853779084&opa=0.4&zoom=8&leg=0&intvl=5&ext=0';
my $fn= $dirname . "/xrain.png";
my $logo = $dirname . '/logo_and_arrow.png';

Log::Log4perl->easy_init($ERROR);
my $mech = WWW::Mechanize::Chrome->new(headless => 1);
$mech->viewport_size({ width => 760, height => 780 });
$mech->get($rain_url);
my $png = $mech->content_as_png;

open(my $fh, '>', $fn) or die $!;
binmode($fh);
print {$fh} $png;
close $fh;

my $img = Imager->new;
$img->read(file => $fn) or die $img->errstr;

$img->rubthrough(
  src  => do {
    my $tmp = Imager->new;
    $tmp->read(file => $logo) or die $tmp->errstr;
    $tmp;
  },
  tx => 460,
  ty => 527,
);

$img->rubthrough(
  src  => do {
    my $tmp = Imager->new;
    $tmp->read(file => $logo) or die $tmp->errstr;
    $tmp;
  },
  tx => 1203,
  ty => 527,
);

$img->rubthrough(
  src  => do {
  my $tmp = Imager->new;
    $tmp->read(file => $logo) or die $tmp->errstr;
    $tmp;
  },
  tx => 460,
  ty => 1170,
);
$img->rubthrough(
  src  => do {
    my $tmp = Imager->new;
    $tmp->read(file => $logo) or die $tmp->errstr;
    $tmp;
  },
  tx => 1203,
  ty => 1170,
);

$img->write(file => $fn) or die $img->errstr;

my $req = HTTP::Request::Common::POST('https://slack.com/api/files.upload',
  Content_Type => 'multipart/form-data',
  Content => [
    token    => 'xxxx',
    channels => 'xxxx',
    file     => [$fn],
    title    => Encode::encode_utf8('XRAIN自動投稿'),
    initial_comment => $rain_url,
  ]
);

my $res = Furl->new->request($req);
my $json = Encode::decode_json($res->content);

#p $json;

うまくいけば↓のように投稿されます。cronで定期実行させています。

スクリーンショット_2019-06-05_21_19_07.png

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/

Server-sent Event

最初リアルタイムで反映されなくて試行錯誤していたのですが、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} $_;
        };
    }
}
OK キャンセル 確認 その他