Spajam2017優秀賞「嫌われAIの命名」の発想プロセスからiOSアプリ実装まで

# 概要


先日、[Spajam](http://spajam.jp/final/result/) というハッカソンに参加しまして、私の参加したチーム「Rising Sun」は東京B予選を**最優秀賞**で突破、そして箱根温泉で行われた本戦にて**優秀賞**を獲得しました！初めてのハッカソンで全国２位という結果が取れたのはとても自信になりました。

今記事では箱根本戦で優秀賞を獲得したアプリ「**嫌われAIの命名**」の設計や実装について紹介します。

![FullSizeRender 3.jpg](https://qiita-image-store.s3.amazonaws.com/0/111366/98a5bd83-ea1f-5ee1-3c8c-d3b399e3e20c.jpeg)

ちなみにですが、このアプリ、**8/7(月)**にリリースしております😎

https://itunes.apple.com/jp/app/id1263913285?l=en&mt=8

# コンセプト

## 発想プロセス
<img width="1280" alt="スクリーンショット 2017-08-09 21.21.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/6f5480a4-2feb-15d1-7c7e-7403f0c34de4.png">

Spajam2017の箱根本戦のテーマは「**勇気**」でした。

最終的には、「**知り合いから友達にステップアップするためには勇気がいるよね？ その勇気を後押しするためのツールをつくったよ！**」って感じです。

友達と仲良くなるに当たって、**呼び方**というのは意外に重要だったりします。あだ名をつけるとよそよそしさが無くなって一気に仲良くなれるという経験をしたことがある方もいるのではないでしょうか？

(**しかし、ぶっちゃけ言うと、このコンセプト設計のロジックは後付けだった部分もあります笑**)

## 本当はこのように考えました。

私たちのチームは東京B予選でも**ラップアプリ**を作っていますが、ここでの勝因の一つに「**世の中のトレンドを掴んでいる**」というものがあります。今日本ではラップブームが起こっていますよね。(ちなみに私はラッパーではなくダンサーです)

今回の「勇気」というテーマに対しては、トレンディなキーワードとして「**嫌われる勇気**」を思いつきました。これに様々な要素を被せていきます。

したがって、アプリのコンセプト設計のプロセスとしては、

「嫌われる勇気」　
-> 「嫌われる勇気」を後押しするアプリ 
-> ニックネームを代わりにつけてくれるアプリ
+ (流行りの)AI 
+ (流行りの)嫌われマツコ
=> 「**嫌われAIの命名**」 

こんな感じです。

# アプリ設計

```
あだ名を作る材料 -> AIがあだ名を作る -> 音声が出てくる
```

このようなプロセスを経ます。それぞれ以下のような仕様を考えました。

## あだ名をつくる材料はツイート
<img width="1280" alt="スクリーンショット 2017-08-09 21.36.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/18b5337e-1fda-efd2-7407-6fd2609a5ff6.png">

TwitterKitを使って、あだ名をつけたい人の直近500文字のツイートを集めます。これがあだ名をつけるための材料。

### TwitterKitのAPIを使って取得する様子

```swift
        let client = TWTRAPIClient.withCurrentUser()
        
        let dataSource = TWTRUserTimelineDataSource(screenName: screenName, apiClient: client)
        dataSource.loadPreviousTweets(beforePosition: nil, completion: { result in
            guard let datas = result.0 else {
                self.showAlert()
                return
            }
            
            var tweetStrings = ""
            
            for tweet in datas {
                let text = tweet.text
                tweetStrings += text
            }

        }
```

## ツイートからIBM Watsonが性格を分析する
<img width="1017" alt="スクリーンショット 2017-08-09 22.22.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/69e61ab5-df77-7eb5-d33a-6046c6a84594.png">

IBM Watsonの[Personality Insights ](https://www.ibm.com/watson/jp-ja/developercloud/personality-insights.html)のAPIを使って、文字列(収集したツイート)から５つの性格要素をパーセンテージで表してもらいます。

以下のjsonから`personality`を取り出し、`Openness`(開放性),`Conscientiousness`(誠実性),`Extraversion`(外向性),`Agreeableness`(合理性),`Emotional range`(感情幅)の`percentile`を使います。
 
### レスポンス例

```json
"personality": [
    {
      "trait_id": "big5_openness",
      "name": "Openness",
      "category": "personality",
      "percentile": 0.8011555009553,
      "raw_score": 0.77565404255038,
      "children": [
        {
          "trait_id": "facet_adventurousness",
          "name": "Adventurousness",
          "category": "personality",
          "percentile": 0.89755869047319,
          "raw_score": 0.54990704031219
        },
        . . .
      ]
    },
    {
      "trait_id": "big5_conscientiousness",
      "name": "Conscientiousness",
      "category": "personality",
      "percentile": 0.81001753184176,
      "raw_score": 0.66899984888815,
      "children": [
        {
          "trait_id": "facet_achievement_striving",
          "name": "Achievement striving",
          "category": "personality",
          "percentile": 0.84613299226628,
          "raw_score": 0.74240118454888
        },
        . . .
      ]
    },
{
      "trait_id": "big5_extraversion",
      "name": "Extraversion",
      "category": "personality",
      "percentile": 0.64980796071382,
      "raw_score": 0.56817738781166,
      "children": [
        {
          "trait_id": "facet_activity_level",
          "name": "Activity level",
          "category": "personality",
          "percentile": 0.88220584913965,
          "raw_score": 0.60106995926143
        },
        . . .
      ]
    },
    {
      "trait_id": "big5_agreeableness",
      "name": "Agreeableness",
      "category": "personality",
      "percentile": 0.94786124793821,
      "raw_score": 0.80677815631809,
      "children": [
        {
          "trait_id": "facet_altruism",
          "name": "Altruism",
          "category": "personality",
          "percentile": 0.99241983824205,
          "raw_score": 0.79028406290747
        },
        . . .
      ]
    },
    {
      "trait_id": "big5_neuroticism",
      "name": "Emotional range",
      "category": "personality",
      "percentile": 0.5008224041628,
      "raw_score": 0.46748200007024,
      "children": [
        {
          "trait_id": "facet_anger",
          "name": "Fiery",
          "category": "personality",
          "percentile": 0.17640022058508,
          "raw_score": 0.48490315691802
        },
        . . .
      ]
    }
  ]
```

出典： https://www.ibm.com/watson/developercloud/personality-insights/api/v3/#profile

詳しくは [SPAJAM2017で優秀賞を獲ったはなし（バックエンド）|enish engineering blog](http://engineering.enish.jp/?p=1280) にも書いてます。

## 性格からあだ名を決定する

ここで機械学習が登場すればいいんですが、今回は**筋肉エンジニア**という人間が単語の候補を考えていますw

<img width="1047" alt="スクリーンショット 2017-08-09 22.25.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/dd06fef9-56e2-0630-9320-96188ec6c521.png">

取得した`percentile`をもとに、自分たちで定義した**嫌われラインすれすれの単語**をマッピングします。

### 例：**開放性**からのスコアリングと単語のマップ

まず、開放性のパーセンテージをlow, middle, higheの三段階に分けます。

```swift
        switch inOpePer {
        case 0 ..< 1/3:
            score = .low
        case 1/3 ..< 2/3:
            score = .middle
        case 2/3 ... 1:
            score = .high
        default:
            break
        }
```

次に３つのstring配列をもつ２重配列を用意しておき、lowは0番めから、middleは1番めから、highは2番めの配列から単語を選ぶことを決定します。

```swift
let nounList =
[
  ["泥棒", "ナルシスト", "深海魚", "妖怪", "キャバ嬢", "変質者", "大学生"],
  ["ポンコツ", "ゴリラ", "モンスター", "サーファー", "おかま", "一発屋", "エンジニア"],
  ["おじさん", "ホームレス", "ホスト", "ヤンキー", "原始人", "デブ専", "サラリーマン"]
]
```

あとはその配列の中からランダムで使う単語を決定します。

この作業を５つのパーソナリティに対して行い、５つの単語を決定します。

決定した５つの単語を組み合わせたり、あえて１単語だけにしたりして、**最終的なあだ名を決定します。**

![DGrjj_1U0AABD9i.jpg](https://qiita-image-store.s3.amazonaws.com/0/111366/c5b2f81e-9dc0-feb6-0fcd-eeef5dac6a8b.jpeg)

https://twitter.com/keiyonekawa/status/894777686630977538 (僕の友達のあだ名)


## 出来たあだ名を**AI Talk**に喋らす

<img width="1280" alt="スクリーンショット 2017-08-09 21.37.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/84ab9b59-53e6-45a3-50f1-8e353b5a56fa.png">

そして出来たあだ名をSpajamで提供されていた[AI Talk](http://www.ai-j.jp/about/)を使って**あんず**ちゃんに喋ってもらいます。

<img width="421" alt="スクリーンショット 2017-08-09 22.38.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/b4785a10-af39-b1b0-b7bd-e18e783cfdb7.png">

[AI Talk](http://www.ai-j.jp/about/)のAPIは東京B予選でも使ったので、その時のコードを流用することができました。

[AI Talk](http://www.ai-j.jp/about/)は、文字列を投げると、音声ファイルを返してくれます。普通は有料だと思いますが、ハッカソンで提供されていたので使い倒しました。（ストアにリリースするときは代わりにiOSのAVSpeechSynthesizerに喋らせてます）

大まかなアプリの仕組みは以上です。

# その他の技術

## 動画再生

![8月-09-2017 22-48-50.gif](https://qiita-image-store.s3.amazonaws.com/0/111366/a0282a73-52fe-dd97-e411-a92800c16c94.gif)

私たちのチームのデザイナー[@shujihirai](https://twitter.com/shujihirai)は動画も作れるツワモノです。(クラブでVJとかもできる)

予選で動画を使ってウケが良かったので、本戦でも絶対に使おうと決めていました。iOSでの動画再生は難しくありませんが、初めてだと思わぬところで詰まるものです。２４時間しかない中で動画実装に時間をかけないためにも、事前に動画再生クラスを用意しときました。

```swift
 // パスからassetを生成.
let path = Bundle.main.path(forResource: "movie_parts", ofType: "mp4") ?? ""
let fileURL = URL(fileURLWithPath: path)
let avAsset = AVURLAsset(url: fileURL, options: nil)
        
let playerItem = AVPlayerItem(asset: avAsset)
videoPlayer = AVPlayer(playerItem: playerItem)
        
// 動画再生完了の監視
NotificationCenter.default.addObserver(self, selector: #selector(MovieViewController.movieEnd(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
```

動画の再生を監視して次の画面に遷移するなどの実装も先に用意しときました。

基本的な実装は以下のやつが参考になると思います。
[SwiftでVideo Player|Qiita](http://qiita.com/naoto0822/items/07da0e96aab0e3e2a64f)

また、Copy Bundle Resourcesにファイルを追加するのを忘れずに！！
https://stackoverflow.com/a/25349246

## Twitterログイン

今回、[TwitterKit](https://dev.twitter.com/twitterkit/ios/overview)を使ってTwitterログインを実装し、そこで得たアクセストークンを使って、該当のユーザーのツイート一覧を取ってきています。

Twitterログインの実装もハッカソン当日にやったわけではなく、ちょうど同時期に私が開発していた [ダイエット動画で継続できる！習慣化アプリ- Lily | App Store](https://itunes.apple.com/jp/app/id1261813918?mt=8)の開発で行なっていたTwitterログインの実装が生きています。

### ツイート取得してWatsonAPIのモデルに投げるまで

```swift
        let client = TWTRAPIClient.withCurrentUser()
        
        let dataSource = TWTRUserTimelineDataSource(screenName: screenName, apiClient: client)
        dataSource.loadPreviousTweets(beforePosition: nil, completion: { result in
            guard let datas = result.0 else {
                self.showAlert()
                return
            }
            
            var tweetStrings = ""
            
            for tweet in datas {
                let text = tweet.text
                tweetStrings += text
            }
            print(tweetStrings)
            WatosonAPI.getPersonality(data: tweetStrings, { nickName in
                
                if let nickName = nickName {
                    
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        ScreenTransitionManager.shared.goToMovie()
                        NickNameManager.shared.nickName = nickName
                    }
                } else {
                    // 失敗
                    self.showAlert()
                }
            })
        })
```

## 画像をTwitterにシェア
<img width="582" alt="スクリーンショット 2017-08-10 2.03.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/a92bbac5-e599-c0fb-dddc-e43caf55cbf6.png">

当初のアプリ設計にはシェア機能は入ってませんでしたが、ハッカソン中にどうしてシェア機能をつけたくなりました。発表の時に実際にシェアできたら盛り上がるし、#spajamのタイムラインをジャックできる未来が見えたからです😎

しかし、文字だけでシェアするのではインパクト的にしょぼい！ということでスクリーンショットをシェアすることを思いつきました。

```swift
    override func viewDidLoad() {
        super.viewDidLoad()

        nickNameLabel.text = NickNameManager.shared.nickName
        AudioPlayer.shared.playMusic(.loopmusic)
        
        // キャプチャを保存
        NickNameManager.shared.screenShot = getSnapShot()
    }
    
    func getSnapShot() -> UIImage {
        // キャプチャする範囲を取得.
        let screenWidth = UIScreen.main.bounds.width
        let rect = CGRect(x: 0, y: 0, width: screenWidth, height: screenWidth-40)
        
        // ビットマップ画像のcontextを作成.
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
        let context: CGContext = UIGraphicsGetCurrentContext()!
        
        // 対象のview内の描画をcontextに複写する.
        self.view.layer.render(in: context)
        
        // 現在のcontextのビットマップをUIImageとして取得.
        let capturedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        
        // contextを閉じる.
        UIGraphicsEndImageContext()
        
        return capturedImage
    }
```

参考： [UIViewからUIImageを取得する（スクリーンショット）|Swift Docs](https://sites.google.com/a/gclue.jp/swift-docs/ni-yinki100-ios/uikit/uiviewkarauiimagewo-qu-desuru)

コードこそググって探しましたが、そもそも**スクショを取ればシェアできる！**という発想は、会社で作っている[News Digest](https://itunes.apple.com/jp/app/id950527505?mt=8)にて、ドラッグでwebViewの遷移を作る時にスクショを取ってそれをアニメーションさせていた経験から来ています。

このように、アイデア考えるところから実装まで24時間という短い時間で行いましたが、その実装は事前にやっていたアプリ開発のコードが生きています。

だから爆速で使えるアプリを実装できたのです😎

# 主なフレームワーク

* [Alamofire](https://github.com/Alamofire/Alamofire)
* [TwitterKit](https://dev.twitter.com/twitterkit/ios/overview)
* AVFoundation
* CoreMedia


# まとめ

![s_Image uploaded from iOS.jpg](https://qiita-image-store.s3.amazonaws.com/0/111366/5c489557-56e5-9841-8548-958dfc87be97.jpeg)

実際にストアに出ているので是非、パーティーや**合コン**で使って欲しいです😎 
これからも、ちょこちょこアップデートできたらと思ってます :thumbsup: 

**では、エンジニアの皆さん、筋トレ頑張っていきましょう。**

# ダウンロードはこちらから

https://itunes.apple.com/jp/app/id1263913285?l=en&mt=8

# 関連URL

[SPAJAM 公式](http://spajam.jp/final/result/)

[SPAJAM2017で優秀賞を獲ったはなし（バックエンド）|enish engineering blog](http://engineering.enish.jp/?p=1280)

[【SPAJAM2017】激戦の本選を制したのは『ユウキセキ』を開発したチーム「市川電産」に　シリコンバレーツアーなど豪華賞品を獲得|Social Game Info](http://gamebiz.jp/?p=188601)

[SPAJAM2017で優秀賞を獲得したアプリ『嫌われAIの命名』がApp Storeに登場！　ちょっと嫌味なあだ名を付けあってコミュニケーションを楽しもう|Social Game Info](http://gamebiz.jp/?p=190898)