ヒカキンシンメトリーbotの技術的なところの話

5 min read読了の目安(約4800字

突発的にリリースして完全に技術の無駄遣いをしたbotのテクニカルなあれこれを一応書いておこうと思います。
今後お好きな言語で再実装しようと考えている方は参考にしてください。
また、この記事をご覧になる前にブログのエントリを参照することをお勧めします。

ヒカキンシンメトリーbotってなに

これを見ましょう

開発時に定めた要件

  • できる限り先代に寄せる
  • TypeScriptで書く
  • サーバレスを意識した技術選定を行う

採用したサービス

稼働場所

GCPのComputeEngineを採用しています。開発中

Cloud Runを使えばDockerコンテナを動かすことができる

という風に聞いていたのでそこに載せるつもりでしたが、デプロイの時にコンソールから「HTTPのポート開いてないぞ」と言われてそこで根本的にプロダクトの特徴を誤解していることに気づいて大人しくComputeEngineにしたという経緯です。要はこだわりはないです。

また、Dockerイメージの中にGCPのサービスアカウントなどの認証情報を入れることを前提としているためContainer Registryでイメージを管理しています。
これは本来イメージ設計におけるアンチパターンですが、扱う情報が中々に多く環境変数に切り分けるのが現実的ではないと感じた為です。

データベース

Youtubeの動画を処理したかどうかの進捗管理の目的で先代botではローカルのSQLite3を使用していました。
今回のケースの場合は最悪nodeのコンテナの中にSQLite3のバイナリ等を引っ張ってきて使用するなどすればいいのですが、それでは結果的にデータベースのストレージをメンテナンスする手間が増えてしまい非常に面倒なのでFirestoreを採用しました。

Firestoreを使った理由はぶっちゃけ最初は特に無く、フルマネージドで安いデータベースがないかなあという要求で探した結果これが一番近かったというだけです。
ですが、使ってみるとNoSQLのデータベースを使ったことが初めてなのもありますがサンプルのプロダクトが作りやすい非常に良いプロダクトだと感じます。
オブジェクトを適当に放り投げておくことができるのはかなりキックオフの段階でデータベース構造に悩むより遥かにアドバンテージとして大きいと思いました。

顔認識

このプロダクトにおけるコアの部分ですが、先代に倣ってCloud Vision APIを採用しています。
先代が悩んでいた部分でもあります。先代botのREADMEのchangelogにおいて使用APIが

OpenCV -> FaceAPI/OpenCV -> Cloud Vision API

と変遷していたことから選定には苦労されたのかなと思います。私の作るbotはその変遷とその末に出た結論を尊重することを選びました。


その他Twitter APIやYoutube Data APIなどありますが、こちらについては特に語ることはないかなと思います。

使用しているモジュール

画像処理

nodejsで画像処理をしたい場合、sharpJimpを採用する例が多いと思います。(Googleで日本語記事出てたのはこの2つだけだった)
このプロジェクトでは最終的にimage-jsを使用しています。

SharpはCrop(切り取り)のメソッドが見当たらず、resize()をゴリゴリ頑張って使っていくしかなさそうという印象があったので選定の段階で却下としました。

ではJimpはどうだったのかというと、要件は全て満たしており正常通り画像も生成できていたのですがTwitterのAPIにアップロードする段階でかなりの確率で蹴られる謎の現象に遭遇して原因切り分けの結果Jimpの吐き出してる画像はかなりの高確率で蹴られることがわかったので移行しました。

その後ちゃんとTwitterAPIに通る画像処理ライブラリを探し、辿り着いたのがimage-jsでした。こちらはちゃんと想定通りに画像が生成できてTwitterへのアップロードも成功したので良かったです。
しかし使用する上で問題があり、何故か一部のメソッドがTS上で定義されていないと怒られる(選定でjsに書いたときは大丈夫)という状態であり、結果的にrequire()で無理やりAnyにしてインポートして使用するという形に落ち着きました。なんでだろうね。

Twitterまわり

結論からいうと以下の2つを使っています。

前者のtwitはStreamingを使用したツイートの取得に対応していたのでヒカキン氏の投稿の監視に採用していますが、基本的にレスポンスをCallback内で処理するタイプだったので扱いづらいというのが正直なところです。@typesが提供されていたのは非常にありがたかったです。

後者のtwitter-api-clientは完全にTSで構築されているPromiseベースのモジュールで、画像とそれを内包したツイートをアップロードする部分に採用しています。本当はこちらにTwitterまわりの責務を全部ぶん投げてしまいたいのですがStreamingに完全対応しておらずその部分はtwitに任せているという状態です。

定期実行

TwitterのデータはStreamingさせているのですがYoutubeからのデータは定期取得させないといけないので、そこはnode-cronを使っています。
ちなみに私は実行タイミングの設定間違えてFirestoreの読み取りをアホほど発生させたバカ野郎です。みんなは気を付けてね

実際の生成の流れ(以下画伯です)

Twitterから流れてきた画像の場合を例に解説しますと、だいたいこのようなプロセスを経て処理しています。

プロセスフロー

初めにTwitterに画像が含まれているかどうかを検査し、含まれていた場合はCloudVisionAPIへ顔の検出をリクエストします。

詳細は省きますが、リクエストして顔が検出された場合レスポンスの中には BoundingPolyfdBoundingPolyというキー名のプロパティが含まれています。
両者とも顔の矩形の情報を内包したオブジェクト情報ですが、fdBoundingPolyはより狭い顔のパーツが入る矩形を示しています。(図を見たら分かりやすいとおもいます)
ちなみにfdfaceDetectionの略語です。

fd

(face detection) prefix.

fdBoundingPoly.verticesは顔の矩形の頂点座標が入っており、この頂点座標から顔の中心を割り出してそれを軸にシンメトリー画像を作っています。

あとは軸座標を使用して画像を切り抜いて反転、くっつけて完成、アップロードの流れになります。

たいへんだったこと

画像処理ライブラリの選定

最初Jimpを使ってパパっとアップロード、と考えていたのですがまさかbase64だろうがbinaryだろうがアップロードを蹴られると思っていなくて原因がJimp側にあることを確信したときは軽く絶望しました。
そっから先は未開の地と呼べるものであり、無数のメソッドの中から頑張ってドキュメント読んで適してそうなのを試してを繰り返し、ちゃんと動いた時は自分が天才だと信じて疑いませんでした。でなければ気が狂いそうでした。

TSの扱い

TSならではの安心感は書いていて非常に大きく大変でも意義があると感じましたが、型情報の扱いにまだ慣れてないことやundefinedの処理などかなり大変なことが多く苦労しました。
またtwitの型定義情報が古く、足りていないオブジェクトのキーを自前で足したりなどそういったことをしました。

Twitter APIのstatuses/filterに対する誤解

現在StandardのAPIでストリーミングが可能なエンドポイントは

  • statuses/filter
  • statuses/sample

の2つがあり現実的に使用できるのはstatuses/filterのみです。
このbotでもこのAPIを使用してTwitterを監視しているのですが、リクエストオプションのfollowに設定したユーザーのPostのみ流れてくると思っていたらこれはとんでもない間違いでした。
正確には

  • followUser本人の投稿(ReTweet含む)
  • followUserへのメンション(監視外のユーザーからのリプライなど)

などが入ってきます。極端なことを言えば@hikakinに顔が映った画像を飛ばしただけで勝手にシンメトリー画像を作られるという迷惑極まりないことになりかねないもので、statusのユーザー情報をもとにフィルタリングをすれば問題ありませんが一歩間違えば大変なことになっていたかもしれません。

今後について

一応一通りの実装は終わっている、と認識していますがモジュールの更新やアプリケーション自体の機能追加などをちまちま行っていく予定です。

最近考えているのはYoutubeのコミュニティの投稿で、ここにしか投稿されていない画像もあるのでどうにかして取得できないかなあと考えている次第です。