数千人規模の音楽&テクノロジーフェスにて実況ツイート表示実験が大成功を収めた件

こんにちは。トゥギャッター株式会社でバックエンド中心にエンジニアをしている [@MintoAoyama](http://twitter.com/MintoAoyama) です。

[**Togetter**](http://togetter.com) はツイートを始めとした様々な情報を組み合わせてコンテンツを作り出すキュレーションサービスです。
2009年に誕生してから**今年で9年目**に突入し、現在も**月間PV約7000万、月間UU約1000万という規模**で成長を続けています。

<a href="http://togetter.com"><img width="1064" alt="すごいぞTogetter" src="https://qiita-image-store.s3.amazonaws.com/0/7574/307f039c-ffe6-f841-c90f-5faf07975ed9.png"></a>

海外版の [**Chirpstory**](http://chirpstory.com) も政治家・公共団体・ジャーナリストなど影響力のあるユーザに支えられ、成長を続けています。

<a href="http://togetter.com"><img width="1070" alt="すごいぞChirpstory" src="https://qiita-image-store.s3.amazonaws.com/0/7574/7f12e90f-2849-df17-063a-c61a7d2b05e3.png"></a>

そんなTogetterですが、キュレーションサービス以外にも**イベントスポンサー**と呼ばれる支援業務を行うことがあります。
その支援業務とは、規模を問わず様々なイベント（カンファレンス）主催者と協力し、『ハッシュタグの選定や告知などのツイート実況に関わる支援』『実際のツイート実況のまとめ作成を行う』ことなどです（その成果の一旦である実況まとめが<a href="https://togetter.com/li/896134">こちら</a>）。

これらは一定の評価を頂いておりましたが、実は昨年更に新しい試みを行っていました。
それが、昨年5月に <a href="https://www.j-wave.co.jp/iwf2017/">J-WAVE INNOVATION WORLD FESTA</a> にて実施した**『実況ツイートをリアルタイムで会場内のスクリーンに表示する仕組み』**（通称：**ツイート流れるマシーン**）の提供です。

この仕組み、実現することでイベント来場者の方たちに非常に大きなインパクトを与えることに成功したのですが、社内に関心を持って貰える人があまり居なかったので、事例紹介とともにとりあえずここに整理することにしました。

なお、基本的な仕組みはシンプルなため、外にサーバを置く必要もなく手元の開発環境で手軽に動作確認ができます。
身近な技術カンファレンスで採用してみると結構面白いと思いますので、ご興味のある方は是非お試しください。

# J-WAVE INNOVATION WORLD FESTA について

<a href="https://www.j-wave.co.jp/iwf2017/"><img width="1046" alt="J-WAVE INNOVATION WORLD FESTA" src="https://qiita-image-store.s3.amazonaws.com/0/7574/803cc1d7-9f2d-6bf6-e2b7-bfcefb91c815.png"></a>

**J-WAVE INNOVATION WORLD FESTA** （通称：**イノフェス**） は、日本を代表する各界のイノベーター、アーティスト、最先端テクノロジーを駆使したベンチャー企業が集結した音楽とテクノロジーの祭典です。
昨年春は科学技術の国際会議「G7茨城・つくば科学技術大臣会合」の前夜祭イベントとしてJ-WAVEと筑波大学が共同開催していましたが、好評につき今年も開催される運びとなりました。

音楽プロデューサーに**VERBAL**、テクノロジープロデューサーにAR三兄弟長男の**川田十夢**。イノベーターとして、メディアアーティストの**落合陽一**や、ジャーナリストの**田原総一朗**、元LINE代表で現在C Channel代表の**森川亮**など。更にパフォーマーには**きゃりーぱみゅぱみゅ**や**小室哲哉**らが名を連ねる、なかなかに豪華なイベントになっています。

今年は丁度今週末の 6月3日（土） に開催されます。あまり日がありませんのでチケットの状況などは分かりかねますが、もし興味があれば<a href="https://www.j-wave.co.jp/iwf2017/">公式サイト</a>をチェックしてみてください。

弊社では<a href="https://togetter.com/li/891687">2015年からつくばにオフィスを新設</a>しており、編集・開発ともに筑波大学の学生が所属しています。そのゆかりもあり、Twitter上でもイベントを盛り上げることが可能な施策をイノフェス主催者様へご提案しました。
その中で、本格的な運用は未経験ながら**『実況ツイートをリアルタイムで会場内のスクリーンに表示する仕組み』**（通称：**ツイート流れるマシーン**）も可能だというお話をした結果、非常に面白い試みということで受け入れていただきました。

# 『ツイート流れるマシーン』とは

<img width="150" alt="スマートフォンを使う女性のイラスト" src="https://qiita-image-store.s3.amazonaws.com/0/7574/c047b9d7-bf60-ab4e-da17-ce317f4b4640.png">


通常、イベント参加者がTwitterで感想などをツイートする際、「決められたハッシュタグを文章に含めて投稿する」ことがあると思います。
ただし、「投稿することに慣れていない人だと、ハッシュタグを見つけるまでが手間」であることも少なくありません。

また、ハッシュタグを検索して他の参加者の感想などを確認することもあると思いますが、常に全てのツイートを追うことも少ないと思います。
一方、そもそも「何をツイートしたらよいのか分からない」人も少なくありません。

これらの問題を解決しやすくする仕組みの１つが、イベント会場にてデジタルサイネージや壇上のスクリーンなどにハッシュタグと現在投稿されているツイートを表示する仕組みです。

![写真背後、壇上のスクリーンにツイートが流れている](https://qiita-image-store.s3.amazonaws.com/0/7574/0be0c68f-09be-b70a-6e17-195eea299061.jpeg)

👆は実際にイノフェスにてトークセッションの本番で実況ツイートが流れている様子です。写真背後、壇上のスクリーンにツイートが表示されていることがわかると思います。

「#イノフェス でツイートしてください」とのメッセージに従い投稿すると、すぐに自分が投稿した内容がスクリーンに表示されます。
実際に体験すると分かりますが、シンプルかつインタラクティブなため、投稿者自身だけではなく参加者全員のテンションが自然に上がります。しかも、時折登壇者の方がメッセージに気付くことがあり、その場のトークの内容に影響を及ぼしたりなどエキサイティングな体験につながりやすくなります。
これには、実際に運用していた私自身も興奮しました。（同時にオペレーションに恐ろしいほどのプレッシャーが襲いかかるんですけど…）

# Macで簡単 アプリケーション作成までの流れ

ここで、ざっくりですがアプリケーションの作成方法について触れていきたいと思います。
特に複雑な仕組みではない上に、すぐに実装できるため、ここではMac環境を想定した Node.js + jQuery での実装例をご紹介します。

## 1. Twitter API の登録

ツイートを取得するため、まずは Twitter Developers https://dev.twitter.com/ にてアプリケーション登録を行います。詳細な手順は割愛しますが、ここで consumer_key / consumer_secret / access_token_key / access_token_secret を取得してください。

## 2. Node をインストールする

特にバージョンは指定しませんが、比較的最近のバージョンでも動くと思います（現在私の手持ちでは 6.9.1での動作を確認しています）。
Macであれば、例えばHomebrewをインストール済みの状態で以下のようにnvmを活用し特定のバージョンをインストールできます。

```
$ brew install nvm

（インストール時のメッセージに従い）

$ echo "source \$(brew --prefix nvm)/nvm.sh" >> ~/.bash_profile

$ nvm install v6.9.1

$ nvm alias default v6.9.1

$ node -v
v6.9.1

$ npm -v
3.10.8
```

## 3. 実際にコードを書いてみる

ここではNodeでツイートを取得しクライアントに非同期通信で配信するWebSocketサーバ、HTMLファイルでツイートを取得・表示するWebページを書きます。
単純な構成なら、用意するファイルは `package.json` `stream.js` `index.html` の3つだけで事足ります。

### package.json

```package.json
{
  "dependencies": {
    "config": "*",
    "twitter": "*",
    "socket.io": "*"
  }
}
```

ポイントは "dependencies" です。今回はツイートを扱うため Twitter API にアクセスする "twitter"、WebSocketを扱うため "socket.io" を利用します。

### stream.js

```stream.js
var util = require('util');
var twitter = require('twitter');

var twit = new twitter({
  consumer_key: 'XXXX',
  consumer_secret: 'XXXX',
  access_token_key: 'XXXX',
  access_token_secret: 'XXXX'
});

var keyword = process.argv[2];
var option = {
  'track': keyword
};
console.log('keyword='+keyword);

var fs = require('fs');
var app = require('http').createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end(fs.readFileSync('index.html'));
}).listen(3000);

var io = require('socket.io').listen(app);
io.sockets.on('connection', function(socket) {});
twit.stream('statuses/filter', option, function(stream) {
  stream.on('data', function(tweet) {
    if (!tweet.retweeted_status) {
      console.log(tweet);
      io.sockets.emit('tweet', {
        name: tweet.user.name,
        screen_name: tweet.user.screen_name,
        profile_image_url : tweet.user.profile_image_url,
        text: tweet.text,
      });
    }
  });
});
```

3000ポートでWebSocketサーバを起動、スクリプト実行時に渡ってきた引数をキーワードとして Twitter Stream API に問い合わせ、「リツイートを除いた」結果、ユーザID・ユーザ名・アイコン画像URL・ツイートの文言をクライアントに送信しています。
簡単な動作確認用に、ツイートのコンソール出力も行っています。

また、Webサーバから接続すると `index.html` を返すように設定しています。

### index.html

```index.html
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<head>
  <style type="text/css">
  <!--
  body{
      margin: 0;
      background-color: #434443;
  }
  * html body{
      overflow: hidden;
  }
  div.message {
    margin: 5px;
    padding: 5px;
    color: #fff;
    background-color: rgba(0, 0, 0, 0.95);
  }
  img.profile_image_url {
      width: 15px;
      float: left;
      margin: 3px;
  }
  span.name {
      font-weight: bold;
      margin-right: 2px;
  }
  span.screen_name {
      color: gray;
  }
  -->
  </style>
</head>
<script src="/socket.io/socket.io.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$(function() {
  var socket = io.connect();
  socket.on('tweet', function(data) {
    $('div#contentsArea').prepend(
      '<div class="message">' +
        '‏<img class="profile_image_url" src="' + data.profile_image_url + '">' +
        '<span class="name">‏' + data.name + '</span>' +
        '<span class="screen_name">‏@' + data.screen_name + '</span>' +
        '<div class="text">' + data.text + '</div>' +
      '</div>'
    );
    var tweets = $('div#contentsArea > div.message');
    var tweets_length = tweets.length;
    tweets.eq(0).hide().fadeIn(500);
  });
});
</script>
<body>
<div id="contentsArea"></div>
</body>
</html>
```

雑なCSSが頭に書かれていますが、あまり重要ではありません。カラーリングも暗い部屋のスクリーンで映えるようにざっくり黒を基調に設定していますので、お好みに合わせて変更してください。

`socket.io.js` をCDNからロードし、WebSocket通信を開始しています。
また `jquery.min.js` を同じくCDNからロードし、WebSocketサーバからデータを受け取る度にdivを挿入しています。その際、自然に見えるようフェードインを行ってます。

見た目も処理も手を入れていきたいポイントはたくさんありますが、とりあえずはこの位ざっくりした記述で動作確認が行なえます。

### 実行してみる

それではWebSocketサーバを起動してみましょう。
起動前に、必要なモジュールをnpmでインストールします。マシンパワーやネットワークに依存しますが、最初は少し時間が掛かります。

```
$ npm install
```
インストールが完了したら、nodeコマンドで起動してみましょう。
引数でキーワードを設定すると、そのキーワードを含むツイートが流れます。

```
$ node stream.js "togetter"
keyword=togetter
```

エラーメッセージもなく正常に実行できているようなら、 http://localhost:3000 にアクセスしてみましょう。

![TweetStream.gif](https://qiita-image-store.s3.amazonaws.com/0/7574/a5d264f8-d97c-3826-7f7e-eecb27791983.gif)

該当のツイートが出てくれば成功です！ フェードインでリストの一番上に追加表示されます。

# 本番のトラブルを想定した対策

ここまでで基本的な動作は確認できたわけですが、見た目はともかく、実際に大勢の参加者（それもチケット購入いただいている、イベントを楽しみにしておられるお客様…）の前でオペレーションすることを想定すると、色々なトラブルを考慮に入れる必要があります。

## NGワード

屋内とは言え、ある程度の規模のカンファレンスになると公共の場であることを踏まえた対策が必要になります。アダルトや公序良俗に反する単語が含まれるツイートは表示しないよう、フィルタリングすることが求められるでしょう。
また、Twitterでも普段から見かけるスパムツイートの類いも、明らかにそれと分かるキーワードを含むものを対象にフィルタリングします。（特にトレンドに上がったハッシュタグにはbotから大量のスパムツイートが流れます…。）

これには特製のリストを事前に用意し、スクリプトの実行時に読み込み、フィルタリング処理を実装します。
予めjsonファイルとして作っておくと個別に編集できて便利だと思いますし、適切なデータストアに保存すれば管理画面も実装できます。

## NGユーザ

NGワード同様、明らかに投稿内容に問題が分かっているユーザであればキーワードではなくユーザIDでフィルタリングしてしまう方が効率的です。事前にトレンドに上がったキーワードの検索結果からスパムユーザのリストを作ってしまってもいいでしょう。
また、ユーザ名（screen name）に不適切な言葉が含まれていることもありますので、イベントの内容など状況に応じてフィルタリング条件を考えておきましょう。

## その他、イベント会場にそぐわないもの

トークセッションなどの限られた空間での上映では、一件問題のないツイートでもフィルタリング対象としなければならないことがあります。
それは、イベントに使用するハッシュタグが1つしかない場合などに必ず考慮する必要があることです。

例えば真剣に討論が行われている最中にスクリーンに、「🌭美味しい」というメッセージとともに野外の写真が流れてきたら…。
「楽しそう」「来ました」「楽しかった」などの、本来ありがたい感想ツイートも、トークセッション中に壇上で流すべきメッセージではないこともあります。

しかし、それらのメッセージをNGワードのような形でフィルタリングすることには限界があります。
また、仮に表示されているものをクリックなどで消す処理を実装したとしても、リアルタイムでツイートが流れている以上、観客にはほぼ確実に認識されてしまいます。一体どうすれば不必要なツイートの表示を防げるのか…。

その時思い出したのが、2015年のNHK紅白で行われた 小林幸子×ニコ生 コラボで行われた『コメントをリアルタイムで紅白歌合戦に流すシステム』、そしてその中の『人力フィルター』でした。

<a href="http://ascii.jp/elem/000/001/112/1112997/">![ASCII.jp：NHK紅白に"弾幕"流した男 小林幸子"ラスボス"舞台裏](https://qiita-image-store.s3.amazonaws.com/0/7574/f6d41482-810c-d329-42f1-6ae4c68f008b.png)</a>
[ASCII.jp：NHK紅白に"弾幕"流した男 小林幸子"ラスボス"舞台裏](http://ascii.jp/elem/000/001/112/1112997/)

> いくらNGを強化しても変なコメントが出るのは避けられないんです。いわゆるネガティブワードだけじゃない「番組に出したくない単語」というのもあります。そこで99.999％の精度を求めるためにはそれじゃいかん。それで今回採用したのが、「全部人の目で見る」。

> 10人の監視員を置いて、来たコメントが入力された瞬間、空いているに端末に表示するようにしたんです。まっくらな画面にコメントがポンと表示されて、「アリ」「ナシ」を判定してボタンを押す。

これだ。
オペレーションできるのは私一人ですが、ルールが複雑である以上、かえって一人で行うほうが現実的です。

そこで、**memcached**を用いたバッファ機構を追加しました。

ツイートの取得・送信はもともと非同期で行われるので、tweet idをキーにmemcachedにsave。5秒程度sleepの後、memcachedに保存していた値を再び確認し、送信する仕組みにします。

一方、スクリーンに映すWebページとは異なるページに、先に（リアルタイムで）送信しておきます。表示されているツイートがタップされた場合に対象のtweet idをmemcachedからdeleteしておくと、結果スクリーンには表示されなくなるというわけです。

例えばこのページにスマホからアクセスしておくと、**表示されてから5秒以内にタップすることで対象のツイートを削除できる管理ページ**になり、使い勝手が良くなります。簡易の人力フィルター機能の完成です。

# 前日までに起こったハプニング

## 日本語ハッシュタグが拾えない…

ミッションクリティカルな案件で時間が十分に確保できるなら、出来る限りの検証は行ったほうが安全です。

今回、本番を迎えるにあたり、よりにもよって前日に**対象の日本語ハッシュタグを含むツイートが拾えていない**ことに気が付きました（もっと事前に検証しておけよって話なんですけど…）。

今回公式ハッシュタグを #イノフェス として告知を行っていたのですが、色々なキーワードで試しても、明らかに日本語ハッシュタグを拾えていない…。ネットでそれらしいキーワードで検索してもそれらしい情報は何も出てこない…。
Stream APIだけに実際にリアルタイムでツイートしないと検証できない分、イライラが募ります。

どうしよう。**全部無かったことにして逃げてしまおうか。**いや、それならハッシュタグを英語に変更してもらうか。でも運営が混乱しそうだし、連絡にも手間取りそう。そもそもどんな名前が適切なんだろうか…。

そうぼんやりと考えながらも深夜、コーヒーを飲み一息つきながら一旦冷静になり、Twitter公式のAPIリファレンスを丁寧に読んでいた時、それらしいオプションに気が付きました。

Streaming API request parameters — Twitter Developers
https://dev.twitter.com/streaming/overview/request-parameters#language

"language" です。通常のツイート検索にもありますが、言語で絞り込むことができるオプションです。
今回、余計なツイートを拾わないよう予め日本語限定（`'language': 'ja'`）で呼び出していましたが、該当箇所を外してみると…。拾えるようになった！！！

…なんで日本語に絞り込むと日本語ハッシュタグが拾えなくなるのか…？？？
理屈が通っていない気がしますが、とにかく期待通りになりました。結局のところこの仕様は現在変わっておらず、理由もよく分かっていません。検証は重要です。

# 当日起こったハプニング

## 伊野尾慧ファンの襲来

これは #イノフェス がトレンドに上がってくるまで全く予想が出来ていなかったんですが、「イノフェス」というフレーズを目にしたTwitterユーザが『**伊野尾慧くんのためのフェスティバルだと勘違いするツイート**』を行い、大量に流れてきました。

当時の私の混乱はSlackの分報チャンネルにも記録されています。最初は『何の話をしているのかも分からないツイート』の意味に気付いてからは、NGワードによるフィルタリングなどを駆使して事なきを得ました。

実際にやってみないと予測できないリアクションはこれに限らずいっぱいあります。どんな状況にも対応できる冷静さが必要ですね…。（伊野尾担の皆さん、がっかりさせてしまっていたのならすみません…）

## 会場が一体になって司会に呼びかけ激励する『応援可能上映』

田原総一朗さん、土屋敏男さん、夏野剛さんによる『メディア革命の時代』というトークセッションにて、本来は進行上『ラジオというメディアの可能性』についても触れる必要があったんですが（J-WAVEの主催ですからね…）、三人のトークがテレビやネットなどのメディアの話で加熱し、一向にラジオについて触れない。

ラジオパーソナリティーの岡田マリアさんがセッションの進行役として参加されていたんですが、どんなに根気よく風向きを変えようとしても一向にラジオについて触れない。

その時、『壇上で話す三人』『岡田マリアさん』に加えて、『会場にいるみなさん』がスクリーンに流れるツイートを通して進行に参加する流れが出来たのです。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ラジオの良さを<br>ラジオの良さを‼<br>伝 え て …<a href="https://twitter.com/hashtag/%E3%82%A4%E3%83%8E%E3%83%95%E3%82%A7%E3%82%B9?src=hash">#イノフェス</a></p>&mdash; ねぇ誰か教えて‼bot (@simasimasima3) <a href="https://twitter.com/simasimasima3/status/731383273029394433">2016年5月14日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">てかJWAVE､ラジオ取り上げられてないけどいいの?www <a href="https://twitter.com/hashtag/%E3%82%A4%E3%83%8E%E3%83%95%E3%82%A7%E3%82%B9?src=hash">#イノフェス</a></p>&mdash; marimo＠充電中 (@marimo_nekomimi) <a href="https://twitter.com/marimo_nekomimi/status/731384316891029504">2016年5月14日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">そう！そうです！ラジオの話！！<br> <a href="https://twitter.com/hashtag/%E3%82%A4%E3%83%8E%E3%83%95%E3%82%A7%E3%82%B9?src=hash">#イノフェス</a></p>&mdash; ちょんさんﾀﾞﾖｰ(/・ω・)/ (@Thon_world) <a href="https://twitter.com/Thon_world/status/731385010968629248">2016年5月14日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">&gt;&gt;&gt;&gt;ラジオについて！！！&lt;&lt;&lt;&lt; <a href="https://twitter.com/hashtag/%E3%82%A4%E3%83%8E%E3%83%95%E3%82%A7%E3%82%B9?src=hash">#イノフェス</a></p>&mdash; ぽーる (@lblpaul) <a href="https://twitter.com/lblpaul/status/731385110126174208">2016年5月14日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">わかったからマリアさんに喋らせてあげてぇぇぇ<a href="https://twitter.com/hashtag/%E3%82%A4%E3%83%8E%E3%83%95%E3%82%A7%E3%82%B9?src=hash">#イノフェス</a></p>&mdash; ちょんさんﾀﾞﾖｰ(/・ω・)/ (@Thon_world) <a href="https://twitter.com/Thon_world/status/731385480424480768">2016年5月14日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">マリアさん笑ってる… <a href="https://twitter.com/hashtag/%E3%82%A4%E3%83%8E%E3%83%95%E3%82%A7%E3%82%B9?src=hash">#イノフェス</a></p>&mdash; JKの仮面を被った院生 (@Kiev_nico) <a href="https://twitter.com/Kiev_nico/status/731385416163545088">2016年5月14日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

これらはほんの一部ですが、こういったツイートが全て壇上に流れ出した時の高揚感はよく覚えています。
この時ばかりは本来静かなはずのPA席（観客席より後方に配置される、映像や音声の中継用の作業エリア）からも笑いが起こっていました。「Twitterすごいなー」という声も聞こえてきたりして。

マリアさんが笑っているのは言うまでもなくスクリーンのツイートが目に入ってしまったからです。最終的には観客側に目配せをしながら進行するという素晴らしいセッションとなりました。

下手をすると進行の邪魔になりかねない状況ではありますが、許容できるセッションであったがゆえに起こった嬉しいハプニングです。
こういった奇跡が起きるなら、何度でもこのシステムで貢献したいなと思わせてくれた瞬間でした。

# 今年もよろしくお願いします

以上。色々書いてしまい長くなってしまいました。

記事の始めの方にもありますが今年も <a href="https://www.j-wave.co.jp/iwf2017/">イノフェス</a> は開催されます。昨年同様参加される方はよろしくお願いします。

また、もし参加される予定の方がいれば、『ツイート流れるマシーン』が無事稼働できるよう見守って頂ければ幸いです。

![配信の様子](https://qiita-image-store.s3.amazonaws.com/0/7574/9f24e49f-b187-f052-e250-12e6e7d43960.jpeg)
※ 去年の配信の様子。サーバの状態を確認する時や障害時のスペアのためにMacBookを複数台持ち込んだ。実際はこれに加えてツイート削除操作用のスマホが活躍する。PA席の中に場所を確保して頂いた。