[iOS]リアルタイム音声認識(Speech to Text)を実装してみる

# Speechフレームワーク

[Speech](https://developer.apple.com/documentation/speech)
[Apple公式のサンプルコード](https://developer.apple.com/documentation/speech/recognizing_speech_in_live_audio)
音声をマイクから認識して文字列に変換するという機能を提供してくれています。
標準のキーボードのマイクボタンを押した時と同じ機能をアプリ上から利用できます。

# 処理の流れ
1. 音声認識機能利用の権限の確認、取得
1. リクエスト作成、設定
1. タスク作成
1. マイクインプット作成、設定
1. 録音開始
1. テキスト変換結果の取得

# カスタムクラス実装
以下のクラスを実装していきます。
#### SpeechRecognizerクラス
- Speechフレームワークを利用するクラス

#### ListenViewModelクラス
- SpeechRecognizerクラスを保持

#### ListenViewクラス
- ListenViewModelを保持

# 事前準備
info.plistに以下を追加します。
Privacy - Microphone Usage Description
Privacy - Speech Recognition Usage Description

# コード
#### SpeechRecognizer.swift
- パーミッション取得
- 音声認識開始
- 認識結果テキスト取得

SpeechRecognizerは
[Apple公式のサンプルコード](https://developer.apple.com/documentation/speech/recognizing_speech_in_live_audio)
のSpeechFramework利用部分を切り出しているだけなのでほぼ内容は同じになっているのでここでは省略しますが、少し変更している部分だけ紹介します。
```SpeechRecognizer.swift
// ...
    /// 音声認識結果のSubject,Publisher
    private let speechResultSubject: PassthroughSubject<String, Never> = .init()
    lazy var speechResult: AnyPublisher<String, Never> = {
        speechResultSubject.eraseToAnyPublisher()
    }()

    /// 録音開始可能判定のSubject,Publisher
    private let recordEnabledSubject: PassthroughSubject<Bool, Never> = .init()
    lazy var recordEnabled: AnyPublisher<Bool, Never> = {
        recordEnabledSubject.eraseToAnyPublisher()
    }() 
// ...
```
```SpeechRecognizer.swift
// ...
            if let result = result {
                // 認識結果取得
                self?.speechResultSubject.send(result.bestTranscription.formattedString)
// ...
```
認識結果テキストと録音可能判定の部分をCombineのSubjectとPublisherで切り出して外から値を取得できるようにしています。
```SpeechRecognizer.swift
// ...
        // 認識結果を部分的に取得するようにするか
        recognitionRequest.shouldReportPartialResults = true
        // 音声認識処理に通信を利用しないようにするか
        recognitionRequest.requiresOnDeviceRecognition = true
// ...
```
SFSpeechAudioBufferRecognitionRequestのプロパティで設定を入れています。
音声認識の部分的な更新の取得と、通信の利用をしないようにしています。
ただ、通信を利用した方が精度は高くなり、他言語への対応もできます。

#### ListenViewModel.swift
- SpeechRecognizerクラスを保持
- 認識結果テキストデータを保持
- Viewへの橋渡し

```ListenViewModel.swift
class ListenViewModel: ObservableObject {
    private var speechRecognizer = SpeechRecognizer()

    @Published var recordButtonEnabled: Bool = false
    @Published var speechResult: String = ""

    private var cancellables: Set<AnyCancellable> = .init()

    deinit {
        cancellables.forEach { cancellable in
            cancellable.cancel()
        }
    }

    init() {
        speechRecognizer.recordEnabled
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in
                self?.recordButtonEnabled = $0
            }
            .store(in: &cancellables)

        speechRecognizer.speechResult
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in
                self?.speechResult = $0
            }
            .store(in: &cancellables)
    }

    /// 音声認識権限確認、取得
    func authorizeRecord() {
        speechRecognizer.authorizeRecord()
    }

    /// 録音開始
    func tapRecordButton() {
        do {
            try speechRecognizer.startRecording()
        } catch {
            speechResult = "\(error)"
        }
    }
}
```

#### ListenView.swift
- ListenViewModelを保持
- 表示開始時にViewModelのパーミッション取得を呼び出し
- 録音開始ボタン
- 認識状態、結果に合わせてボタン、テキストUIの表示更新
```ListenView.swift
struct ListenView: View {
    @ObservedObject var viewModel: ListenViewModel

    init(viewModel: ListenViewModel = ListenViewModel()) {
        self.viewModel = viewModel
    }

    var body: some View {
        VStack {
            Text(viewModel.speechResult)
            Spacer()
            Button("Start") {
                viewModel.tapRecordButton()
            }
            .disabled(!viewModel.recordButtonEnabled)
        }
        .onAppear {
            viewModel.authorizeRecord()
        }
    }
}
```

# 試してみる
#### 喋る内容
```
アイオーエスのスピーチフレームワークをつかってリアルタイムおんせいにんしき
かっこスピーチトゥーテキストかっことじるをじっそうしてみましたまる
かいぎょうあいふぉんてんアイパッドてんえあぽっど
```
#### 結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678249/e510d831-2f93-78c6-66c7-81763041d71a.png" width="320">

# まとめ
試してみたところ、音声認識の精度やスピードは端末の標準で利用できるものと同等のものを使うことができるようです。
コード自体も複雑なことをしなくても気軽に実装できて良かったです。
サンプルコードではテキストに出力する、というキーボード的な用途で利用しましたが、それはキーボードを使えば良いというだけのことですので、このフレームワークの本質的な使い道は例えば***音声コマンド入力***とか手を***使えない状態***でアプリを利用できるようにする（***次のページという言葉を聞き取ったら次のページに遷移、下にスクロールという言葉を聞き取ったら下にスクロールする処理をする***）ことだと思います。
新しいアプリのUXを提供できるという点でこのフレームワークをしっかりと習得できると今後プラスになるかなと思いました。