概要
先日、Spajam というハッカソンに参加しまして、私の参加したチーム「Rising Sun」は東京B予選を最優秀賞で突破、そして箱根温泉で行われた本戦にて優秀賞を獲得しました!初めてのハッカソンで全国2位という結果が取れたのはとても自信になりました。
今記事では箱根本戦で優秀賞を獲得したアプリ「嫌われAIの命名」の設計や実装について紹介します。
ちなみにですが、このアプリ、**8/7(月)**にリリースしております😎
コンセプト
発想プロセス
Spajam2017の箱根本戦のテーマは「勇気」でした。
最終的には、「知り合いから友達にステップアップするためには勇気がいるよね? その勇気を後押しするためのツールをつくったよ!」って感じです。
友達と仲良くなるに当たって、呼び方というのは意外に重要だったりします。あだ名をつけるとよそよそしさが無くなって一気に仲良くなれるという経験をしたことがある方もいるのではないでしょうか?
(しかし、ぶっちゃけ言うと、このコンセプト設計のロジックは後付けだった部分もあります笑)
本当はこのように考えました。
私たちのチームは東京B予選でもラップアプリを作っていますが、ここでの勝因の一つに「世の中のトレンドを掴んでいる」というものがあります。今日本ではラップブームが起こっていますよね。(ちなみに私はラッパーではなくダンサーです)
今回の「勇気」というテーマに対しては、トレンディなキーワードとして「嫌われる勇気」を思いつきました。これに様々な要素を被せていきます。
したがって、アプリのコンセプト設計のプロセスとしては、
「嫌われる勇気」
-> 「嫌われる勇気」を後押しするアプリ
-> ニックネームを代わりにつけてくれるアプリ
- (流行りの)AI
- (流行りの)嫌われマツコ
=> 「嫌われAIの命名」
こんな感じです。
アプリ設計
あだ名を作る材料 -> AIがあだ名を作る -> 音声が出てくる
このようなプロセスを経ます。それぞれ以下のような仕様を考えました。
あだ名をつくる材料はツイート
TwitterKitを使って、あだ名をつけたい人の直近500文字のツイートを集めます。これがあだ名をつけるための材料。
TwitterKitのAPIを使って取得する様子
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が性格を分析する
IBM WatsonのPersonality Insights のAPIを使って、文字列(収集したツイート)から5つの性格要素をパーセンテージで表してもらいます。
以下のjsonからpersonality
を取り出し、Openness
(開放性),Conscientiousness
(誠実性),Extraversion
(外向性),Agreeableness
(合理性),Emotional range
(感情幅)のpercentile
を使います。
レスポンス例
"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 にも書いてます。
性格からあだ名を決定する
ここで機械学習が登場すればいいんですが、今回は筋肉エンジニアという人間が単語の候補を考えていますw
取得したpercentile
をもとに、自分たちで定義した嫌われラインすれすれの単語をマッピングします。
例:開放性からのスコアリングと単語のマップ
まず、開放性のパーセンテージをlow, middle, higheの三段階に分けます。
switch inOpePer {
case 0 ..< 1/3:
score = .low
case 1/3 ..< 2/3:
score = .middle
case 2/3 ... 1:
score = .high
default:
break
}
次に3つのstring配列をもつ2重配列を用意しておき、lowは0番めから、middleは1番めから、highは2番めの配列から単語を選ぶことを決定します。
let nounList =
[
["泥棒", "ナルシスト", "深海魚", "妖怪", "キャバ嬢", "変質者", "大学生"],
["ポンコツ", "ゴリラ", "モンスター", "サーファー", "おかま", "一発屋", "エンジニア"],
["おじさん", "ホームレス", "ホスト", "ヤンキー", "原始人", "デブ専", "サラリーマン"]
]
あとはその配列の中からランダムで使う単語を決定します。
この作業を5つのパーソナリティに対して行い、5つの単語を決定します。
決定した5つの単語を組み合わせたり、あえて1単語だけにしたりして、最終的なあだ名を決定します。
https://twitter.com/keiyonekawa/status/894777686630977538 (僕の友達のあだ名)
出来たあだ名をAI Talkに喋らす
そして出来たあだ名をSpajamで提供されていたAI Talkを使ってあんずちゃんに喋ってもらいます。
AI TalkのAPIは東京B予選でも使ったので、その時のコードを流用することができました。
AI Talkは、文字列を投げると、音声ファイルを返してくれます。普通は有料だと思いますが、ハッカソンで提供されていたので使い倒しました。(ストアにリリースするときは代わりにiOSのAVSpeechSynthesizerに喋らせてます)
大まかなアプリの仕組みは以上です。
その他の技術
動画再生
私たちのチームのデザイナー@shujihiraiは動画も作れるツワモノです。(クラブでVJとかもできる)
予選で動画を使ってウケが良かったので、本戦でも絶対に使おうと決めていました。iOSでの動画再生は難しくありませんが、初めてだと思わぬところで詰まるものです。24時間しかない中で動画実装に時間をかけないためにも、事前に動画再生クラスを用意しときました。
// パスから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
また、Copy Bundle Resourcesにファイルを追加するのを忘れずに!!
https://stackoverflow.com/a/25349246
Twitterログイン
今回、TwitterKitを使ってTwitterログインを実装し、そこで得たアクセストークンを使って、該当のユーザーのツイート一覧を取ってきています。
Twitterログインの実装もハッカソン当日にやったわけではなく、ちょうど同時期に私が開発していた ダイエット動画で継続できる!習慣化アプリ- Lily | App Storeの開発で行なっていたTwitterログインの実装が生きています。
ツイート取得してWatsonAPIのモデルに投げるまで
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にシェア
当初のアプリ設計にはシェア機能は入ってませんでしたが、ハッカソン中にどうしてシェア機能をつけたくなりました。発表の時に実際にシェアできたら盛り上がるし、#spajamのタイムラインをジャックできる未来が見えたからです😎
しかし、文字だけでシェアするのではインパクト的にしょぼい!ということでスクリーンショットをシェアすることを思いつきました。
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
コードこそググって探しましたが、そもそも**スクショを取ればシェアできる!**という発想は、会社で作っているNews Digestにて、ドラッグでwebViewの遷移を作る時にスクショを取ってそれをアニメーションさせていた経験から来ています。
このように、アイデア考えるところから実装まで24時間という短い時間で行いましたが、その実装は事前にやっていたアプリ開発のコードが生きています。
だから爆速で使えるアプリを実装できたのです😎
主なフレームワーク
- Alamofire
- TwitterKit
- AVFoundation
- CoreMedia
まとめ
実際にストアに出ているので是非、パーティーや合コンで使って欲しいです😎
これからも、ちょこちょこアップデートできたらと思ってます
では、エンジニアの皆さん、筋トレ頑張っていきましょう。
ダウンロードはこちらから
関連URL
SPAJAM2017で優秀賞を獲ったはなし(バックエンド)|enish engineering blog
【SPAJAM2017】激戦の本選を制したのは『ユウキセキ』を開発したチーム「市川電産」に シリコンバレーツアーなど豪華賞品を獲得|Social Game Info
SPAJAM2017で優秀賞を獲得したアプリ『嫌われAIの命名』がApp Storeに登場! ちょっと嫌味なあだ名を付けあってコミュニケーションを楽しもう|Social Game Info