【iOS】Speech語言識別初探(swift)

RT:
一直以來,對Siri的語音識別功能很感興趣幻枉,但一直沒有時間去研究扔傅,今天心血來潮耍共,便找了點資料,自己動手試著做了一個簡單的Dome猎塞,效果如下——

最終效果:

動圖效果

制作思路:

  1. 在Info.plist文件添加麥克風和語言識別權(quán)限描述
  2. 引入Speech語言識別包
  3. 真機測試(必須真機)

Microphone Usage Description和Speech Recognition Usage Description使用意圖描述试读,內(nèi)容隨便寫!


Info.plist

requiresOnDeviceRecognition屬性可以設(shè)置為true荠耽,不需要訪問服務器钩骇,貌似有使用數(shù)量限制!僅限與設(shè)備上就沒有

if #available(iOS 13, *) {
    // 將此屬性設(shè)置為true以防止SFSpeechRecognitionRequest通過網(wǎng)絡(luò)發(fā)送音頻
    // 設(shè)備上的請求將不那么準確铝量。
    recognitionRequest.requiresOnDeviceRecognition = true
}

并且可以識別本地音頻倘屹,不過最好是將音頻的時間限制在1分中以內(nèi)!
本地音頻文件識別部分慢叨,我注釋掉了纽匙,只留實時錄制音頻識別部分的代碼,感興趣可以取消注釋測試看看效果拍谐!

///////////  識別音頻文件
/*============================================================================*/
//    @objc private func recognizeBtnDidClick(_ sender: UIButton) {
//        var info = ""
//        sender.isSelected = !sender.isSelected
//
//        if sender.isSelected {
//            info = "正在識別···"
//            print(info)
//            sender.setTitle(info, for: .normal)
//            sender.backgroundColor = .orange
//            let path = Bundle.main.path(forResource: "Track 1_004", ofType: "wav")
//            let url: NSURL = NSURL.init(fileURLWithPath: path!)
//            recognizeFile(url: url)
//
//        } else {
//            info = "停止識別烛缔!"
//            print(info)
//            sender.setTitle(info, for: .normal)
//            sender.backgroundColor = .blue
//        }
//    }
    
//    // 音頻文件識別
//    func recognizeFile(url:NSURL) {
//
//        guard let myRecognizer = SFSpeechRecognizer.init(locale: Locale.init(identifier: "zh-CN")) else { return }
//
//        if !myRecognizer.isAvailable { return }
//
//        let request = SFSpeechURLRecognitionRequest(url: url as URL)
//        myRecognizer.recognitionTask(with: request) { (result, error) in
//            guard let result = result else { return }
//
//            self.textView.text = result.bestTranscription.formattedString
//            if result.isFinal {
//                print("Speech in the file is \(result.bestTranscription.formattedString)")
//                self.textView.text = result.bestTranscription.formattedString
//            }
//        }
//    }
/*============================================================================*/

官方也有相關(guān)的案例,感興趣可以下載過來學習研究……(-)

完整代碼:

//
//  SpeechVC.swift
//  UIKit-basic
//
//  Created by Qire_er on 2022/1/16.
//

import UIKit
import Speech

class SpeechVC: UIViewController {
    
    var textView: UITextView! // 用于顯示識別文本
    var recognizeBtn: UIButton! // 錄制按鈕
    
    // 定義語言識別需要用到的幾個對象的引用
    /*====================================================================================*/
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "zh-CN"))! // 創(chuàng)建與指定區(qū)域設(shè)置關(guān)聯(lián)的語音識別器
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? // 語音識別的請求
    private var recognitionTask: SFSpeechRecognitionTask? // 語音識別的任務類
    private let audioEngine = AVAudioEngine() // 音頻引擎轩拨,用于音頻輸入
    /*====================================================================================*/
    
    private let recodingBG: UIColor = .red // 定義【正在錄制】按鈕背景色
    private let enableBG: UIColor = .blue // 定義【可用狀態(tài)】按鈕背景色
    private let disableBG: UIColor = .systemGray3 // 定義【禁用狀態(tài)】按鈕背景色
    
    // 添加UI
    override func viewDidLoad() {
        super.viewDidLoad()

        let vStack = UIStackView()
        vStack.translatesAutoresizingMaskIntoConstraints = false
        vStack.axis = .vertical
        
        textView = UITextView()
        textView.font = .boldSystemFont(ofSize: 46)
        textView.backgroundColor = .systemGray5
        
        recognizeBtn = UIButton()
        recognizeBtn.setTitle("開始錄制", for: .normal)
        recognizeBtn.setTitleColor(UIColor.gray, for: .disabled)
        recognizeBtn.addTarget(self, action: #selector(recordButtonTapped), for: .touchUpInside)
        recognizeBtn.isEnabled = false  // 默認禁用
        
        vStack.addArrangedSubview(textView)
        vStack.addArrangedSubview(recognizeBtn)
        
        view.addSubview(vStack)
        view.backgroundColor = .white
        
        NSLayoutConstraint.activate([
            vStack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 15),
            vStack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -15),
            vStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15),
            vStack.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -15),
            recognizeBtn.heightAnchor.constraint(equalToConstant: 80)
        ])
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        speechRecognizer.delegate = self // 設(shè)置代理
        
        // MARK: 請求語音識別權(quán)限
        SFSpeechRecognizer.requestAuthorization { (status) in
            print("status = \(status.rawValue)")
            
            OperationQueue.main.addOperation {
                switch status {
                case .authorized :  // 用戶已授權(quán)
                    self.recognizeBtn.isEnabled = true
                    self.recognizeBtn.backgroundColor = .blue
                    
                case .notDetermined :  // 用戶未授權(quán)
                    self.recognizeBtn.isEnabled = false
                    self.recognizeBtn.setTitle("語音識別未經(jīng)授權(quán)践瓷!", for: .disabled)
                    self.recognizeBtn.backgroundColor = self.disableBG
                    
                case .denied :  // 用戶拒絕
                    self.recognizeBtn.isEnabled = false
                    self.recognizeBtn.setTitle("用戶拒絕訪問語音識別!", for: .disabled)
                    self.recognizeBtn.backgroundColor = self.disableBG
                    
                case .restricted :  // 設(shè)備不支持
                    self.recognizeBtn.isEnabled = false
                    self.recognizeBtn.setTitle("語音識別不支持此設(shè)備气嫁!", for: .disabled)
                    self.recognizeBtn.backgroundColor = self.disableBG
                    
                default:  // 默認情況
                    self.recognizeBtn.isEnabled = false
                    self.recognizeBtn.backgroundColor = self.disableBG
                }
            }
        }
    }
    
    // 錄制方法
    private func startRecording() throws {
        
        // 取消上一次正在識別任務(如果有的話)
        recognitionTask?.cancel()
        self.recognitionTask = nil
        
        // 配置應用程序的音頻會話
        let audioSession = AVAudioSession.sharedInstance() // 管理音頻硬件資源的分配
        try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers) // 設(shè)置音頻會話的類別当窗、模式和選項。
        try audioSession.setActive(true, options: .notifyOthersOnDeactivation) // 激活音頻會話
        let inputNode = audioEngine.inputNode // inputNode|outputNode分別對應硬件的麥克風和揚聲器

        // 創(chuàng)建并配置語音識別請求
        recognitionRequest = SFSpeechAudioBufferRecognitionRequest() // 從捕獲的音頻內(nèi)容(如來自設(shè)備麥克風的音頻)識別語音的請求
        guard let recognitionRequest = recognitionRequest else { fatalError("無法創(chuàng)建SFSpeechAudioBufferRecognitionRequest對象") }
        
        // 設(shè)置在音頻錄制完成之前返回結(jié)果
        // 每產(chǎn)生一種結(jié)果就馬上返回
        recognitionRequest.shouldReportPartialResults = true
        
        // 將語音識別數(shù)據(jù)僅限于設(shè)備上
        if #available(iOS 13, *) {
            // 將此屬性設(shè)置為true以防止SFSpeechRecognitionRequest通過網(wǎng)絡(luò)發(fā)送音頻
            // 設(shè)備上的請求將不那么準確寸宵。
            recognitionRequest.requiresOnDeviceRecognition = true
        }
        
        // 為語音識別會話創(chuàng)建識別任務
        // 保留對任務的引用崖面,以便可以取消該任務
        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in
            var isFinal = false
            
            if let result = result {
                // 使用識別結(jié)果更新文本視圖
                self.textView.text = result.bestTranscription.formattedString
                isFinal = result.isFinal
                print("【識別內(nèi)容】\(result.bestTranscription.formattedString)")
            }
            
            if error != nil || isFinal {
                // 如果出現(xiàn)問題元咙,停止識別語音
                self.audioEngine.stop()
                inputNode.removeTap(onBus: 0)

                self.recognitionRequest = nil
                self.recognitionTask = nil

                self.recognizeBtn.isEnabled = true  // 設(shè)置按鈕為可用狀態(tài)
                self.recognizeBtn.setTitle("開始錄制", for: []) // 設(shè)置按鈕文字
                self.recognizeBtn.backgroundColor = self.enableBG
            }
        }

        // 配置麥克風輸入
        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            self.recognitionRequest?.append(buffer)
        }
        
        audioEngine.prepare()
        try audioEngine.start()
        
        // 提示用戶開始錄制
        textView.text = "點擊【開始錄制】···"
    }
    
    // 定義按鈕點擊處理函數(shù)
    @objc private func recordButtonTapped() {
        if audioEngine.isRunning {
            audioEngine.stop()
            recognitionRequest?.endAudio()
            recognizeBtn.isEnabled = false
            recognizeBtn.setTitle("停止錄制", for: .disabled)
        } else {
            do {
                try startRecording()
                recognizeBtn.setTitle("停止錄制", for: [])
                recognizeBtn.backgroundColor = recodingBG
            } catch {
                recognizeBtn.setTitle("錄音不可用!", for: [])
                recognizeBtn.backgroundColor = self.disableBG
            }
        }
    }
    
///////////  識別音頻文件
/*============================================================================*/
//    @objc private func recognizeBtnDidClick(_ sender: UIButton) {
//        var info = ""
//        sender.isSelected = !sender.isSelected
//
//        if sender.isSelected {
//            info = "正在識別···"
//            print(info)
//            sender.setTitle(info, for: .normal)
//            sender.backgroundColor = .orange
//            let path = Bundle.main.path(forResource: "Track 1_004", ofType: "wav")
//            let url: NSURL = NSURL.init(fileURLWithPath: path!)
//            recognizeFile(url: url)
//
//        } else {
//            info = "停止識別巫员!"
//            print(info)
//            sender.setTitle(info, for: .normal)
//            sender.backgroundColor = .blue
//        }
//    }
    
//    // 音頻文件識別
//    func recognizeFile(url:NSURL) {
//
//        guard let myRecognizer = SFSpeechRecognizer.init(locale: Locale.init(identifier: "zh-CN")) else { return }
//
//        if !myRecognizer.isAvailable { return }
//
//        let request = SFSpeechURLRecognitionRequest(url: url as URL)
//        myRecognizer.recognitionTask(with: request) { (result, error) in
//            guard let result = result else { return }
//
//            self.textView.text = result.bestTranscription.formattedString
//            if result.isFinal {
//                print("Speech in the file is \(result.bestTranscription.formattedString)")
//                self.textView.text = result.bestTranscription.formattedString
//            }
//        }
//    }
/*============================================================================*/
}

// MARK: SFSpeechRecognizerDelegate
extension SpeechVC: SFSpeechRecognizerDelegate {
    public func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) {
        if available {
            recognizeBtn.isEnabled = true
            recognizeBtn.setTitle("開始錄制···", for: [])
        } else {
            recognizeBtn.isEnabled = false
            recognizeBtn.setTitle("語言識別不可用庶香!", for: .disabled)
        }
    }
}

控制臺也有相關(guān)信息的輸出!感覺還是挺強大简识,值得好好研究……


控制臺輸出

(==完==)

ps: 以上僅代表個人淺見赶掖,如果你有什么高見,也歡迎討論交流七扰!-

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末奢赂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子颈走,更是在濱河造成了極大的恐慌膳灶,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件立由,死亡現(xiàn)場離奇詭異轧钓,居然都是意外死亡,警方通過查閱死者的電腦和手機锐膜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門毕箍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人道盏,你說我怎么就攤上這事而柑。” “怎么了捞奕?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵牺堰,是天一觀的道長。 經(jīng)常有香客問我颅围,道長伟葫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任院促,我火速辦了婚禮筏养,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘常拓。我一直安慰自己渐溶,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布弄抬。 她就那樣靜靜地躺著茎辐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拖陆,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天弛槐,我揣著相機與錄音,去河邊找鬼依啰。 笑死乎串,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的速警。 我是一名探鬼主播叹誉,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼闷旧!你這毒婦竟也來了长豁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤鸠匀,失蹤者是張志新(化名)和其女友劉穎蕉斜,沒想到半個月后逾柿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缀棍,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年机错,在試婚紗的時候發(fā)現(xiàn)自己被綠了爬范。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡弱匪,死狀恐怖青瀑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情萧诫,我是刑警寧澤斥难,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站帘饶,受9級特大地震影響哑诊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜及刻,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一镀裤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缴饭,春花似錦暑劝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春傅联,著一層夾襖步出監(jiān)牢的瞬間智嚷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工纺且, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盏道,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓载碌,卻偏偏與公主長得像猜嘱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嫁艇,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容