iOSのSpeechフレームワークで音声認識 - 対応言語は58種類！

iOS 10のドキュメントが公開された当日に書いた下記記事で、最も反響が大きかったのが音声認識APIでした。

- [API Diffsから見る iOS 10 の新機能 #WWDC2016 - Qiita](http://qiita.com/shu223/items/9ac3261cdbf2a35468ed)

今回公開された SiriKit（Intents / IntentsUI）とは別のフレームワーク、**Speech Framework** として公開されたものです。<b>リアルタイム音声にも、録音済み音声にも使える</b>ようです。

![icon.jpg](https://qiita-image-store.s3.amazonaws.com/0/3180/d0dd5dd1-e095-f9d4-34f3-489def28c7d1.jpeg)

今までも色々と[音声認識を実現する手段](http://qiita.com/shu223/items/eda02dc7d334c339ff64)はありましたが、やはり<b>Apple純正</b>のフレームワークとなると一気に本命になってきます。[^1]

[^1]: 古い話かつ余談ですが、音声合成も以前色々と比較してみて結局 AVSpeechSynthesizer が良い、という結論に落ち着いたことがありました：[iOSで使える日本語OKな音声読み上げエンジン8種（TTS,音声合成） - Qiita](http://qiita.com/shu223/items/223492e4f061032e652e)


というわけで本記事では Speech フレームワークを色々いじってみて、<b>何ができるのか</b>とか、<b>どうやるのか</b>とか見てみたいと思います。


なお、NDA期間中につき、スクショは自粛します。

- [Apple Developer Agreement](https://developer.apple.com/programs/terms/apple_developer_agreement.pdf)


##まずはサンプルを動かしてみる

"SpeakToMe: Using Speech Recognition with AVAudioEngine" というサンプルコードが公開されているのでまずは試してみます。


アプリが起動してすぐ、<b>Speech Recognition の利用許可を求めるダイアログ</b> が出てきます。そんなパーミッションも追加されたんだ、と思って [Settings] > [Privacy] を見てみると、確かに [Speech Recognition] の欄が追加されています。


録音を開始するボタンを押すだけで認識スタンバイOKです。試しに Hello と話しかけてみると・・・**"How do"** という結果が・・・気を取り直してもう一度 Hello と言ってみると・・・**"I don't"**・・・


これは自分の英語力の問題なので、諦めてターミナルから `say` コマンドで Hello と言わせると "Hello" と認識してくれました。同時に過去の "I don't" とかも "Hello" に修正されたので、<b>その後の入力からの認識結果によって過去の認識結果も修正する</b>っぽいです。


##サンプルの実装を見てみる

###SFSpeechRecognizerの準備

初期化して、

```swift
private let speechRecognizer = SFSpeechRecognizer(locale: Locale(localeIdentifier: "en-US"))!
```

デリゲートをセット

```swift
speechRecognizer.delegate = self
```

###音声認識の利用許可を求める

```swift
SFSpeechRecognizer.requestAuthorization { authStatus in
    OperationQueue.main().addOperation {
        switch authStatus {
            case .authorized:
                // 許可された

            case .denied:
                // 拒否された

            case .restricted:
                self.recordButton.isEnabled = false
                self.recordButton.setTitle("Speech recognition restricted on this device", for: .disabled)

            case .notDetermined:
                self.recordButton.isEnabled = false
                self.recordButton.setTitle("Speech recognition not yet authorized", for: .disabled)
        }
    }
}
```

コールバックはメインスレッドで呼ばれているとは限らないので `OperationQueue.main().addOperation` で実行しているようです。

###認識リクエストの準備

プロパティを定義しておいて、

```swift
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?    
```

認識開始前に初期化。

```swift
recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
guard let recognitionRequest = recognitionRequest else { fatalError("Unable to created a SFSpeechAudioBufferRecognitionRequest object") }
```

このプロパティをセットすると、<b>録音が終わる前の "partial (non-final)" な結果を報告してくれる</b>、とのことです。

```swift
recognitionRequest.shouldReportPartialResults = true
```

つまり、確定前の結果を取得できるようで、先ほどの、過去に戻って認識結果が修正される挙動はこのプロパティによるものではないかと思われます。<b>リアルタイム認識時には有効にしておきたい</b>重要プロパティですが、**デフォルトでは `false`** とのことです。

###認識タスクの登録

SFSpeechRecognizer の `recognitionTask:with:resultHandler:` メソッドに SFSpeechRecognitionRequest オブジェクトと、結果を受け取ったときの処理を渡します。

```swift
recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in
    var isFinal = false
    
    if let result = result {
        self.textView.text = result.bestTranscription.formattedString
        isFinal = result.isFinal
    }
    
    if error != nil || isFinal {
        self.audioEngine.stop()
        inputNode.removeTap(onBus: 0)
        
        self.recognitionRequest = nil
        self.recognitionTask = nil
        
        self.recordButton.isEnabled = true
        self.recordButton.setTitle("Start Recording", for: [])
    }
}
```

戻り値として、 SFSpeechRecognitionTask オブジェクトが返ってきます。このサンプルでは、認識結果は SFSpeechRecognitionResult オブジェクトの `bestTranscription` プロパティより得ています。


###録音開始（認識開始）

ここは AVAudioEngine の機能であって、iOS 8 からあるものなので、省略します。AVAudioEngine については下記記事にも書いたのでよろしければご参照ください。（残念ながら録音については書いてないのですが。。）

- [iOS 8 の新機能のサンプルコード集『iOS8-Sampler』 - Qiita](http://qiita.com/shu223/items/5c5df2ad9484c7abdb30)
- [Audio Unit Extensions ～オーディオエフェクトのアプリ間共有～](http://d.hatena.ne.jp/shu223/20150624/1435098847)

唯一 Speech フレームワークと直接関係するポイントは以下。

```swift
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
    self.recognitionRequest?.append(buffer)
}
```

これで、<b>マイクから得られる音声バッファが SFSpeechRecognitionRequest オブジェクトに渡される</b>ようになり、録音開始と共に認識が開始されることになります。


##対応言語は58言語！

基本的な使い方がわかったところで、以下ではサンプルに触れられてない部分も色々と見てみます。まずは、対応言語から。


SFSpeechRecognize は初期化時に以下のように Locale を指定できるようになっています。

```swift
private let speechRecognizer = SFSpeechRecognizer(locale: Locale(localeIdentifier: "en-US"))!
```

`supportedLocales()` というメソッドがあるので、全部出力してみます。

```swift
SFSpeechRecognizer.supportedLocales().enumerated().forEach {
    print($0.element.localeIdentifier)
}
```

出力結果：

```swift
nl-NL
es-MX
zh-TW
fr-FR
it-IT
vi-VN
en-ZA
ca-ES
es-CL
ko-KR
ro-RO
fr-CH
en-PH
en-CA
en-SG
en-IN
en-NZ
it-CH
fr-CA
da-DK
de-AT
pt-BR
yue-CN
zh-CN
sv-SE
es-ES
ar-SA
hu-HU
fr-BE
en-GB
ja-JP
zh-HK
fi-FI
tr-TR
nb-NO
en-ID
en-SA
pl-PL
id-ID
ms-MY
el-GR
cs-CZ
hr-HR
en-AE
he-IL
ru-RU
de-CH
en-AU
de-DE
nl-BE
th-TH
pt-PT
sk-SK
en-US
en-IE
es-CO
uk-UA
es-US
```

なんと、<b>58言語</b>もあります。もちろん<b>日本語 "ja-JP"</b>もサポート。


##長文の認識精度

英語で長文を入れてみてもそもそも自分の発音が、というところがあるので、日本語で試してみます。

```swift
private let speechRecognizer = SFSpeechRecognizer(locale: Locale(localeIdentifier: "ja-JP"))!
```

上記の一文をしゃべって入力してみました。今現在フードコートのテーブルでこれを書いてるので、周りはそこそこ騒がしいです。一発勝負で結果は以下でした。

> 英語で長文を入れてみても様々自分の発音がどういうところがあるので日本語で試してみます

なんと、**間違った箇所は「そもそも」 → 「様々」（さまさま？）だけ**！

なかなか優秀なんじゃないでしょうか。


##パフォーマンス

ちゃんと測ってないですが、体感としてはiOSに従来からある音声入力と同じぐらいです。リアルタイムからほんのちょっと遅れて結果が確定していく感じです。


##SFSpeechRecognitionResult

認識結果として SFSpeechRecognitionResult オブジェクトが得られるわけですが、公式サンプルで用いられている `bestTranscription` は<b>最も信頼度(confidence)の高い認識結果</b>で、その他の候補も `transcriptions` プロパティより取得することができます。

```swift
@property (nonatomic, readonly, copy) SFTranscription *bestTranscription;
@property (nonatomic, readonly, copy) NSArray<SFTranscription *> *transcriptions;
```

##録音済みファイルの認識

SFSpeechAudioBufferRecognitionRequest の代わりに、<b>SFSpeechURLRecognitionRequest クラスを利用</b>します。どちらも SFSpeechRecognitionRequest のサブクラスです。


実装して試してはないのですが、バッファの代わりに<b>ファイルのURL（ローカル／オンライン）を指定</b>するだけで、あとは上述の方法と同じかと思われます。

```swift
public init(url URL: URL)    
public var url: URL { get }
```


##WWDC 2016のセッションで言ってたこと

- <b>Siri および 音声入力機能と下回りは同じ</b>ものを使用

> Same speech technology as Siri and Dictation

- 自動で<b>ユーザーに適応</b>する
- 基本的には<b>要インターネット</b>
 - 例外あり（some some languages and device models）


##後で調べる

- シミュレータでも実行できるのか？