[iOS][macOS] SFSpeechRecognizer の使い方

     
音声認識のためのフレームワーク Speech の使い方を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.4 beta4
  • Xcode 13.3.1
  • iOS 15.4

Speech

Apple プラットフォームであれば、音声認識向けのフレームワーク Speech が用意されています。

Apple のドキュメントは、こちら

特に利用料も発生せず、音声データから、テキストデータに変換することができます。

実際に使用する class は、SFSpeechRecognizer というそのままの名前のクラスです。
Apple のドキュメントは、こちら

いろいろと準備が必要なので、1つづつ 進めていきます。

実装

実際に音声認識を使用するためには、以下のような手順が必要となります。
・使用許諾
・準備(マイク設定、音声認識設定)
・音声認識と結果の処理
・終了処理

使用許諾

音声データを音声認識に使用するためには、写真へのアクセスや通知の表示と同じようにユーザーから許諾を得る必要があります。

なお、録音データではなく、マイクを使って音声認識する場合は、マイクを使用することに対してのユーザ許諾も必要です。

auth_recognize
auth_mic

Info.plist

音声認識を使用するための理由を Info.plist に記述する必要があります。

記述するキーは、NSSpeechRecognitionUsageDescription/"Privacy - Speech Recognition Usage Description" です。

マイクを使うのであれば、マイク使用に関する記述も必要です。
記述するキーは、NSMicrophoneUsageDescription / "Privacy - Microphone Usage Description" です。

いずれの記述も不足しているままアクセスしようとすると例外が発生します。

ユーザーからの許諾

Info.plist に記述するだけでは足りません。Info.plist の記述はアプリが使用することを宣言しているだけです。宣言に加え、別途 ユーザーに許諾を取る必要があります。

許諾を得るためのダイアログ表示

ユーザーから許諾を取るためにダイアログを表示するためのメソッドとして requestAuthorization が用意されています。

Apple のドキュメントは、こちら

このメソッドは、呼び出すたびにダイアログを表示するわけではありません。
一度ユーザーが許諾について指示を行った後では、メソッドを呼び出しても、以前に 選択された結果を返すだけです。
なお、一度許諾を選択した後でも、ユーザーは 設定アプリから許諾を変更することができるので、都度の確認が必要となります。

ユーザーからの許諾状態の確認

ユーザーからの許諾の状態を確認するメソッド authorizationStatus も用意されています。
Apple のドキュメントは、こちら

許諾状態

ユーザーからの許諾の状態を表す 型(enum) として、 SFSpeechRecognizerAuthorizationStatus が用意されています。

requestAuthorization も authorizationStatus の許諾状態としてこの型で 情報を返してきます。

Apple のドキュメントは、こちら

取りえる値は以下の通りです。
・notDetermined / 未選択状態
・denied / 非許諾
・restriected / 制限付き(どのような状態か説明はありません。)
・authorized / 許諾済み

取得した状態が authorized であれば、音声認識に同意していると考えられます。

準備

実際に、マイクからの音声に対して 音声認識を行う前に 以下のような準備作業が必要となります。

・マイク設定
・音声認識設定

マイク設定/AVAudioSession

音声認識するためには、マイクから音声を入力する必要があります。マイクを制御するのは、AVAudioSession です。AVAudioSession はマイクだけではなく、オーディオ全般を管理していますので、音声認識向けに正しく設定する必要があります。

Apple のドキュメントは、こちら

Apple の例では、以下の設定値で初期化しています。

category: .record
mode: .measurement
.options: .duckOthers

category に .record を選択することで、実行時に、再生中のものを無音にします。
mode に .measurement を指定することで、入出力を使用して なんらかの測定するモードになります。
options に .duckOthers を選択することで、別アプリからの再生音量を小さします。

MEMO
さまざまなオプションが用意されています。詳細は、上記の Apple ドキュメントを参照してください。

初期設定後、アクティブにします。options に .notifyOtherOnDeactivation を指定することで、自アプリが AudioSession の使用をやめたときに別アプリが元のモードに戻れるようにシステム側から通知が行われます。


    let audioSession = AVAudioSession.sharedInstance()
    try self.audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
    try self.audioSession.setActive(true, options: .notifyOthersOnDeactivation)

マイク設定/AVAudioEngine

Audio デバイスの管理をしている AVAudioEngine を使って、デバイス情報を取得します。

Apple のドキュメントは、こちら

AVAudioEngine には、複雑な初期設定はありません。singleton なので、インスタンス化して、ノードを取得するだけです。


    let audioEngine = AVAudioEngine()
    let inputNode = audioEngine.inputNode

音声認識設定/SFSpeechRecognizer

音声認識は、SFSpeechRecognizer へ対して、SFSpeechRecognitionRequest を使って行われます。
Apple のドキュメントは、こちら

SFSpeechRecognitionRequest は抽象クラスです。実際には、以下のクラスのいずれかを使用します。

録音された音声を音声認識に使用する場合は、SFSpeechURLRecognitionRequest を使います。
マイクを使って音声認識を行う場合は、SFSpeechURLRecognitionRequest を使います。

今回は、マイクからの音声入力を使いますので、後者を使って Request を作成することになります。
設定としては、区切れ目ごとに結果を欲しいかを設定する shouldReportPartialResults と
サーバーに送らずに音声認識するかを設定する requiresOnDeviceRecognition があります。

最後に、(インスタンス化した) SFSpeechRecognizer に対して、リクエストを登録します。
その際に、認識結果を処理するための resultHandler も設定します。(処理内容は後ほど)


recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
recognitionRequest.shouldReportPartialResults = true
recognitionRequest.requiresOnDeviceRecognition = true

// speechRecognizer is an instance of SFSpeechRecognizer
recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest,
                                                   resultHandler: { (result, error) in
    // recognized result will come
})

音声認識処理

先の recognitionTask で登録した resultHandler に渡される (result, error) が 音声認識した結果(とエラー)です。

result は、SFSpeechRecognitionResult という型です。
Apple のドキュメントは、こちら

result には、3つのデータが含まれています。

bestTranscription が(音声認識エンジンが)ベストと考える結果
transciptions が、信頼度順になっている 音声認識結果
speechRecognitionMetadata が、音声認識で得られた音声のメタデータ(時間当たりの発語数等)です。

大抵は、bestTranscription が使いたいデータでしょう。

上記以外に、処理についての情報として、
isFinal という SFSpeechREcognizer が処理を終えたかどうかのフラグを返すようになっています。

MEMO
isFinal は、渡したバッファの recognition が終わったかどうかを表すものではありません。
この isFinal は、recognitionTask が動いている限り、true になりません。

task を cancel もしくは finish をすることで、isFinal が true にセットされた形で、resultHandler が呼び出されます。

音声認識

AudioSession の使用開始を宣言

まずは、マイク使用開始の準備をします。
そして、inputNode から流れてくるデータを 音声認識の Request に設定するようにします。


        try! self.audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        self.inputNode = self.audioEngine.inputNode

        let recordingFormat = inputNode?.outputFormat(forBus: 0)
        inputNode?.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
            self.recognitionRequest?.append(buffer)
        }

AudioEngine 動作開始

準備ができたので、マイクの使用を開始します。


        audioEngine.prepare()
        try audioEngine.start()

音声認識の結果処理

マイクの使用開始を行うと、音声入力が始まり、認識できると、先ほどの resultHandler が呼び出されます。

渡された result は、音声がテキスト化されたデータ(String)ですので、その文字を使用して処理していきます。

サンプルアプリでは、Text に設定しているだけです。

サンプルアプリ

仕様概略
・ボタンを押すと、聞き取り開始して、認識したテキストを表示する
・ボタンを再度押すと、聞き取り終了

AppImage
MEMO
認識したテキストは、Text に表示しますが、保存はしません。
アーキテクチャメモ
enum を使って、App の状態を管理すると、一部の状態の時のみ使う class 内のプロパティをなくすことができます。

今回の例では、認識中のみ使用する AVAudioInputNode, SFSpeechRecognitionTask を App状態を管理する enum で持つようにしています。
もっと複雑なアプリでは、View の状態も enum で管理すると便利です。

ContentView.swift

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2022/05/11
//  © 2022  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    var body: some View {
        VStack {
            Text(viewModel.state.titleString).font(.title)
            Text(viewModel.text)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
            Button(action: {
                if viewModel.state.isRecording {
                    viewModel.stop()
                } else {
                    do {
                        try viewModel.record()
                    } catch {
                        print(error.localizedDescription)
                    }
                }
            }, label: {
                Image(systemName: viewModel.state.isRecording ? "pause.circle" : "record.circle")
                    .resizable().scaledToFit()
                    .frame(width: 50, height: 50)
            })
        }
        .padding()
        .onAppear {
            viewModel.requestAuthorization()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ViewModel.swift

//
//  ViewModel.swift
//
//  Created by : Tomoaki Yagishita on 2022/05/11
//  © 2022  SmallDeskSoftware
//

import Foundation
import Speech

enum AppState: Equatable {
    case notRecording
    case recording(AVAudioInputNode, SFSpeechRecognitionTask)
    
    var isRecording: Bool {
        return self != .notRecording
    }
    
    var titleString: String {
        switch self {
        case .notRecording:
            return "Voice Memo"
        case .recording(_,_):
            return "Recording/Recognizing"
        }
    }
}

@MainActor
class ViewModel: ObservableObject {
    @Published var state: AppState = .notRecording
    @Published var text: String = ""

    let audioSession = AVAudioSession.sharedInstance()
    let audioEngine = AVAudioEngine()
    private var speechRecognizer: SFSpeechRecognizer {
        let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))
        guard let recognizer = recognizer else { fatalError("failed to prepare recognizer")}
        recognizer.supportsOnDeviceRecognition = false
        return recognizer
    }

    func requestAuthorization() {
        SFSpeechRecognizer.requestAuthorization {_ in }
    }
    
    func stop() {
        guard SFSpeechRecognizer.authorizationStatus() == .authorized else { return }
        
        if case .recording(let node,let task) = self.state {
            self.audioEngine.stop()
            node.removeTap(onBus: 0)
            task.finish()
            self.state = .notRecording
        }
    }
    
    func record() throws {
        guard SFSpeechRecognizer.authorizationStatus() == .authorized else { return }

        let recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        recognitionRequest.shouldReportPartialResults = true
        recognitionRequest.requiresOnDeviceRecognition = false
        let recognitionTask = self.speechRecognizer.recognitionTask(with: recognitionRequest,
                                                            resultHandler: { (result, error) in
            if let result = result {
                self.text = result.bestTranscription.formattedString
            }
        })

        try self.audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
        try! self.audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        let inputNode = self.audioEngine.inputNode

        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, when) in
            recognitionRequest.append(buffer)
        }
        
        audioEngine.prepare()
        try audioEngine.start()
        
        self.state = .recording(inputNode, recognitionTask)
    }
}

まとめ

音声認識フレームワーク Speech の使い方を説明しました。

音声認識フレームワーク Speech の使い方
  • 使用するには、使用許諾をユーザーから得る必要がある
  • SFSpeechRecognizer で音声認識を行う
  • マイクを使って音声を取得するならば別途 使用許諾が必要

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。