一昨日に開催された第2回 botソンに参加して画像レスbotを作成した話
2016年07月09日、株式会社トレタ様で開催されたbotハッカソンこと通称botソンに参加してきました。個人でもくもく開発して最後に発表といった感じの勉強会です
この記事はその勉強会で作った簡単に画像レスをするためのSlack botの紹介みたいなもので、Herokuの使い方などについては触れませんのでご了承ください
背景
最近、Slack Teamを作ったのですが、やっぱり文字やアイコンばかりだと盛り上がりにかけ、誰も発言しなくなる => 過疎のコンボに繋がると思いました
かといって、下記にもあるようにおもしろ画像をセコセコと探しているのを誰かに見られるとものすごい萎えます(ちなみに僕は画像レス用のディレクトリをDropboxに持っていますが、そこから画像を選別しているのを見られるのも嫌です)
オモシロ画像を貼るためにせっせとマウスでURLをコピペする姿も、ウケるために必死な感じが出てしまいますしあまり人に見られたくありません
なので今回は、なるべく少ない手数で、それこそLINEスタンプ感覚で画像レスができるように、 レス画像検索サイトTiqavのAPIとGoogle Custom Search APIを使ってメッセージのとあるキーワードに反応して自動的に画像レスを行ってくれるSlack Botを作りたいと思いました
構成
1年ほど前からHerokuは無料プランでは1日18時間しか使用できなくなったし・・普段RubyばっかでNodeほとんど書かないし、Rubotyとかあるし・・とか思ったのですが、やっぱり一番情報が集めやすそうなHerokuとHubotという構成にしました
近年のベストプラクティスみたいなものがあればぜひ教えていただきたいです
無料プランはほっておくとSleepに入ってしまうため、New RelicでPingを送ってあげたり、1日18時間しか使用できないので、Process Schedulerというアドオンを使って意図的に眠らしてあげたりする必要があります
Tiqav API
Tiqavは画像レスを検索するためのサイトでAPIも公開しています。大体の画像が漫画系なのもいいです
Tiqav
Tiqav API
今回はSearch APIとサムネイル用のImage URLを使います
Search API
GET search
http://api.tiqav.com/search.json?q=[query]&callback=[fucntion_name]
GET search/random
http://api.tiqav.com/search/random.json?&callback=[fucntion_name]
Example response
[ {"id":"3om","ext":"jpg","height":1442,"width":1036,"source_url":"http://example.com/image1.jpg"}, {"id":"1eb","ext":"jpg","height":171,"width":250,"source_url":"http://example.com/image2.jpg"} ]
Image URL
Search APIのResponseにはTiqav上の画像URLが含まれていません(Source URLはすでにNot Foundになっていることが多い)
そこで、ResponseのIDとEXTを組み立ててTiqav上の画像のURLに変換します
http://img.tiqav.com/[id].[ext]
所感
Tiqavの画像はほとんどが漫画画像だと思われるのでキーワードに対する漫画画像のレスポンス率は非常に高いです(tsで発火するようにしてた)
ただし、検索が結構あいまいなところがあり、下記のように意思疎通でできていないことも多いです(getsで発火するようにしてた)
また、Googleと違ってフィルタリングができないため、「ピー」な画像を引くこともたまにあります
誰かに見られたらたまりません
というような問題があったためTiqavに対応してからGoogleにも対応することにしました
Google Custom Search API
無料枠だと1日100回までしかリクエストしか受け取れず、結構さんさんたる言われようですが、検索精度は流石だと思います
なお、Custom Search APIを使用するにはCustom Search API KeyとAPIで使用するCustom Search Engineを指定する必要があるため、API KeyとEngine IDをそれぞれ取得する必要があります
1. Custom Search API
2. Custom Search Engine
それぞれこちらの方が非常に分かりやすく解説してくださっています
また、Custom Search APIのドキュメントはこちらになります
API Reference | Custom Search | Google Developers
Search API
GET search
https://www.googleapis.com/customsearch/v1
Parameters
とりあえず以下のパラメータはつけておきましょう
Name | Description |
---|---|
key | ①で取得したCustom Search API Key |
cx | ②で取得したCustom Search Engine ID |
q | 検索キーワード |
searchType | 検索タイプ |
qには任意の文字列を、searchTypeには画像を検索するために"image"を指定します
Example response
すみません、レスポンスは少し長いので省略します
ただ、Google Search APIは使用制限があるため、実際に使用する前に①のサイトのこの API を API Explorer で試す
というリンクでしっかりテストをするといいと思います
Filtering
こちらはTiqavと違ってただのGoogle画像検索なので「ピー」な画像を引く可能性も高く、漫画画像以外の画像もいっぱいレスポンスに含まれてしまっています
前者に関してはGoogleにはセーフサーチという機能があるのでそれにあやかります
後者に関してもImageMagickで画像の彩度を図ってフィルタリングしようとしたのですが、そもそもGoogle Custom Engineにカラーのフィルタリング機能も備わっていたためこれを使用することで解決しました(Google検索すげー)
Name | Description |
---|---|
safe | 検索セーフレベル("high", "medium", "off") |
imgColorType | 画像カラータイプ("color", "gray", "mono") |
それぞれ"high"と"gray"を選択しました。"mono"より"gray"がちょうどよかったです
セーフサーチはお好みで外してください
所感
検索精度の高さ、検索結果の多さがTiqavの比ではないため、漫画のセリフを入れると大体帰ってくるのがすごいですし、紹介した以外にもたくさんフィルタリング機能があるように見えます
ただし、漫画画像に特化した検索エンジンというわけではないので、グレースケールに近い画像が多く含まれそうなキーワードを入れると、漫画画像以外が返ってきます
なので、うまく使い分けれれば大体のキーワードはなんとかなりそうな気がしました
ソースコード
ソースコードは僕のGit Hubに公開しているので、自由に見ることができますが、一部だけ簡単な解説を入れようと思います
shuffle = (array) -> # For clone arr = array.slice() i = arr.length while --i j = Math.floor(Math.random() * (i + 1)) tmp = arr[i] arr[i] = arr[j] arr[j] = tmp arr # Avoid to be cached same url image by Slack get_timestamp = () -> (new Date()).toISOString().replace(/[^0-9]/g, '') send_with_tiqav = (msg, path, query = {}) -> msg.http(path).query(query).get() (err, res, body) -> json = JSON.parse body if json.length > 0 items = shuffle json msg.send "http://img.tiqav.com/#{items[0].id}.th.#{items[0].ext}?#{get_timestamp()}" send_with_google = (msg, path, query = {}) -> msg.http(path).query(query).get() (err, res, body) -> json = JSON.parse body if json.items.length > 0 items = shuffle json.items.slice(0, 3) msg.send "#{items[0].link}?#{get_timestamp()}" module.exports = (robot) -> robot.hear /(.*?) tr/i, (msg) -> send_with_tiqav msg, 'http://api.tiqav.com/search/random.json' robot.hear /(.*?) ts/i, (msg) -> keyword = msg.match[1] send_with_tiqav msg, 'http://api.tiqav.com/search.json', {q: keyword} robot.hear /(.*?) gs/i, (msg) -> keyword = msg.match[1] send_with_google msg, 'https://www.googleapis.com/customsearch/v1', {key: process.env.GCS_KEY, cx: process.env.GCSE_ID, q: keyword, searchType: 'image', imgColorType: 'gray', safe: 'high'}
1
毎回同じ検索結果が表示されると面白くないため、検索結果からランダム取得をしてきています。JavaScriptで配列をシャッフルする方法にもいろいろとあるのですが、Math.random()
だけだと偏るので、Fisher-Yatesアルゴリズムを使っています
下記サイトでアルゴリズムごとの偏りがグラフで見れるので是非確認してみてください
2
Slackは画像URLを自動的に画像に展開してくれるため、Content-Typeを指定する必要もなく楽でいいですが、同じ画像が使用されると
Pssst! I did not unfurl #{url} because it was already shared in this channel quite recently (within the last hour) and I didn't want to clutter things up.
といって画像を展開してくれないため、画像URLにパラメータとしてTimestampを付与しています(これはどっちでもいいかな)
さいごに
結果として、そこそこの頻度で期待している画像レスが返せるようになったので満足しています
あとはブラックリストを作ったり、画像解析をやっていくともっとやれることもありそうなのですが、その辺りはおいおいやってみて、もしかしたら記事にするかもしれません