その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

【iOS 10】Speechフレームワークで音声認識 - 対応言語リスト付き

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


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



今までも色々と音声認識を実現する手段はありましたが、やはりApple純正となると一気に本命になってきます。*1


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


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

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

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


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


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


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

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

SFSpeechRecognizerの準備

初期化して、

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


デリゲートをセット

speechRecognizer.delegate = self
音声認識の利用許可を求める
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` で実行しているようです。

認識リクエストの準備

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

private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?    


認識開始前に初期化。

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


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

recognitionRequest.shouldReportPartialResults = true

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

認識タスクの登録

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

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 については下記記事にも書いたのでよろしければご参照ください。(残念ながら録音については書いてないのですが。。)


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

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

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

対応言語は58言語!

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


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

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


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

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


出力結果:

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

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

長文の認識精度

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

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


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

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


なんと、間違った箇所は「そもそも」 → 「様々」(さまさま?)だけ


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

パフォーマンス

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

SFSpeechRecognitionResult

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

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

録音済みファイルの認識

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


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

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

(追記)WWDCのセッションで言ってたこと

  • Siri および 音声入力機能と下回りは同じものを使用

Same speech technology as Siri and Dictation

  • 自動でユーザーに適応する
  • 基本的には要インターネット
    • 例外あり(some some languages and device models)

後で調べる

  • シミュレータでも実行できるのか?


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