[iOS 10] SFSpeechRecognizerで音声認識を試してみた

2016.08.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

おばんです、休日は友人の誕生日祝いということでご飯を食べに行きましたが、ふらふら飲み歩いてる経験を活かして店選びをうまくやれて、楽しく過ごすことができました田中です。(よかった(小並感)

さて今回はiOS 10より搭載されたSFSpeechRecognizerによる音声認識をとりあげて書いていきます。

本記事は Apple からベータ版として公開されているドキュメントを情報源としています。 そのため、正式版と異なる情報になる可能性があります。ご留意の上、お読みください。



音声認識って前からできなかったっけ?

フリーのライブラリや外部APIを通して実装することはできました。

大昔に自分も似たようなものを実装していたという記録がありました。(OpenEarsを使ってみた - 情弱spartanの開発日記)

iOS 10からの音声認識概要

公式には以下のWWDCのビデオで説明されています。
Speech Recognition API

ビデオの内容を簡単に要約すると

iOSの音声認識のこれまで

  • iPhone 4S時代からキーボードの中に音声認識あった
  • iOS5からあった
  • 使うのにKeyboardを必要とした
  • リアルタイム音声のインプットのみ受付
  • 言語のカスタマイズが出来なかった

iOS 10からの音声認識

  • Speech Recognition API
  • Siriに使われているのと同じものが使える
  • リアルタイムのみならず、録音済みの音声データも音声認識可能に

注意点

リソースに関して
  • 端末ごとの音声認識回数には制限がある
  • アプリごとにも認識回数に制限がある
  • バッテリーとネットワークに関してハイコスト
  • 一回のディクテーション時間のMaxは1分
プライバシーとユーザビリティに関して
  • ユーザーが録音中だとわかるUIを作ろう
  • パスワード、ヘルス情報、経済などの機密情報は音声認識させない
  • エラーをユーザーに伝える

実装方法

公式が出している以下のサンプルコードの内容を写経しつつ、解析をしていきます。
SpeakToMe: Using Speech Recognition with AVAudioEngine

Info.plistの編集

iOS 10からはプライバシーアクセスに関するものはInfo.plistにその用途を記述しないと強制終了します。
[iOS 10] 各種ユーザーデータへアクセスする目的を記述することが必須になるようです

今回必要になるのはNSMicrophoneUsageDescription(マイクアクセス)NSSpeechRecognitionUsageDescription(音声認識アクセス)。 以下のように記述しましょう。

<dict>
    <key>NSMicrophoneUsageDescription</key>
    <string>マイクの用途について記載します。</string>
    <key>NSSpeechRecognitionUsageDescription</key>
    <string>音声認識の用途について記載します。</string>
    .........
    ......
    ...
</dict>

importする

import Speech

用意するプロパティとdelegateの設定

class ViewController: UIViewController {
    
    // "ja-JP"を指定すると日本語になります。
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private let audioEngine = AVAudioEngine()
    
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var button: UIButton!
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        
        speechRecognizer.delegate = self
        
        button.isEnabled = false
    }
}

extension ViewController: SFSpeechRecognizerDelegate {
    // 音声認識の可否が変更したときに呼ばれるdelegate
    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
        if available {
            button.isEnabled = true
            button.setTitle("音声認識スタート", for: [])
        } else {
            button.isEnabled = false
            button.setTitle("音声認識ストップ", for: .disabled)
        }
    }
}

言語設定の対応リストは58種類のようです。以下を参考にさせていただきました。
【iOS 10】Speechフレームワークで音声認識 - 対応言語リスト付き - Over&Out その後

認証

本家のサンプルはviewDidAppearに直接書いていましたが、とりあえず分離させました。

public override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    requestRecognizerAuthorization()
}

private func requestRecognizerAuthorization() {
    // 認証処理
    SFSpeechRecognizer.requestAuthorization { authStatus in
        // メインスレッドで処理したい内容のため、OperationQueue.main.addOperationを使う
        OperationQueue.main.addOperation { [weak self] in
            guard let `self` = self else { return }
            
            switch authStatus {
            case .authorized:
                self.button.isEnabled = true
                
            case .denied:
                self.button.isEnabled = false
                self.button.setTitle("音声認識へのアクセスが拒否されています。", for: .disabled)
                
            case .restricted:
                self.button.isEnabled = false
                self.button.setTitle("この端末で音声認識はできません。", for: .disabled)
                
            case .notDetermined:
                self.button.isEnabled = false
                self.button.setTitle("音声認識はまだ許可されていません。", for: .disabled)
            }
        }
    }
}

音声認識の処理

private func startRecording() throws {
    refreshTask()
    
    let audioSession = AVAudioSession.sharedInstance()
    // 録音用のカテゴリをセット
    try audioSession.setCategory(AVAudioSessionCategoryRecord)
    try audioSession.setMode(AVAudioSessionModeMeasurement)
    try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
    
    recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
    
    guard let inputNode = audioEngine.inputNode else { fatalError("Audio engine has no input node") }
    guard let recognitionRequest = recognitionRequest else { fatalError("Unable to created a SFSpeechAudioBufferRecognitionRequest object") }
    
    // 録音が完了する前のリクエストを作るかどうかのフラグ。
    // trueだと現在-1回目のリクエスト結果が返ってくる模様。falseだとボタンをオフにしたときに音声認識の結果が返ってくる設定。
    recognitionRequest.shouldReportPartialResults = true
    
    recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in
        guard let `self` = self else { return }
        
        var isFinal = false
        
        if let result = result {
            self.label.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.button.isEnabled = true
            self.button.setTitle("音声認識スタート", for: [])
        }
    }
    
    // マイクから取得した音声バッファをリクエストに渡す
    let recordingFormat = inputNode.outputFormat(forBus: 0)
    inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
        self.recognitionRequest?.append(buffer)
    }
    
    try startAudioEngine()
}

private func refreshTask() {
    if let recognitionTask = recognitionTask {
        recognitionTask.cancel()
        self.recognitionTask = nil
    }
}

private func startAudioEngine() throws {
    // startの前にリソースを確保しておく。
    audioEngine.prepare()
    
    try audioEngine.start()
    
    label.text = "どうぞ喋ってください。"
}

音声認識の開始と終了

@IBAction func tappedStartButton(_ sender: AnyObject) {
    if audioEngine.isRunning {
        audioEngine.stop()
        recognitionRequest?.endAudio()
        button.isEnabled = false
        button.setTitle("停止中", for: .disabled)
    } else {
        try! startRecording()
        button.setTitle("音声認識を中止", for: [])
    }
}

ここまでで音声認識は実行可能となります。

まとめ

オフィスでデバッグしてたんですが、なかなか恥ずかしいですね。
急に「こんにちは、あ、あ、あーーー」とか喋り出すと「暑さでやられてしまったのか」と心配されました。

個人的にはこれを待っていたんだという感じです!
日本語の精度も高く、どうやらユーザーに適応していってくれる機能が含まれているようなので、使えば使うほど良いのでしょうか。
ただ処理結果が、

僕「マイクのテスト中です」
iOS「米です」

とか言われたときはどうしてやろうかと思いました。
お米食べろ!!!!!

この辺の技術と組み合わせると、品詞分解してくれるところまでだけでもちょっと楽しいんじゃないかなとか思ってます。今度実験的に試したい。
Swift での自然言語処理 - Realm

参考