iPhoneのSFSpeechRecognizerとAVSpeechSynthesizerと発泡スチロールでボスっぽいなにかを作る


けものフレンズ、良いアニメでしたね。
ボスのポンコツ具合が、いい味を出していたと思います。あんなポンコツロボットが身近にいたら素敵だなと思うので、最終話終了記念に、iPhoneと発泡スチロールで作ってみようと思います。

全体像イメージ

「○○ってなんですか?」って聞くと、Wikipediaで調べた内容をボスっぽい口調で説明してくれるシステム。将来的に、体を動かしたり歩いたりさせたいので、Raspberry Piを使おうか迷いましたが、音声認識/合成のフレームワークがあって楽なので、今回はiPhoneで作りました。

  1. SFSpeechRecognizerを使って、音声認識をする
  2. 「○○ってなんですか?」みたいなパターンを検出したら、そのワードをWikipediaに調べに行く。
  3. Wikipediaに書いてあるサマリーを、ボスっぽい口調に適当に変換する。
  4. AVSpeechSynthesizerで喋らせる。

単語を調べて、ボスっぽい口調にする

音声認識/音声合成部分はインターフェイスで、機能的に根幹となる部分はここなので、まずはここから試作してみます。

Wikipediaの項目のサマリーを元に、ボスっぽい口調の文体に変換する。

サーバル(Leptailurus serval)は、食肉目ネコ科に分類される食肉類。本種のみでLeptailurus属を構成する[4]。
中型の肉食獣である。レプタイルルス属はセルヴァル種のみで構成され、セルヴァル種は18亜種(現生は17)からなる。ハイブリッド(サバンナキャット)は分類の外にある。
毛皮を取るために狩猟され、個体数は時代を追うごとに減ってきている。

自然言語処理をちゃんと行った方がそれっぽくできるんだろうけれども、試しにRubyのスクリプトを書いて機械的に無駄な文字列を取り除いて語尾を置換してみたらそれっぽくなったので、今回はこれでいいや。

require 'wikipedia'

Wikipedia.Configure {
  domain 'ja.wikipedia.org'
  path   'w/api.php'
}
page = Wikipedia.find("サーバル") # ここはパラメータで指定できるようにします

src = page.summary
src.gsub!(/(.+?)/, "").gsub!(/\[.+?\]/, "")

dst = src.split("。").map{ |line|
  if ["である"].any?{|suffix| line.end_with?(suffix)}
    line.gsub(/である$/, ["だよ", "らしいよ"].sample)
  elsif ["る", "った", "う", "い"].any?{|suffix| line.end_with?(suffix)}
    line + ["よ", "んだって", "らしいよ"].sample
  elsif ["とも"].any?{|suffix| line.end_with?(suffix)}
    line
  else
    line + "だよ"
  end
}.join("。")
puts dst

処理の流れ

  1. Wikipediaからその単語のサマリーを取ってくる(wikipedia-clientgemを使用)
  2. 括弧でくくられた注釈や補足説明を取り除く
  3. 文章を読点(。)で区切る
  4. ボスっぽい語尾(「〜らしいよ」「〜だよ」)に置き換える。語尾はランダムに選ぶ。

wikipedia-clientgemが便利だったので、Swiftに書き変えずにこのスクリプトをherokuにデプロイしてAPIとして使うことにします。

置換結果例

サーバルは、食肉目ネコ科に分類される食肉類だよ。本種のみでLeptailurus属を構成するらしいよ。中型の肉食獣だよ。レプタイルルス属はセルヴァル種のみで構成され、セルヴァル種は18亜種からなるらしいよ。ハイブリッドは分類の外にあるらしいよ。毛皮を取るために狩猟され、個体数は時代を追うごとに減ってきているんだって

AVSpeechSynthesizerを使って喋らせる

喋らせる文言の生成ができたので、今度はそれをAVSpeechSynthesizerで喋らせてみます。

import AVFoundation

...

private let synthesizer = AVSpeechSynthesizer()

func speak(_ message: String) {
    try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient)

    let utterance = AVSpeechUtterance(string: message)
    utterance.pitchMultiplier = 1.2
    utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")

    synthesizer.speak(utterance)
}

残念ながら、iPhoneのAVSpeechSynthesizerでは日本語の文字読み上げのボイスは一種類しか用意されていません。また、出力される音声に対してエフェクトをかけたりすることもできません。本当はもっと本物のボスっぽい声にしたいのですが…。声の高さはpitchMultiplierで調整ができるので、デフォルトよりも多少高めに設定してそれっぽくします。

AudioSessionのカテゴリーをセットしているのは、この後に出てくるSFSpeechRecognizerの処理との兼ね合いです。これをセットしないと、音声認識を行った後にスピーカーから声が再生されません。

実際にはエラーハンドリングや状態管理など、細かい部分でいろいろやることがありますが、サマリー取得APIと組み合わせるとこんな感じです。

Alamofire.request("https://example.com/...", parameters: ["word": word]).responseJSON { response in
    switch response.result {
    case .success(let json):
        DispatchQueue.main.async {
            if let dictionary = json as? [AnyHashable: Any], let summary = dictionary["summary"] as? String {
                self.speak(summary)
            }
        }
    case .failure(let error):
        debugPrint(error)
    }
}

SFSpeechRecognizerで質問を検知する

SFSpeechRecognizerの設定自体については、それだけで説明が長くなってしまうのでここでは割愛します
Developers.IOさんの記事がわかりやすいので、それを呼んでください

で、認識された文字列(bestTranscription)をチェックして、質問っぽい語尾が出てきたらそこで認識の処理を止めるようにします。本当はここも自然言語処理で判定するべきところですが、最初のプロトタイプなので単純なパターンマッチングで済ましてしまいます。

「〜ってなんですか」「〜ってなに」みたいなパターンが現れたら、その前の文字列を「ワード」として抽出し、認識を一旦ストップさせます。

private func shouldStopMonitoring(transcription: String) -> Bool {
    for suffix in ["ってなんですか", "てなんですか", "って何", "ってなに", "わかりますか", "なに", "なんですか"] {
        if transcription.hasSuffix(suffix) {
            word = transcription.replacingOccurrences(of: suffix, with: "")
            debugPrint(word, "について調べるよー")
            return true
        }
    }
    return false
}

抽出したワードを先ほどのAPIにパラメータとして渡して、返り値を音声合成で喋らせれば、基本的な仕組みの完成です。

なお、実際にアプリとして動かす上では、「音声認識中」「検索中」「発声中」の状態管理を行って、SFSpeechRecognizerの状態を変化させています。が、ここも細かい話になる割に面白くないので、省略します。

本体をつくる

発泡スチロールで本体を作ります。

切ります

塗ります

できた

完成

実際に動かしている状態はこちら

Wikipediaにない単語を質問したり、通信エラーが発声した時は、白目にして「あわわわ」させるようにしたら可愛い感じになりました。大事ですね。

今後

本当は、フロントカメラからの映像で物体検知を行なって、その物体に関する説明を喋ってくれる機能を作りたかったのですが、物体検知で期待するようなワードを返すようにチューニングするのが難しく、一旦諦めました。いずれ挑戦してみます。

加えて、どうしても動きがないとさみしいので、なんとかして耳とか動かせるようにしたいです。

最後に

良いアニメをありがとうございました

けものフレンズ

追記: 2017/04/18

続き書きました

Google Cloud Vision APIとMicrosoft Translator APIを使って、ラッキービーストに物体認識させるようにしたよ

追記: 2017/05/03

ソースコードをGitHubで公開しました