プレゼンの練習と飲み会のネタのためにパワポカラオケができるアプリを作った話

# TL;DR

* （タイトル通りだが）プレゼンテーション（アドリブ力、トーク力）の練習と、飲み会のコンテンツで困ったとき用に **「パワポカラオケ」** ができるウェブアプリケーションを作った。
* フレームワークとしてAngularを採用し、AngularHttpと無料のAPIで勝手にテーマとスライドが生成されるようにした。
* 結構楽しかった。

## **「パワポカラオケ」** とは

要約すると、

1. 勝手に決められたテーマで、
2. 勝手に与えられたスライドで、
3. その場でいかにも事前に準備していた風を装いながらアドリブでプレゼンをする、
4. Lightning Talk 風のゲームであり、また練習である

詳しくは、以下のリンクを参照されたい。

* [パワーポイントを使ってアドリブで即興プレゼンする「パワポカラオケ」に挑戦した - デイリーポータルZ](http://portal.nifty.com/kiji/170821200468_1.htm)
* [アドリブで即興プレゼン!! パワポカラオケ大会は笑いと熱で溢れていた](https://kobit.in/archives/6951)
* [全NIPPONパワポカラオケ協会（公式） - Twitter](https://twitter.com/pawapokaraoke)

## 成果物

* ウェブアプリケーション - [https://pawapo-karaoke.cf/](https://pawapo-karaoke.cf/)
* Githubレポジトリ - [https://github.com/kyoyababa/powerpoint-karaoke](https://github.com/kyoyababa/powerpoint-karaoke)

> ![Screen Shot 2017-12-10 at 5.52.18 PM.png](https://qiita-image-store.s3.amazonaws.com/0/105252/07b1117e-a4e5-744d-2af6-07c2adec8856.png)
> *せっかくなのでそれっぽいLP的なものも作った.*


## 技術仕様

### プレゼンテーマの生成

Google Trendから何か取得しようと思ったがAPI提供が終了していたようで、 http://kizasi.jp/ という、ブログから話題のキーワードをランキングしているサイトがあったのでそこのAPIを使わせてもらった。

```javascript
private fetchTrendWords(): void {
  const url = 'http://kizasi.jp/kizapi.py?type=rank';
  // GETリクエストの制約上httpsにしたかったので、https形式でAjax通信できるやつを媒介した.
  const target = `https://cors-allow.azurewebsites.net/?url=${url}`;

  // AngularのHttpClientモジュールをhttpServiceとしてInjectしている.
  this.httpService.get(target, {responseType: 'text'}).subscribe((data) => {
    ...
  }
}
```

このままだと、当該APIからはXMLが返されてしまいAngular上で扱うには不便なので、XMLをJSON形式に変換してくれるnpmのパッケージ [xml2js](https://www.npmjs.com/package/xml2js) を使っていい感じに正規表現でキーワードを配列にした。

```javascript
this.httpService.get(target, {responseType: 'text'}).subscribe((data) => {
  xml2js.parseString(data, (response, result) => {
    let items = [];

    // 上記でparseすると、XMLのキーワードリストは result.rss.channel[0].item に入っているので以下のように.
    for ( let i = 0; i < result.rss.channel[0].item.length; i++) {
      items.push(result.rss.channel[0].item[i].title[0]);

      // 余計な単語とHTMLは不要なので正規表現で除外して、半角スペースごとに文字列として分ける.
      let relatedWords = result.rss.channel[0].item[i].description[0]
                           .replace(/.*の関連語:/, '')
                           .replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'')
                           .split(' ');

      for (let j = 0; j < relatedWords.length; j++) {
        items.push(relatedWords[j]);
      }
    }

    // これで、プレゼンのテーマになるキーワードの一覧を配列で取得できた.
    this.items = items;
  });
});
```

あとは `this.items` の中からランダムでひとつサジェストすればプレゼンターにテーマを与えられる。

> ![Screen Shot 2017-12-09 at 9.45.14 PM.png](https://qiita-image-store.s3.amazonaws.com/0/105252/c888ec8d-df0d-3fb9-f8a5-bdbf36531cfc.png)
> *友人にやらせている様子. この人はたまたま「ブレスジャーニー」という言葉を知っていたので、観客からは「お〜」との声が挙がった.*

### プレゼン画像の生成

何の関連性もない画像をランダムで取得するには、 [Flickr](http://www.flickr.com) のAPIが良さそうだった。キーワードを指定せずにGETするとランダムにFlickrに公開されている画像を取得できる。（きわどい写真が出てくる場合もあるので会社でやる場合は注意が必要かもしれない。）

```javascript
private getRandomImages(): void {
  const url = 'http://api.flickr.com/services/feeds/photos_public.gne';
  const target = `https://cors-allow.azurewebsites.net/?url=${url}`;

  this.httpService.get(target, {responseType: 'text'}).subscribe((data) => {
    // 以下、上記と同様にXMLをJSONにパースしているが、そもそもjsonでGETするオプションがあったような気がする. めんどくさいのでこのまま掲出.
    xml2js.parseString(data, (response, result) => {
      const entries = result.feed.entry;
      let images = [];

      for (let i = 0; i < entries.length; i++) {
        const image = entries[i].link.map((link) => {
          if (link.$.rel === 'enclosure') {
            images.push(link.$.href);
          }
        });
      }

      // utilsService.shuffle() は、別途フィッシャー・ヨーテ・シャッフルのメソッドを用意している.
      const shuffledImage = this.utilsService.shuffle(images);

      // あとはプレゼンで使う画像５枚をモデルに格納.
      for (let j = 0; j < 5; j++) {
        this.images.push(shuffledImage[j]);
      }
    });
  });
}
```

> ![Screen Shot 2017-12-09 at 9.46.24 PM.png](https://qiita-image-store.s3.amazonaws.com/0/105252/90dffe8d-dc82-1bee-e576-eaee1733b3a2.png)
> *表示されたよくわからない画像に対していかにもそれっぽく説明するプレゼンター.*

### （ちなみに）スライドの遷移

拙速で開発したので、スライドの遷移はAngularのメソッドではなくとりあえずネイティブの `style.display` でやってしまった。

```javascript
public goToNext(): void {
  const slides = document.getElementsByClassName('jsc-slide');
  const currentSlideNumber = this.currentSlideNumber;
  const currentSlide = <HTMLLIElement>slides[currentSlideNumber];
  const nextSlide = <HTMLLIElement>slides[this.currentSlideNumber + 1];

  if (!currentSlide || !nextSlide) return;

  currentSlide.style.display = 'none';
  nextSlide.style.display = 'block';

  this.currentSlideNumber++;
}
```

また、キーボードの矢印キーも反映させて完成。

```javascript
private bindKeyBoard(): void {
  document.onkeydown = (e) => {
    if (e.code === 'ArrowRight' && ...) {
      this.goToNext();

    } else if (this.currentSlideNumber === this.images.length + 1) {
      // 以下はngOnInitをもう一度発動させるメソッドを別途用意.
      this.restart();
    }
  };
}
```

## おわりに

* 本来の「パワポカラオケ」はプレゼンの時間を５分で区切るルールになっているようだが、（飲み会のネタであるというコンセプトもあり）機能としては割愛した。あとよく考えるとAngularで作っている時点でそれは「パワポ」ではないが、それはGoogle Slideを使ったとて同じことなので気にしない。
* このゲームに必要なのは、 **語彙力・表現力・時事の幅広い知識** だが、何より **自信を持ってドヤ顔で観客に立ち向かう勇気** である。リーダーシップやマネジメント能力に欠けていると悩んでいる人は、ぜひやってみよう。