backstage

合唱音源の新着情報の舞台裏

【Azure Cognitive Services】画像認識でalt属性の説明文を自動生成してみた

引越して1ヶ月半、ようやく家にネットが通ったので何かコードを書こうと思いました。

Microsoft Azure Cognitive Servicesには便利そうなたくさんAPIがあります。 そのうちComputer Vision APIを使うと、画像を解析して説明文などを自動生成できます。

参考:過去の記事 s2terminal.hatenablog.com

Computer Vision APIが吐き出す情報は英語か中国語です。 今回はさらにこれをTranslator Text APIで日本語化し、imgタグにして吐き出すプログラムを書いてみました。

f:id:s2terminal:20170814035609p:plain:w360

Public Domain Picturesから適当にいくつか画像を取ってきて、説明文を付けてもらいました。

「木からぶら下がって緑のバナナの束」

木からぶら下がって緑のバナナの束

<img alt="木からぶら下がって緑のバナナの束" src="http://www.publicdomainpictures.net/pictures/130000/velka/banana-bunch-1442837481wI7.jpg#.WZCXAvp82Mc.link">

画像のURLを入力するだけで、こういったalt属性のテキストを自動で書いてくれます。 精度はなかなか良い感じです。

「フェンスの横にゼブラ立って」

フェンスの横にゼブラ立って

<img alt="フェンスの横にゼブラ立って" src="http://www.publicdomainpictures.net/pictures/230000/velka/zebra-at-groenkloof-picnic-spot.jpg#.WZCX2R_ZqTY.link">

フランス料理の添え物みたいな言い回しが気になりますが、内容は完璧です。

「白砂のビーチに立っている人」

白砂のビーチに立っている人

<img alt="白砂のビーチに立っている人" src="http://www.publicdomainpictures.net/pictures/30000/velka/drink-on-beach.jpg#.WZCRVmnOXuE.link">

心霊写真でしょうか。

実装

ReactTypeScriptwebpackという、ほとんど私に馴染みのない技術の組み合わせで作りました。よく調べもせずノリで技術選定したので、生産性の低さが半端無いです。

こんな感じで環境が用意できます。準備は簡単です。

$ echo "{}" > package.json
$ npm install --save-dev webpack typescript awesome-typescript-loader source-map-loader
$ npm install --save react react-dom @types/react @types/react-dom
$ npm install --save superagent @types/superagent

実装時間の8割は、ReactでAPIキーみたいなセキュアな設定情報をどこにどう書けば良いのか悩んでいました。

残りの2割はドラゴンクエスト11でマジスロを回しながらコードを書いたので我ながらかなり適当です。はぐれメタルヘルムがもう1個欲しいので仕方ないです。

メイン処理になるmain.tsxはこんな感じです。

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as SuperAgent from 'superagent';
import * as config from './config';

interface ImageFormProps {
}
interface ImageFormState {
  image_src: string;
  textarea_value: string;
  header: string;
}

class ImageForm extends React.Component<ImageFormProps, ImageFormState> {
  constructor(props) {
    super(props);
    this.state = { image_src: '', textarea_value: '', header: '画像のURLを入力して下さい' };
    this.handleChangeURL = this.handleChangeURL.bind(this);
  }
  handleChangeURL(event) {
    var image_url = event.target.value;
    this.setState({image_src: image_url,header: "(解析しています...)"});
    SuperAgent
      .post(config.computer_vision.api_url)
      .set('Ocp-Apim-Subscription-Key', config.computer_vision.api_key)
      .set('Content-Type', 'application/json')
      .send({url:image_url})
      .end(function(error, response){
        if (error) { return this.setState({header: response.text}); }
        var desc = JSON.parse(response.text).description.captions[0].text;

        this.setState({header: `(翻訳APIのアクセストークンを取得しています...)`});
        SuperAgent
          .post(config.translator_text.issue_token_url)
          .set('Content-Type', 'application/json')
          .set('Accept', 'application/jwt')
          .set('Ocp-Apim-Subscription-Key', config.translator_text.api_key)
          .send()
          .end(function(error, response_token){
            if (error) { return this.setState({header: response_token.text}); }
            var token = response_token.text;
            this.setState({header: `(「${desc}」を翻訳しています...)`});

            SuperAgent
              .get(config.translator_text.api_url)
              .query({
                'appid': 'Bearer ' + token,
                'text': desc,
                'to': 'ja',})
              .set('Accept', 'application/xml')
              .end(function(error, response_trans){
                if (error) { return this.setState({textarea_value: response_trans.text}); }
                var parser = new DOMParser();
                desc = parser.parseFromString(response_trans.text, 'text/xml').firstElementChild.textContent;

                this.setState({header: desc, textarea_value: `<img alt="${desc}" src="${image_url}">`});
              }.bind(this));
          }.bind(this));
      }.bind(this));
  }
  render() {
    return (
      <div>
        <h1>{this.state.header}</h1>
        <form>
          <input
            type="url" placeholder="http://www.example.com/" size={40}
            onBlur={this.handleChangeURL}
          />
        </form>
        <textarea
          placeholder="ここにimgタグが出力されます" rows={5} cols={40}
          value={this.state.textarea_value}
        />
        <img src={this.state.image_src} width="100%" />
      </div>
    );
  }
}

ReactDOM.render(<ImageForm />, document.querySelector('#app'));

handleChangeURL()のコールバックをみると残念な気持ちになってきます。 納品用コードはちゃんとPromiseか何か使ってきれいに書いたほうが良いと思います。

ソースコードの全文はGitHubに上げました。

github.com

まとめ

これくらいなら人間の手で説明文を書いたほうが早いと思いました。

参考文献

【スプラトゥーン2】イカリング2の戦績データをPCブラウザで無理矢理閲覧する

スプラトゥーン2での戦績を閲覧できるイカリング2ですが、 Nintendo Switch Onlineというスマホアプリ内でしか見ることができません。

www.nintendo.co.jp

前作では普通のブラウザ上で閲覧できたので、戦績データをスクレイピングしてクラウドに保存していたのですが、アプリとなると専門外です。

そこで、プロキシを経由して無理矢理に中身を拝見しました。

アプリの通信内容を解析する

今回はプロキシサーバにmitmproxyを使って、イカリングを串焼きにしようと思います。

適当なCentOSホスト上にプロキシサーバを立てました。ファイアウォールの設定で8081番ポート(自由)を自分のIPアドレスに対して公開しておきます。

mitmproxyはpipでインストールできますが、mitmproxy公式のDockerイメージがあったので、それを使って立ち上げました。 良い時代になりました。

$ docker run --rm -it -p 8081:8080 mitmproxy/mitmproxy

プロキシサーバが立ち上がったら、Android/iOSでプロキシの設定をしておいて、 http://mitm.it にアクセスして証明書をインストールし、NintendoSwitchOnlineアプリを開きます。

mitmproxyの画面に https://app.splatoon2.nintendo.net/ というURLへのGETリクエストが出るので、中身を開きます。 iksm_session(イカスミ?)というキーのcookieがあるので、これを控えておきます。 この値を使えば、アプリ上のhttpリクエストが他の環境でも再現できます。いわゆるセッションハイジャックを自身のスマホに対して行います。

ここでPCに戻り、GoogleChromeEditThisCookieを使って、iksm_sessionに先程と同じ値を書き込んで、 https://app.splatoon2.nintendo.net/home にアクセスすればOKです。

f:id:s2terminal:20170723201214p:plain

無事にPCでイカリング2を閲覧できました。

開発者ツールのNetworkタブを覗いてみると、「records」「stages」「active」「timeline」という4つのJSONデータを取得しており、ここさえ叩くことができれば戦績のログデータが手に入ることが分かります。

認証処理を解析してiksm_sessionを手動生成することができれば、スクレイピングでデータを取得することができそうです。 (誰かやって下さい)

参考文献

【IIJmio×mineo×WiMAX】モバイル回線3種でスプラトゥーン2してみた

f:id:s2terminal:20170716150553j:plain

先日、スプラトゥーン2 前夜祭が開催されました。 データが本編には引き継がれず勝敗が関係ないので、試しにモバイル回線を使ってみることにしました。

格安SIMであるmineoIIJmioテザリング、およびUQ WiMAXモバイルルーターを使って、それぞれ3試合ずつしてみました。

回線のスペック

- WiMAX2+ mineo IIJmio
下り 14.25 Mbps 2.09 Mbps 1.42 Mbps
上り 6.66 Mbps 3.38 Mbps 4.03 Mbps
ping 67 ms 108 ms 113 ms

(測定環境: 大阪市内 休日 日中 RBB SPEED TESTを利用)

注目すべきはping応答速度で、WiMAXの67msに対して、テザリングのmineoとIIJmioは2倍近い差をつけられています。 WiMAXなら比較的ラグの少ない環境で通信対戦できると言って良いと思います。

ナワバリバトル結果

f:id:s2terminal:20170716150612j:plain

3回線での合計9試合中、WiMAXで2勝、残りは全敗という散々な結果に終わりました。

単純に、私の得意武器が使えずスプラシューターコラボを使うしかなかったのもありますが、ちょっとひどい戦績です。

f:id:s2terminal:20170716150659j:plain

プレイ中、ゲームが止まったりワープするといった、ラグによるあからさまな異常は起きませんでした。 安定した通信さえできていれば、最低限のマナーを守って遊ぶことができると言えます。

シビアな格闘戦を制することもでき、スペシャルウェポンのジェットパックを使えば次々とキルを取ることができ、普通に楽しむことはできました。 しかし塗りやダメージの反映がちょっと遅れている感じはして、その結果相打ちになったりはしました。ある程度は枷になっていると思います。

またIIJmio利用中に1度だけ、マッチング中に通信エラーが起きてしまい対戦できない時がありました。

通信量について

1回のナワバリバトルに使用する通信量は約10MB/3分程度でした。 秒間に直すと55KB程度なので、通信速度は大抵の4G回線ならば問題ないと思います。

100試合で1GBと考えると、ちょっとずつ遊ぶ分にはモバイル回線の月間通信量が大きく不足することは無いでしょう。 ただしフェスでカンストまで進めたり、ガチマッチをやり込んだりすると、足りなくなると思います。

まとめ

f:id:s2terminal:20170716150815j:plain

  • モバイル回線でも遊べないことはない
  • やり込むなら固定回線必須

NintendoSwitchのスプラトゥーン2は前作WiiUと異なり携帯モードで遊ぶこともでき、必ずしも自宅など固定回線がある環境でなくてもプレイができます。 出先でちょっと遊ぶくらいなら4Gスマホのモバイル回線テザリングでも可能です。 しかし、本格的に楽しむためにはやはりモバイル回線ではなく、家庭の高速で安定した固定回線が必要になってくると思いました。

個人用mastodonサーバーを構築する

f:id:s2terminal:20170423131210p:plain

ニンジャスレイヤーのmastodonサーバーができたようですが、私はmastodonアカウントを持っていないのでフォローできませんでした。

diehardtales.com

上記記事より引用

あなた自身でサーバーを立てて象になることもできますが、それにはある程度のUNIX知識が求められます。

ではせっかくなので、個人用にサーバーを立てて象になってみました。

なおUNIX知識が求められるそうですが普通にLinux(CentOS7)で構築します。

用意したもの

mastodonAWS S3に対応しているそうなので、特に理由が無ければサーバーもAWS EC2で用意するのが無難だと思います。 真面目にやるならAWS ECS+S3+ElastiCache+PostgreSQL on RDSみたいな構成になるのではと思いますが、今回は個人用なので適当です。

サーバーにはdockerやnginxなど必要なものを入れておきます。

アプリケーションサーバーの準備

jtwp470.hatenablog.jp

上記記事などに沿ってdockerイメージのビルド、RailsのアセットプリコンパイルとDBマイグレーションまで終わらせます。 ちょっと時間がかかりますが、気長に待ちます。

最終的に、$ docker-compose up -d すればOKです。

SSL証明書の取得

qiita.com

この作業のときにサーバーの443番ポートをパブリックに開放する必要があります。 (逆に、このとき以外はファイアウォールで必要なアクセス元以外を閉めておけば個人利用には問題ありません)

Let'sEncryptの証明書は3ヶ月で切れますが、3ヶ月も経てば飽きて辞めるか本格運用のためイチから作り直すと思うので自動更新とかはしなくて良いと思います。

メールサーバーの設定

個人利用ならばSMTPサーバーは用意しなくてもDBを直接操作でOKみたいですが、今回はせっかくなのでSendGridを使ってメール送信設定をします。

s2terminal.hatenablog.com

SendGridのアカウント情報を.env.productionに書いておきます。

シングルユーザーモードに設定

qiita.com

上記を参考に、自分のアカウントに管理者権限を付けて、 .env.productionSINGLE_USER_MODE=trueをコメントインし、docker-composeの再ビルドを実行します。

ファイアウォールの設定

今回は個人の検証用途なので、443番ポートは個人のIPアドレス以外に対して基本的に閉じています。IDCFクラウドIPアドレス仮想ルーターファイアウォールで設定しています。

他に、フォローしたいアカウントのあるサーバーからのアクセスを許可する必要があります。今回紹介したようなシンプルな構成だとドメインのAレコードIPとゲートウェイIPアドレスは一緒だと思うので、たとえばニンジャスレイヤーをフォローするには$ dig a dhtls.netした結果をファイアウォールの設定に貼り付けていますが、この運用には限界があります。

本格的に使いたいならば443番ポートをパブリックにできる環境を用意する必要があると思います。

参考

2017年春 IT系カンファレンスまとめメモ

特筆がない限り日時は現地時間。

Facebook F8 2017

www.fbf8.com

2016年のF8ではチャットボットフレームワークが発表されたりしました。
Facebook「F8」開発者カンファレンスの発表まとめ | TechCrunch Japan

Amazon Web Service Global Summit

なおAWS re:Inventは11月27日~12月1日。新製品の発表などはこちらがメインな印象です。
https://reinvent.awsevents.com/

Microsoft Build 2017

build.microsoft.com

2016年のBuildでは Bash on Windows が発表されて大変な反響がありました。
今年は5月中旬と、ここ数年の傾向からすると遅めの時期ですがHoloLensやAzure関連での新情報が楽しみです。

なお de:code 2017 は5月23日~24日です(※日本時間)
https://www.microsoft.com/ja-jp/events/decode/2017/

Google I/O

events.google.com

Google I/O 2016ではInstant Appsという機能が発表されたりしました。
インストールなしでアプリを実行するAndroid Instant Apps発表。リンクを踏んで起動、終了すれば消去 - Engadget 日本版

個人的にはAppleの方が作りたがりそうな機能だと思ったので、これを聞いたAppleWWDC 2017でどんなモノを後出ししてくれるのか楽しみです。

Apple WWDC 2017

E3

まとめ