iOS开发 - Swift使用SFSpeechRecognizer实现苹果原生API语音转文字

一、在.plist文件中添加麦克风、语音识别权限说明

权限说明

二、代码实现

//
//  LKSpeechRecognizer.swift
//  Comic
//
//  Created by 李棒棒 on 2024/1/5.
//

import UIKit
import Foundation
import Speech
import AVFoundation

/// 是否为模拟器
var IS_Simulator:Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}

enum LKSpeechRecognizerStatus:Int {
    //未开始
    case none
    ///未授权
    case noAuthorize
    ///识别中
    case recognizing
    ///识别结束
    case recognizeFinished
    ///识别关闭(被动关闭)
    case recognizeClose
    ///识别超时(超过预设静音时间(默认:3s)、主动结束)
    case recognizeMuteTimeout
    ///识别报错
    case recognizeError
}

class LKSpeechRecognizer: NSObject {

    static let share = LKSpeechRecognizer()
    
    ///status 状态  baseText:识别结果, speechText:识别校验后的结果
    typealias LKSpeechRecognizerResult = (_ status:LKSpeechRecognizerStatus ,
                                          _ baseText:String?,
                                          _ speechText:String?,
                                          _ error:Error?) -> Void
    
    private var recognizerResult: LKSpeechRecognizerResult? = nil
    
    private var bestText:String? = ""
    private var speakText:String? = ""
    
    //静音间隔时间 默认3s
    var muteTime:TimeInterval = 3.0
    
    var recognizerStatus:LKSpeechRecognizerStatus = .none
    
    private var timer:Timer? = nil
    private var isHaveInput:Bool = false
    
    private var speechTask:SFSpeechRecognitionTask?
    // 语音识别器
    private var speechRequest:SFSpeechAudioBufferRecognitionRequest?
    
    private var speechRecognizer:SFSpeechRecognizer = {
        let locale = Locale(identifier: "zh_CN")
        //NSLocale.current
        let sRecognizer:SFSpeechRecognizer = SFSpeechRecognizer(locale: locale)!//设置识别语种跟随系统语言
        return sRecognizer
    }()
    
    private var audioEngine:AVAudioEngine = {
        let aEngine: AVAudioEngine = AVAudioEngine()
        return aEngine
    }()
    
    override init() {
        super.init()
        
        self.speechRecognizer.delegate = self
        //请求权限
        if IS_Simulator == false {
            //checkAuthorized()
        } else {
            print("模拟器不支持")
        }
        
    }
}

//MARK: -
extension LKSpeechRecognizer {
    
    //开始识别
    func startRecordSpeech() {
        
        bestText = nil
        speakText = nil
        
        //请求授权
        requestSpeechAuthorization {[weak self] authorizeStatus in
            guard let self = self else { return }
            
            if authorizeStatus == false {//用户未授权
                recognizerStatus = .noAuthorize
                recognizerResult?(.noAuthorize,nil,nil,nil)
                return
            }
            
            requestRecordSpeech()
        }
    }
    
    
    func requestRecordSpeech() {
        
        if speechTask != nil {
            speechTask?.cancel()
        }
        
        bestText = nil
        speakText = nil
        
        //AVAudioSession:音频会话,主要用来管理音频设置与硬件交互
        //配置音频会话
        let audioSession = AVAudioSession.sharedInstance()
        do {
    
            try audioSession.setCategory(AVAudioSession.Category.record)
            try audioSession.setMode(AVAudioSession.Mode.measurement)
            try audioSession.setActive(true, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)
            
        } catch let error {
            
            print("audioSession properties weren't set because of an error:\(error.localizedDescription)")
            recognizerStatus = .recognizeError
            recognizerResult?(.recognizeError,self.bestText,self.speakText,error)
            
            return
        }
        
        
        speechRequest = SFSpeechAudioBufferRecognitionRequest()
        speechRequest?.contextualStrings = ["data","bank","databank"]
        
        speechRequest?.shouldReportPartialResults = false
        speechRequest?.taskHint = .search
        
        speechRequest?.shouldReportPartialResults = true
        speechTask = speechRecognizer.recognitionTask(with: self.speechRequest!, resultHandler: { [weak self] (result, error) in
            
            guard let self = self else { return }
            
            var isFinished = false
            isFinished = result?.isFinal ?? false
            
            if  result != nil {//有音频输入
                
                self.isHaveInput = true
                
                let bestString = result!.bestTranscription.formattedString
                print("bestString:\(bestString)")
                
//                var range = NSRange(location: 0, length: bestString.count)
//                if self.speakText?.count ?? 0 > 0 {
//                    range = NSString(string: bestString).range(of: self.speakText ?? "")
//                }
//
//                print("range:\(range)")
//
//                //let nowString = bestString.substring(from: range.length)
//                var nowString = ""
//                nowString = (bestString as NSString).substring(from: range.location)
//
//                print("bestString:\(bestString) - nowString:\(nowString)")
            
                self.bestText = bestString
                self.speakText = bestString
                
                self.recognizerStatus = .recognizing
                self.recognizerResult?(.recognizing,self.bestText,self.speakText,nil)
                
                //一次识别结束后开启静默监测,2s内没有声音做结束逻辑处理
                self.startDetectionSpeech()
            }
            
            if error != nil || isFinished == true {
                
                self.audioEngine.stop()
                self.speechRequest?.endAudio()
                self.speechTask?.cancel()
                
                if self.audioEngine.inputNode.numberOfInputs > 0 {
                    self.audioEngine.inputNode.removeTap(onBus: 0)
                }
                
                if isFinished == true {//结束
                    self.recognizerStatus = .recognizeFinished
                    self.recognizerResult?(.recognizeFinished,self.bestText,self.speakText,nil)
                    print("转换结束了")
                }
                
                if let error = error {//报错了
                    
                    if self.recognizerStatus != .recognizeMuteTimeout {
                        self.recognizerStatus = .recognizeError
                        self.recognizerResult?(.recognizeError,self.bestText,self.speakText,error)
                    }
                }
            }
        })
        
        let inputNode:AVAudioInputNode? = audioEngine.inputNode
        let format = inputNode?.outputFormat(forBus: 0)
        if let inputNode = inputNode {
            
            inputNode.installTap(onBus: 0, bufferSize: 400, format: format, block: { [weak self] buffer, when in
                guard let self = self else { return }
                if let speechRequest = self.speechRequest {
                    speechRequest.append(buffer)
                    self.isHaveInput = false
                }
            })
        }
        
        //准备
        self.audioEngine.prepare()
        do {
            try self.audioEngine.start()
        } catch let error {
            print("audioEngine couldn't start because of an error:\(error.localizedDescription)")
        }
    }
    
    //MARK: - 关闭录音识别
    func closeRecordSpeech() {
        
        stopDetectionSpeech()
        
        if audioEngine.inputNode.numberOfInputs > 0 {
            audioEngine.inputNode.removeTap(onBus: 0)
        }
        audioEngine.stop()
        audioEngine.reset()
        
        speechRequest?.endAudio()
        
        speechTask?.cancel()
        //speechTask?.finish()
        
        recognizerStatus = .recognizeClose
        recognizerResult?(.recognizeClose,self.bestText,self.speakText,nil)
        
        print("录音关闭")
    }
    
    //MARK: - 状态及结果回调
    func recognizerResult(_ completion: LKSpeechRecognizerResult?) {
        recognizerResult = completion
    }
}

//MARK: - 静音监测
extension LKSpeechRecognizer {

   private func startDetectionSpeech(){
        
        if let timer = timer {
            if timer.isValid {
                timer.invalidate()
            }
        }
        
        NSLog("开始计时检测")
        timer = Timer.scheduledTimer(timeInterval: muteTime, target: self, selector: #selector(self.didFinishSpeech), userInfo: nil, repeats: false)
        RunLoop.main.add(timer!, forMode: RunLoop.Mode.common)
    }
    
    private func stopDetectionSpeech() {
        if timer != nil {
            timer?.invalidate()
            timer = nil
            NSLog("结束计时检测")
        }
    }
    
    @objc private func didFinishSpeech() {
        
        if isHaveInput == false {
            
            print("检测到\(muteTime)s内没有说话")
            
            stopDetectionSpeech()
            
            if audioEngine.inputNode.numberOfInputs > 0 {
                audioEngine.inputNode.removeTap(onBus: 0)
            }
            
            audioEngine.stop()
            audioEngine.reset()
            
            
            speechRequest?.endAudio()
            
           
            speechTask?.cancel()
            
            recognizerStatus = .recognizeMuteTimeout
            recognizerResult?(.recognizeMuteTimeout,self.bestText,self.speakText,nil)
        }
    }
}

//MARK: - SFSpeechRecognizerDelegate
extension LKSpeechRecognizer: SFSpeechRecognizerDelegate {
    //录音发生变化
    func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
        print("音频转化发生变化:\(speechRecognizer) - \(available)")
    }
}

//申请权限
extension LKSpeechRecognizer {
    
    //MARK: 检测权限
    func checkAuthorized() {
        requestSpeechAuthorization { authorizeStatus in
            
        }
    }
    
    //MARK: - 申请语音识别权限
    func requestSpeechAuthorization(authorize: @escaping (Bool)-> Void ) {
        
        if IS_Simulator == true {
            authorize(false)
            print("模拟器不支持")
            return
        }
        
        //请求权限
        DispatchQueue.global().async {
            
            SFSpeechRecognizer.requestAuthorization {[weak self] status in
                //SFSpeechRecognizerAuthorizationStatus
                guard let self = self else {return}
                
                DispatchQueue.main.async {
                    let isSpeechAuthorized:Bool = (status == SFSpeechRecognizerAuthorizationStatus.authorized)
                    authorize(isSpeechAuthorized)
                }
                
                switch status {
                case .notDetermined:
                    NSLog("Speech Recognizer Authorization Status-Not Determined")
                case .denied://拒绝
                    NSLog("Speech Recognizer Authorization Status-Denied")
                    DispatchQueue.main.async {
                       
                        let alertController:UIAlertController = UIAlertController(title: "无法访问语音权限", message: "请在iPhone的\"设置-隐私-语音识别\"\"中允许访问麦克风、语音识别权限\"", preferredStyle: .alert)
                        
                        alertController.addAction(UIAlertAction(title: "取消", style: .cancel, handler: { alertAction in
                            
                        }))
                        alertController.addAction(UIAlertAction(title: "设置", style: .default, handler: { alertAction in
                            UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
                           
                        }))
                        AppDelegate.currentViewController?.present(alertController, animated: true)
                    }
                    
                case .restricted:
                    NSLog("Speech Recognizer Authorization Status-Restricted")
                case .authorized:
                    NSLog("Authorized")
                @unknown default:
                    NSLog("unknown")
                }
            }
        }
    }
    
}

三、调用方法

开始转换

LKSpeechRecognizer.share.startRecordSpeech()

主动关闭

LKSpeechRecognizer.share.closeRecordSpeech()

转换结果回调

LKSpeechRecognizer.share.recognizerResult({ [weak self] (status,baseText,speechText,error) in
            guard let self = self else { return }
            
            print("结果:\(speechText ?? "")")
            
            if status == .noAuthorize {
                self.statusLab.text = "状态:用户麦克风未授权"
            }else if status == .recognizeFinished || status == .recognizeMuteTimeout{
                self.statusLab.text = "状态:您长时间未讲话,已停止识别"
            }else if status == .recognizeClose {
                self.statusLab.text = "状态:已手动停止识别"
            }else if status == .recognizing {
                self.statusLab.text = "请讲话……"
            }else if status == .recognizeError {
                self.statusLab.text =  "状态error:\(error?.localizedDescription ?? "")"
            }
    
            self.textView.text = speechText
            
        })

四、实现效果

效果

五、源码地址

LKSpeech

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容