使用 iOS 10 的 Speech 框架构建语音转文本应用

学习自SwiftGG

在 2016 年的 WWDC 上,Apple 介绍了一个十分有用的语音识别 API,那就是 Speech 框架。事实上,Siri 的语音识别正是由 Speech Kit 提供支持。就目前来说,可用的语音识别框架并非没有,但是它们要么太贵,要么不够好。在本教程中,我将会向你演示如何使用 Speech Kit 来创建一个像 Siri 一样的应用来进行语音到文本的转换。

应用界面设计

由于界面比较简单,就直接在这里下载初始工程,然后再继续进行完善。


使用 Speech 框架

要使用 Speech 框架,第一件要做的事自然是引入这个框架,并遵循 SFSpeechRecognizerDelegate 协议。所以,我们先引入该框架,然后将它的协议添加到 ViewController.swift 类中。此时 ViewController.swift 应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import UIKit
import Speech
class ViewController: UIViewController, SFSpeechRecognizerDelegate {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var microphoneButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func microphoneTapped(_ sender: AnyObject) {
}
}

用户权限

在使用 Speech 框架进行语音识别之前,你必须先请求用户许可,原因是识别不仅发生在 iOS 设备本地,还需要依赖 Apple 的服务器。具体来说,所有音频数据都会被传输到苹果后台进行处理。因此需要获取用户的权限。
我们将在 ViewDidLoad方法中处理授权。其中包括用户必须允许应用使用的音频输入和语音识别权限。首先,声明一个名为 speechRecognizer 的变量:
private let speechRecognizer = SFSpeechRecognizer(locale: Locale.init(identifier: “en-US”))
然后将 ViewDidLoad 方法修改为下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
override func viewDidLoad() {
super.viewDidLoad()
microphoneButton.isEnabled = false // 在语音识别被激活前,默认设置麦克风按钮为禁用状态
speechRecognizer.delegate = self // 将语音识别中的 delegate 设置为 ViewContriller 中的 self
SFSpeechRecognizer.requestAuthorization{(authStatus) in // 通过调用 SFSpeechRecognizer.requestAuthorization 来请求语音识别权限
var isButtonEnabled = false
switch authStatus{ // 检查验证状态
case .authorized:
isButtonEnabled = true
case .denied:
isButtonEnabled = false
print("User denied access to speech recognition")
case . restricted:
isButtonEnabled = false
print("Speech recognition restricted on this device")
case .notDetermined:
isButtonEnabled = false
print("Speech recognition not yet authorized")
}
OperationQueue.main.addOperation {
self.microphoneButton.isEnabled = isButtonEnabled
}
}
}


提供授权信息

Apple 要求应用为所有请求的权限提供自定义消息,对于语音权限的情况,我们必须为两个行为请求授权:

  1. 麦克风的使用
  2. 语音的识别

要自定义消息,你需要在 info.plist 文件中定义这些消息。

让我们打开 info.plist 文件的源代码。方法是在 info.plist 上点击右键。然后选择 Open As > Source Code。最后,复制下面的 XML 代码并将它们插入到 标签前。

1
2
<key>NSMicrophoneUsageDescription</key> <string>Your microphone will be used to record your speech when you press the &quot;Start Recording&quot; button.</string>
<key>NSSpeechRecognitionUsageDescription</key> <string>Speech recognition will be used to determine which words you speak into this device&apos;s microphone.</string>

已经将两个 key 添加到 info.plist 中了:

NSMicrophoneUsageDescription – 音频输入授权请求的自定义信息。注意,音频输入授权请求只发生在用户点击麦克风按钮的时候。
NSSpeechRecognitionUsageDescription – 语音识别授权请求的自定义信息。

处理语音信息

首先,在 ViewController 中定义下述对象:

1
2
3
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
private var recognitionTask: SFSpeechRecognitionTask?
private let audioEngine = AVAudioEngine()

  1. recognitionRequest 对象用于处理语音识别请求,为语音识别提供音频输入。
  2. recognitionTask可以将识别请求的结果返回给你,它带来了极大的便利,必要时,可以取消或停止任务。
  3. 最后的 audioEngine 是音频引擎。它的存在使得你能够进行音频输入。

接下来,让我们创建一个名为 startRecording() 的新函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
func startRecording() {
// 检查 recognitionTask 的运行状态,如果正在运行,取消任务
if recognitionTask != nil{
recognitionTask?.cancel()
recognitionTask = nil
}
let audioSession = AVAudioSession.sharedInstance() // 创建一个 AVAudioSession 对象为音频录制做准备。
do{
try audioSession.setCategory(AVAudioSessionCategoryRecord)
try audioSession.setMode(AVAudioSessionModeMeasurement)
try audioSession.setActive(true, with: .notifyOthersOnDeactivation)
}catch{
print("audioSession properties weren't set because of an error")
}
// 创建 SFSpeechAudioBufferRecognitionRequest 对象,然后我们就可以利用它将音频数据传输到 Apple 的服务器。
recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
// 检查 audioEngine (你的设备)是否支持音频输入以录音。如果不支持,报一个 fatal error
guard let inputNode = audioEngine.inputNode else{
fatalError("Audio engine has no input node")
}
guard let recognitionRequest = recognitionRequest else{
fatalError("Unable to creat an SFSpeechAudioBufferRecognitionRequest object")
}
// 告诉 recognitionRequest 不要等到录音完成才发送请求,而是在用户说话时一部分一部分发送语音识别数据。
recognitionRequest.shouldReportPartialResults = true
// 在调用 speechRecognizer 的 recognitionTask 函数时开始识别。该函数有一个完成回调函数,每次识别引擎收到输入时都会调用它,在修改当前识别结果,亦或是取消或停止时,返回一个最终记录。
recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest, resultHandler:{(result, error) in
// 定义一个 boolean 变量来表示识别是否已结束。
var isFinal = false
// 倘若结果非空,则设置 textView.text 属性为结果中的最佳记录。同时若为最终结果,将 isFinal 置为 true。
if result != nil {
self.textView.text = result?.bestTranscription.formattedString
isFinal = (result?.isFinal)!
} // 检查 audioEngine (你的设备)是否支持音频输入以录音。如果不支持,报一个 fatal error
// 如果请求没有错误或已经收到最终结果,停止 audioEngine (音频输入),recognitionRequest 和 recognitionTask。同时,将开始录音按钮的状态切换为可用。
if error != nil || isFinal {
self.audioEngine.stop()
inputNode.removeTap(onBus: 0)
self.recognitionRequest = nil
self.recognitionTask = nil
self.microphoneButton.isEnabled = true
}
})
// 向 recognitionRequest 添加一个音频输入。值得留意的是,在 recognitionTask 启动后再添加音频输入完全没有问题。Speech 框架会在添加了音频输入之后立即开始识别任务。
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat){(buffer, when) in
self.recognitionRequest?.append(buffer)
}
// 将 audioEngine 设为准备就绪状态,并启动引擎。
audioEngine.prepare()
do{
try audioEngine.start()
} catch {
print("audioEngine couldn't start because of an error")
}
textView.text = "Say something, I'm listening!"
}

触发语音识别

在创建语音识别任务时,我们首先得确保语音识别的可用性,因此,需要向 ViewController 添加一个 delegate 方法。如果语音识别不可用,或是改变了状态,应随之设置 microphoneButton.enable 属性。针对这个方案,我们实现了 SFSpeechRecognizerDelegate 协议的 availabilityDidChange 方法。详细实现如下所示:

1
2
3
4
5
6
7
8
func speechRecognizer(_ speechRecognizer:SFSpeechRecognizer,
availabilityDidChange available: Bool) {
f available {
microphoneButton.isEnabled = true
} else {
microphoneButton.isEnabled = false
}
}

这个方法会在按钮的可用性改变时被调用。如果语音识别可用,录音按钮也将被启用。
最后,我们还需要更新一下 microphoneTapped(sender:) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 这个函数的用途是检查 audioEngine 是否在运行。
如果正在运行,停止 audioEngine,终止recognitionRequest 的音频输入,禁用 microphoneButton,并将按钮文字改为“开始录音”。
若 audioEngine 正在工作,则应用应该调用 startRecording(),以及设置按钮的文字为“停止录音“*/
@IBAction func microphoneTapped(_ sender: AnyObject) {
if audioEngine.isRunning {
audioEngine.stop()
recognitionRequest?.endAudio()
microphoneButton.isEnabled = false
microphoneButton.setTitle("Start Recording", for: .normal)
} else {
startRecording()
microphoneButton.setTitle("Stop Recording", for: .normal)
}
}

总结

  • 设计图形界面
  • func viewDidLoad() :请求用户许可
  • 将两个 key 添加到 info.plist :提供授权信息
  • fuc startRecording() :处理语音信息
  • func speechRecognizer :确保语音识别的可用性
  • fuc microphoneTapped(sender:) : 启用录音按钮

Speech 框架使用的语音识别框架与 Siri 相同。该 API 虽小,但功能十分强大,为开发者创造像是获取音频文件记录一样震撼的东西提供了极大的便利。

视频资料:
WWDC 2016 的 session 509