SwiftUIとSpeech Frameworkで動画の文字起こしアプリを作ってみる

音声認識に興味が出たので、Apple製のSpeech FrameworkとSwiftUIを使って簡易的なMacアプリ作ってみたので得られた知見をご紹介します。

## 完成品

ニュース動画のファイルを選択して、音声認識しています。
![SpeechRecognizer.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147358/a68b14a4-a0e1-e2a9-30ec-bf2c30a493cf.gif)

-----------


ニュース動画なのでアナウンサーの声だけでノイズがないからか、かなりの精度:ok_hand:


------------------

## 開発環境
- SwiftUI
- Speech Framework
- Swift5.1
- macOS 10.15.2(Catalina)
- Xcode 11.3.1

MacでSpeech FrameworkとSwiftUI使えるのはCatalina以降なので最新版にアップデートしましょう

-----------


## SwiftUIベースのMacアプリ作成

### 新規プロジェクトでSwiftUIを選択

`create new project`からmacOSのAppを選択し、`User Interface`の項目を`SwiftUI`にします

<img width="1172" alt="スクリーンショット 2020-01-25 15.40.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147358/9cd739ca-3d43-de68-adf0-74cf160a61bd.png">

-----------

初期状態で作成される`ContentView.swift`にレイアウト定義と音声認識の処理をつらつら書いていきます。
SwiftUIといえど、Viewに処理を書くのはあまり良くありませんが、今回は簡易的なアプリなので、 全部Viewに処理を書いてしまいます

```ContentView.swift

import SwiftUI
import Speech

struct ContentView: View {
    @State var recognizedText: String?
    @State var message: String = ""
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))
    
    var body: some View {
        VStack(alignment: .trailing) {
            Text(recognizedText ?? "")
                .font(.body)
                .frame(width: 480, height: 320, alignment: .top)
                .border(Color.gray)
                .padding()
            HStack {
                Text(message)
                Button("Choose file") {
                    SFSpeechRecognizer.requestAuthorization { (status) in
                        guard status == .authorized else {
                            print("音声入力が認可されていません")
                            return
                        }
                        // NSOpenPanelはMain Threadからのみアクセス可
                        DispatchQueue.main.async {
                            let panel = NSOpenPanel()
                            let result = panel.runModal()
                            guard result == .OK, let url = panel.url else {
                                print("ファイル読み込みに失敗")
                                return
                            }
                            
                            let speechRequest = SFSpeechURLRecognitionRequest(url: url)
                            self.message = "音声認識中..."
                            self.recognizedText = ""
                            _ = self.speechRecognizer?.recognitionTask(with: speechRequest, resultHandler: { (speechResult, error) in
                                guard let speechResult = speechResult else {
                                    return
                                }
                                
                                if speechResult.isFinal {
                                    self.message = "音声認識が完了しました"
                                    print("Speech in the file is \(speechResult.bestTranscription.formattedString)")
                                } else {
                                    let text = speechResult.bestTranscription.formattedString
                                    self.recognizedText = text
                                }
                            })
                        }
                    }
                }
            }.padding()
        }
    }
}
```

### コード解説

####  `@State`
  
SwiftUIのホットリロード機能の一番簡単な`@State`を用いて、UI更新処理を書かないようにしています

#### NSOpenPanel
Macのファイルダイアログをプログラムから呼ぶのは`NSOpenPanel`というクラス使うらしいです。初めてしりました。Viewファイルに書いてあるから大丈夫かと思いきや明示的にMainスレッド指定しないと実行時エラーになってしまうので注意

### SFSpeechRecognizer
今回のアプリの肝のクラスですね
日本語の文字起こしを想定しているので ローカル情報を`ja-JP`にしたインスタンスを保持。
ユーザ認可をリクエストしたのち、ファイルダイアログから取得できたファイルのURLで`SFSpeechURLRecognitionRequest`のインスタンスを作って、認識タスクをコールバックと同じく登録して完了です。

コールバックで返却される`SFSpeechRecognitionResult`に認識結果が返ってくるので、それを画面に表示してあげるだけで完了です。
`.bestTranscription.formattedString`というプロパティにいわゆる文字起こし結果が入ってきますが、他にも声の抑揚や、話す早さなどが返ってくるのが面白いところなので、興味ある方は色々デバッグして見てみると良いかもしれません。




## 注意点

### plistに音声入力の説明を定義忘れずに
`NSSpeechRecognitionUsageDescription`に音声入力を許可するダイアログ時の文言をセットを忘れずに
最近めっきりプライバシーに厳しいAppleフレームワーク。他のフレームワーク同様、音声入力もユーザ認可が必要なのであしからず。


### 音声認識できる時間の上限が1分
オンラインを介した音声認識のみ、上限が設けられています。
これはそもそもiOS等々のキーボードからの音声入力が1分という上限があるかららしい。(バッテリーや通信量に配慮する為)
1分ごとに区切ってタスクを捌いていけば、長時間動画の完全文字起こしもいけそう？

iOS13から端末上のみで音声認識できるようになり、そちらなら上限はないようだが、対応言語が絞られる(日本語非対応)上に継続的に改善されるオンラインとは違い、精度が良くないとのこと。この辺はトレードオフですね。


## まとめ

### SwiftUIとSpeech Framework凄すぎ
音声認識の知識ほぼ皆無な私でも簡単なアプリ作れちゃうくらいシンプルなインターフェイスと使いやすいフレームワークな上に精度もかなりでビックリしました。
Macアプリは殆ど作ったことない私でもこれ、ググったりするだけで1時間くらいで作成できました(むしろこのQiita書くほうが時間かかってる。。。)




## 参考にした記事
[[iOS] 最新のSpeech Recognitionについて](https://qiita.com/mtfum/items/d842c8f5a0e5b99fa4ad)

[【iOS】Speech Frameworkの実装](https://qiita.com/KentaKudo/items/8473146d5d596069b857)