アプリケーション開発ポータルサイト
ServerNote.NET
カテゴリー【SwiftiPhone/iPadXcode
【Swift UI】マイク使用許可を得て音声をテキストに変換する(音声認識)
POSTED BY
2023-07-13

iPhone端末のマイクでしゃべった内容をテキストに変換して出力するサンプルです。
「スピーチ開始」で、端末に向かって喋りかければ、変換内容を随時テキスト出力します。

ネイティブのSFSpeechRecognizerクラスを使うことになりますが、以下の特徴があります。

・無料
・内部ではマイクからの音声データをネットワーク通信でAppleサーバに送信し翻訳を受け取るので動作は重い
・ネットワーク通信をしない=オフライン辞書もあるがこれは英語専用
・1回のスピーチ入力制限時間は1分

今回日本語をターゲットにするのでオフライン辞書は利用しません。

プロジェクト一式はこちら。

https://github.com/servernote/iPhoneSample/tree/master/VoiceToText

以下ソースです。
まず、Info.plistにてマイクの使用と音声認識の使用を宣言します。

Info.plist をクリック
Information Property List をクリックして+
Privacy - Microphone Usage Description
Privacy - Speech Recognition Usage Description
を追加、説明Stringはそれぞれ
このアプリはマイクを使用します
このアプリは音声を認識します
などとする。

最小限のサンプルなので作成クラスは表示クラスContentView.swiftと音声認識クラスSpeechRecorder.swiftの2つです。

SwiftContentView.swiftGitHub Source
//
//  ContentView.swift
//  VoiceToText
//
//  Created by webmaster on 2020/06/14.
//  Copyright © 2020 SERVERNOTE.NET. All rights reserved.
//
import SwiftUI
import Speech
import AVFoundation

struct ContentView: View {
    @ObservedObject private var speechRecorder = SpeechRecorder()
    @State var showingAlert = false
    
    var body: some View {
        ScrollView{
            VStack(alignment: .leading, spacing: 5) {
                HStack() {
                    Spacer()
                    Button(action: {
                        if(AVCaptureDevice.authorizationStatus(for: AVMediaType.audio) == .authorized &&
                            SFSpeechRecognizer.authorizationStatus() == .authorized){
                            self.showingAlert = false
                            self.speechRecorder.toggleRecording()
                            if !self.speechRecorder.audioRunning {
                                DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
                                    
                                }
                            }
                        }
                        else{
                            self.showingAlert = true
                        }
                    })
                    {
                        if !self.speechRecorder.audioRunning {
                            Text("スピーチ開始")
                                .padding()
                                .overlay(
                                    RoundedRectangle(cornerRadius: 10)
                                        .stroke(Color.blue, lineWidth: 1))
                        } else {
                            Text("スピーチ終了")
                                .padding()
                                .overlay(
                                    RoundedRectangle(cornerRadius: 10)
                                        .stroke(Color.red, lineWidth: 1))
                        }
                    }
                    .alert(isPresented: $showingAlert) {
                        Alert(title: Text("マイクの使用または音声の認識が許可されていません"))
                    }
                    Spacer()
                }
                Text(self.speechRecorder.audioText)
            }
            .onAppear{
                AVCaptureDevice.requestAccess(for: AVMediaType.audio) { granted in
                    OperationQueue.main.addOperation {
                        
                    }
                }
                SFSpeechRecognizer.requestAuthorization { status in
                    OperationQueue.main.addOperation {
                        //switch status {
                        //    case .authorized:
                        //
                        //    default:
                        //
                        //}
                    }
                }
            }
        }.padding(.vertical)
    }
}

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

・まずViewのonAppearでマイクと音声認識の許可を求めるダイアログを出しておきます。
AVCaptureDevice.requestAccess(for: AVMediaType.audio) { granted in //マイク
SFSpeechRecognizer.requestAuthorization { status in //音声認識
それぞれ許可されているかどうかは別のシステム関数で取得できるので、ここで結果を保存しておく必要はないです。

・ボタンタップで上記2つの許可がおりているかを確認し、許可されていなかったらアラートを出します。
 許可されていれば、スピーチ認識を開始、もしくは終了します。

・SpeechRecorderクラスで音声認識されたテキストを常時表示します。

SwiftSpeechRecorder.swiftGitHub Source
//
//  SpeechRecorder.swift
//  VoiceToText
//
//  Created by webmaster on 2020/06/14.
//  Copyright © 2020 SERVERNOTE.NET. All rights reserved.
//
import Foundation
import Combine
import AVFoundation
import Speech

final class SpeechRecorder: ObservableObject {
    @Published var audioText: String = ""
    @Published var audioRunning: Bool = false
    private var audioEngine = AVAudioEngine()
    private var speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    
    func toggleRecording(){
        if self.audioEngine.isRunning {
            self.stopRecording()
        }
        else{
            try! self.startRecording()
        }
    }
    
    func stopRecording(){
        self.recognitionTask?.cancel()
        self.recognitionTask?.finish()
        self.recognitionRequest?.endAudio()
        self.recognitionRequest = nil
        self.recognitionTask = nil
        self.audioEngine.stop()
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(AVAudioSession.Category.playback)
            try audioSession.setMode(AVAudioSession.Mode.default)
        } catch{
            print("AVAudioSession error")
        }
        self.audioRunning = false
    }
    
    func startRecording() throws {
        self.speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
        let audioSession = AVAudioSession.sharedInstance()
        try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
        try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        let inputNode = audioEngine.inputNode
        inputNode.removeTap(onBus: 0)
        self.recognitionTask = SFSpeechRecognitionTask()
        self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        if(self.recognitionTask == nil || self.recognitionRequest == nil){
            self.stopRecording()
            return
        }
        self.audioText = ""
        recognitionRequest?.shouldReportPartialResults = true
        if #available(iOS 13, *) {
            recognitionRequest?.requiresOnDeviceRecognition = false
        }
        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest!) { result, error in
            if(error != nil){
                print (String(describing: error))
                self.stopRecording()
                return
            }
            var isFinal = false
            if let result = result {
                isFinal = result.isFinal
                self.audioText = result.bestTranscription.formattedString
                print(result.bestTranscription.formattedString)
            }
            if isFinal { //録音タイムリミット
                print("recording time limit")
                self.stopRecording()
                inputNode.removeTap(onBus: 0)
            }
        }
        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            self.recognitionRequest?.append(buffer)
        }
        self.audioEngine.prepare()
        try self.audioEngine.start()
        self.audioRunning = true
    }
}

・スピーチの制限時間(1分)にい到達して終わってしまった場合recognitionTask.result.isFinalがtrueになるので、このタイミングでstartRecordingを呼んでやれば、自動で再度録音を開始させることができますが、翻訳バッファーテキストはクリアされるので、どこかに蓄積しておくなど工夫が必要です。

※本記事は当サイト管理人の個人的な備忘録です。本記事の参照又は付随ソースコード利用後にいかなる損害が発生しても当サイト及び管理人は一切責任を負いません。
※本記事内容の無断転載を禁じます。
【WEBMASTER/管理人】
自営業プログラマーです。お仕事ください!
ご連絡は以下アドレスまでお願いします★

【キーワード検索】