Help us understand the problem. What is going on with this article?

【Speech Framework】音声認識してテキストを入力する

More than 3 years have passed since last update.

概要

AppleのSpeechフレークワークサンプルコードを再現します。
iOS10から追加されたSpeechフレームワークを使って音声認識。
「認識された音声をテキスト化して画面に表示する」という単純なアプリが完成します。
ほとんどApple提供のサンプルコードのままですが、日本語対応させたりコードの記述が違ったりする箇所があります。

動作環境及び開発環境

Swift3
Xcode8
macOS10.12 Sierra
iOS10(iPad, iPhone)

手順

プロジェクト作成

プロジェクト名は「SpeakWriter」とする。
テンプレートは「iOS > Single View Application」を選択。

プロパティリスト

Info.plistに許諾説明を追加する。
Privacy - NSSpeechRecognitionUsageDescription: このアプリは音声認識機能を使用します。
Privacy - Microphone Usage Description: このアプリはマイクを使用します。

Storyboard

まずは、ユーザインターフェースをつくる

認識テキストを表示するビュー

UITextViewを配置(認識された音声をテキスト表示: textView)
背景・テキストカラー・フォントサイズを変更
インスペクタエリアからisUserInteractionプロパティを無効にする

録音開始ボタン

UIButtonUITextViewの真下に配置
背景・テキストカラー・フォントサイズを変更

Constraintsを付加する

この設定値および設定方法は適当にしてある。
UITextView(Top, Leading, Trailing Space = 0, Bottom to next view = 10)
UIButton(Width = 200, Height = 64, Bottom to = 10, Horizontal center)

Interface Builderと接続

UITextView <=> @IBOutlet: textView
UIButton <=> @IBOutlet: recordButton, @IBAction: recordButtonTapped

ViewController.swiftにコーディング

録音開始ボタンを無効にする

recordButtonはアプリ起動時は無効で、ユーザから録音許可を得た後に有効化する。

viewDidLoad()メソッド
recordButton.isEnabled = false

Speechフレームワークを組み込む

音声認識するための機能を盛り込んだフレームワーク

viewControllerクラス
import Speech

SFSpeechRecognizerインスタンスを生成

ローカルを指定しない場合は、端末の地域設定になる。
メンバプロパティで生成・初期化しておく。
明示的アンラップしておく。

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

ユーザから使用許可を得る

コールバックをメインスレッドで実行している

viewWillAppear(_
SFSpeechRecognizer.requestAuthorization { (status) in
    OperationQueue.main().addOperation {
                switch status {
                case .authorized:   // 許可OK
                    self.recordButton.isEnabled = true
                    self.recordButton.backgroundColor = UIColor.blue
                case .denied:       // 拒否
                    self.recordButton.isEnabled = false
                    self.recordButton.setTitle("録音許可なし", for: .disabled)
                case .restricted:   // 限定
                    self.recordButton.isEnabled = false
                    self.recordButton.setTitle("このデバイスでは無効", for: .disabled)
                case .notDetermined:// 不明
                    self.recordButton.isEnabled = false
                    self.recordButton.setTitle("録音機能が無効", for: .disabled)
                }
    }
}

一旦、ビルド

アプリ起動直後、ユーザ許可を求められることを確認する

音声認識のタスクを宣言する

メンバプロパティでタスクのオブジェクトを宣言
初期値は設定しないからオプショナル

ViewControllerクラス
.
.
.
private var recognitionTask: SFSpeechRecognitionTask?

録音スタート処理を実装する

タスクにリクエストを登録すると、その結果に音声認識された文字列が返ってくる
startRecording()throwsメソッドを実装
エラーハンドリングするためthrowsキーワードをつける(実際はしない)

ViewControllerクラス
private func startRecording() throws {
//ここに録音する処理を記述
}

既存のタスクが存在するなら、全てキャンセル

startRecording()メソッド
    if let recognitionTask = recognitionTask {
    // 既存タスクがあればキャンセルしてリセット
        recognitionTask.cancel()
        self.recognitionTask = nil
    }

セッションを準備する

startRecording()メソッド
    ...
    ...

    let audioSession = AVAudioSession.sharedInstance()
    try audioSession.setCategory(AVAudioSessionCategoryRecord)
    try audioSession.setMode(AVAudioSessionModeMeasurement)
    try audioSession.setActive(true, with: .notifyOthersOnDeactivation)

認識リクエストのインスタンスを宣言

クラスのメンバプロパティで宣言する
初期値は設定しないからオプショナル

ViewControllerクラス
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?

認識開始の前に認識リクエストを初期化

startRecording()メソッド
    ...
    ...

    recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
    guard let recognitionRequest = recognitionRequest else { fatalError("リクエスト生成エラー") }

shouldReportPartialResultsプロパティ

録音完了前に途中の結果を報告してくれる(デフォルトはfalse)
trueにすると、完了時に結果をさかのぼって修正してくれる

startRecording()メソッド
    ...
    ...

    recognitionRequest.shouldReportPartialResults = true

端末のマイクを使う準備

AVAudioEngineクラス(iOS8~)を使う
メンバプロパティで生成・初期化する

ViewControllerクラス
...
...

private let audioEngine = AVAudioEngine()

inputNode

audioEngineインスタンスのinputNodeプロパティを取得する

startRecording()メソッド
    ...
    ...

    guard let inputNode = audioEngine.inputNode else { fatalError("InputNodeエラー") }

リクエストを登録してタスクを実行

認識タスクのインスタンスの初期化メソッド内のハンドラで、結果を処理する
生成されるタスクのインスタンスは、SFSpeechRecognitionTask型オブジェクト
ハンドラ内のresultにはSFSpeechRecognitionResult型オブジェクト
resultのbestTranscriptionプロパティには、最も精度が高かった認識結果のStringオブジェクトが入っている
shouldReportPartialResultsプロパティがtrueなら、bestTranscriptionのオブジェクトが変化するのかもしれない

startRecording()メソッド
    ...
    ...

    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: [])
        self.recordButton.backgroundColor = UIColor.blue

    }
}

マイクからの録音フォーマット

startRecording()メソッド
    ...
    ...

    let recordingFormat = inputNode.outputFormat(forBus: 0)


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

オーディオエンジンで録音を開始して、テキスト表示を変更する

startRecording()メソッド
    ...
    ...    

    audioEngine.prepare()   // オーディオエンジン準備
    try audioEngine.start() // オーディオエンジン開始

    textView.text = (認識中…そのまま話し続けてください)

デリゲート処理

ViewControllerクラスにデリゲートを採用する
speechRecognizerの状態変化を受け取れるようになる

viewControllerクラス
Class ViewController: UIViewController, SFSpeechRecognitionTaskDelegate {

   ...
   ...

自身のクラスをデリゲート先を設定する

viewWillAppear(_;)メソッド
    override func viewWillAppear(_ animated: Bool) {
        speechRecognizer.delegate = self    // デリゲート先になる
        ...
        ...

デリゲートメソッド実装

音声認識機能の状態が変化するタイミングで呼ばれる
録音ボタンの有効と無効を切り替える

viewDidLoad()メソッド
    public func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
        if available {
        // 利用可能になったら、録音ボタンを有効にする
            recordButton.isEnabled = true
            recordButton.setTitle("Start Recording", for: [])
            recordButton.backgroundColor = UIColor.blue
        } else {
        // 利用できないなら、録音ボタンは無効にする
            recordButton.isEnabled = false
            recordButton.setTitle("現在、使用不可", for: .disabled)
        }
    }

IBActionメソッドを実装

recordButtonがタップされたときの処理
audioEngineが動作中なら、音声の認識中ということ
録音開始

ViewControllerクラス
    @IBAction func recordButtonTapped() {
        if audioEngine.isRunning {
        // 音声エンジン動作中なら停止
            audioEngine.stop()
            recognitionRequest?.endAudio()
            recordButton.isEnabled = false
            recordButton.setTitle("Stopping", for: .disabled)
            recordButton.backgroundColor = UIColor.lightGray
            return
        }
        // 録音を開始する
        try! startRecording()
        recordButton.setTitle("認識を完了する", for: [])
        recordButton.backgroundColor = UIColor.red
    }

考察

面白い。すごく面白い。
初心者にはいろいろ、ややこしいことは多かったが作業量の割に楽しめるものができた。
テキストビューのリセットボタンつけたり、を外部ライブラリの翻訳機能と連携させたりしたら、使えるアプリも作れるかもしれない。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away