pixiv insideは移転しました! ≫ https://inside.pixiv.blog/

ES2016+ on herokuでSlackにイラストを流してユーザ像を感じ取る話

ピクシブ株式会社 Advent Calendar 2016、15日目の記事です。

はじめまして。4月より入社した新卒エンジニアの@f_subalです。普段は3分動画でお絵かき上達ができるサイトsensei by pixivの開発などに携わっています。今回は、senseiの新機能である お題投稿機能 をリリースした際に、ユーザーの皆様が投稿したイラストをSlackに流すbotを開発した話をします。

senseiにおけるお題投稿

senseiは3分間でイラストの描き方を学べる動画を100件以上提供しています。人体・背景・キャラクターの講座をはじめ、有名クリエイターのメイキング動画もあり、手軽に描き方を学ぶことができます。

が、講座動画を見た後で実際に絵の練習をするというアクションにつなげるための仕組みはいささか不足していました。そこで開発されたのがこのお題投稿機能です。下図のようなお題のテンプレートがあり、これを元に弊社サービスの pixiv Sketch に練習イラストを投稿してもらえるという機能になっています(実際にはpixiv Sketchの機能である「描いてリプライ」を用いて、sensei公式アカウントの投稿にリプライする形で投稿します)。

f:id:devpixiv:20161215170121p:plain f:id:devpixiv:20161215170130j:plain

ちなみにこれは私の絵です。

sketch.pixiv.net

お題には2種類あります。各講座に紐付くお題と、日替わりで提供される「今日のお題」です*1。前者は例えば「横顔を描く」講座なら「横顔のアタリを模写してみよう」といった内容で、基本的に変わりません。一方で、今日のお題は毎日違うものが増えていきます。Twitterの@pixivsenseiでも毎晩20:00に今日のお題告知を流しているので、お気軽に参加してみてください。

Slackbotでお題投稿の様子を知る

さて、こうしてリリースされたお題投稿機能でしたが、実際にどういうイラストが投稿されるのか、またどういうお題が盛り上がるのかをチームとして把握したい気持ちがありました。もちろんDBを叩いて後から各お題への投稿数を見ることはできますが、どうせなら投稿ピーク時の盛り上がりをリアルタイムに感じたいですし、魅力的な投稿がすぐに捕捉できるに越したことはありません。

そこで、社内のSlackに専用のチャンネルを用意して、pixiv Sketchにお題テンプレートへのリプライがあるたびに通知するbotを作りました。実装方法は色々ありますが、今回はHeroku にexpressJSベースのアプリケーションを立ち上げ、Heroku Schedulerで定期実行する方式にしました。

アプリケーションの構成

今回は Babel + webpack + Node.js を用いてbotを作成しました。選択の理由としては、Node.jsが単純に他の言語より手に馴染んでいたこと、あとは仕様上不定回数の非同期処理が発生するので、async/await が欲しかったというのが大きいです(後述)。

Babelとwebpackの用意については弊社の@geta6のブログ記事が参考になります(ちょうど本日 webpack v2 が rc になりましたが、以下ではwebpack v1を前提に記述します。async/await も現在は stage-4 に格上げされましたが、一部記述に stage-3 であった頃の設定が含まれています。こちらも注記しつつ説明いたします)。

さて、今回のbotには4つの要素が必要になります。

  1. pixiv SketchのAPIを叩いて、お題テンプレートやそのリプライ作品を取得するクラス
  2. DBを叩いて、すでに送信済みの作品IDを取得/格納するDAOクラス
  3. 1.で取った作品IDと2.で取った作品IDの差分を取り、まだ送ってない作品を調べるクラス
  4. 3.の結果をSlackに送信するクラス(こいつが上の3つのクラスを叩くエントリーポイントになる)

各クラスをES classで書いて import します。DBはHeroku Postgresを用いました。

async/await を用いたAPIリクエスト

さて、senseiのお題は毎日増えます。それはつまり、お題テンプレートが毎日増えるということであり、したがって各お題テンプレートへのリプライを取得するAPIリクエストの回数も増える(=一定値にならない)ということです。

一般に、APIを外から叩く際には、連続で叩いて相手先に負荷をかけないように間隔を空ける必要があります。そのためにはいわゆる sleep 関数を挟みつつ、リクエスト処理を直列に実行できなければなりません。

かつてJavaScriptにおいて、非同期処理を直列に不定回数実行するのは難しいことでした*2。しかし ES2017 に導入予定の async/await 構文を用いれば、非同期処理の直列実行が簡単に実装できます(今回botの実装にBabel + webpackを選択した主要な理由がこれです)*3

.babelrcの presets に stage-0 を加えると、ES.nextにて提案段階の機能を用いることができます(実装当初は presetsに es2015stage-0 を入れていました)。ですが async/await は今夏に Stage 4(finished)に上がりましたので、これだけを目当てにするなら現在は必要はありません。

$ npm install --save-dev babel-preset-latest
{
  "presets": [
    "latest" // es2015 ~ es2017の内容がすべて入る(async/await含む)   
  ]
}

sleep 関数は自作しても良かったのですがnpmに便利なモジュールがあるのでそちらを使いました: wait-promise)。

import { sleep } from 'wait-promise';
import { map as pluck } from 'lodash';

......


async fetchRepliesByItemIds(ids) {
    let result = [];

    for (let id of pluck(templateItems, 'id')) {
        const replies = await fetchRepliesByItemId(id);
        result = [...result, ...replies];
        await sleep(1000);
    }
    return result;
}

for ループの中でfetchしていることに注目してください。

HTTPリクエストにはaxiosを用いました。デフォルトでPromiseを返すので async/await を使いたいときには大変便利です。ブラウザ上でも動くので、モダン環境でのAjaxにも良さげです。SlackへのWebhookも同じくaxiosで投げています。

import axios from 'axios';
import { get } from 'lodash';

......


sendToSlack(itemsToSend) {
    const attachments = itemsToSend.map(item => {
        const url  = BASE_URL + get(item, ['id'], '');
        const text = get(item, ['text'], '');

        return {
            "fallback": `${text} ${url}`,
            "text": `${text} ${url}`,
            "image_url": get(item, ['image', 'url'], ''),
            "unfurl_links": true,
            "unfurl_media": true
        }
    });

    return axios.post(SLACK_WEBHOOK_URL, {
        attachments: attachments
    });
}

投げ終わったら、送信済みの作品として、IDをデータベースに入れます。

実際の運用

上のbotを10分に1回のペースで実行しています。これでもなかなかに臨場感があり、チームメンバーも魅力的な投稿イラストを目にする機会が増えました。

弊社Slackの #_sensei_odai チャンネルができて以降、ユーザがどのように使っているかを知りやすくなりました。「このユーザーさんよく投稿してくれるな」とか「こういうテーマはみんな自分の推しカプにやらせたくなるのだな」といったことが肌感で分かるようになりました。

senseiチームでは翌月のお題を考えるブレストミーティングを月に1回行っているのですが、こうした形でユーザー像ををチーム内で持てるようになった結果、お題を考える際に「これはみんな好きそう」「これはみんな描く気にならないのではないか」といった判断がしやすくなりました(もちろんこれは感覚値ですが)。絵描きの気持ちになるということは、お絵描き学習サービスをやるにあたってとても重要な事だと改めて思います。

まとめ

ピクシブ株式会社では、クリエイターの立場に立って技術力を奮ってくれるエンジニアを募集しています。明日は @uchienneo が、GitLabを用いたプロジェクト運営についてのお話をしてくれます! それでは、明日からも引き続きピクシブ株式会社 Advent Calendar 2016をお楽しみください。

*1:実は「お題投稿機能」は厳密には前者の「講座お題」の投稿機能のみを指します。日替わりでお題を出して描いてもらう企画は、この機能のリリースより前からpixiv Sketch上で実験的に行っていましたし、sensei側の機能自体とは独立です。

*2:並列での実行であれば jQuery.when や Promise.all を用いて簡単に行なえますが、今回はそういうわけにいきません

*3:一応 async/await を使わなくても頑張ればPromiseの入った配列を Array#reduce する方法などで実現できますが、ちょっと冗長です