iOS 10 by Tutorials 笔记(七)

Chapter 7: Speech Recognition

iOS 10 新的语音识别 API 允许实时或将预先录制好的音频转换成文字模式。它利用和 Siri、键盘听写相同的语音识别引擎,并且提供了更为强大的功能。

这个识别引擎的准确度和速度都非常厉害,支持超过 50 种语言。甚至可以结合你机器设备中的用户信息来生成特定的结果。

本章将构建一个叫做 Gangstribe 的应用,将一些录制好的饶舌音频文件转换成文字,也能实时检测我们说话时的情绪关键词,并将其转换成 emojis 表情包。

本章实时录音使用了 AVAudioEngine,如果不熟悉建议推荐去看看 WWDC 的最佳实践

Speech Recognition 框架不支持模拟器,必须真机调试。而且识别请求是会发送给苹果服务器,远端识别完成后才返回结果给客户端,并不是在本机上完成。

真机运行下本章 Demo,熟悉下界面 点击导航栏右上角的 Face Replace 按钮,选择一个 emoji 表情,应用会用此表情帖到我们的脸上

打开工程熟悉下基本结构

  • MasterViewController.swift 以 TableView 展示了播放列表。每个音频文件的 model 对象定义在 Recording.swift
  • RecordingViewController.swift 播放选定音频文件的控制器,通过实现 handleTranscribeButtonTapped(_:) 方法来开启音频转换
  • LiveTranscribeViewController.swift 用来处理换脸(表情包的叠加)操作,主要利用 FaceReplace 文件夹中的代码,它会展示实时影像,然后用选中的表情包自动叠加到影像中的人脸上。你将在这里添加记录和转换音频的代码
  • FaceReplace 包含了用表情包实时替换人脸的库文件,主要使用了 Core ImageCIDetector,如果你对此感兴趣,可以去查看官方文档

本章前半部分先来实现将预先录制好的音频文件转换成文字,后半部分专注语音控制表情包换脸操作。

Transcription basics

语音识别有四个主要的参与者

  1. SFSpeechRecognizer 最主要的控制器,负责生成识别任务,然后返回结果
  2. SFSpeechRecognitionRequest 识别请求,两种类型
    • SFSpeechURLRecognitionRequest 一种来自音频文件
    • SFSpeechAudioBufferRecognitionRequest 一种来自 buffer
  3. SFSpeechRecognitionTask 请求启动时的任务,用于追踪转换状态,可以取消
  4. SFSpeechRecognitionResult 包含转换的结果

转换的代码也非常简单:

let request = SFSpeechURLRecognitionRequest(url: url)  
SFSpeechRecognizer()?.recognitionTask(with: request) { (result, _) in  
  if let transcription = result?.bestTranscription {
    print("\(transcription.formattedString)")
  }
}

SFSpeechRecognizer 根据 SFSpeechURLRecognitionRequest 请求开启了一个 SFSpeechRecognitionTask 任务(通过 recognitionTask(with:resultHandler:) 方法),最后在结果回调中,我们使用了 bestTranscription 属性,这是经过多次迭代累积找出的最优值。

Audio file speech transcription

在开始读取用户音频并发送给苹果的转换服务器前,我们需要申请用户权限,这也符合苹果的一贯作风。打开 RecordingViewController.swifthandleTranscribeButtonTapped(_:) 中添加下面代码

import Speech

SFSpeechRecognizer.requestAuthorization {  
  [unowned self] (authStatus) in
  switch authStatus {
  case .authorized:
    if let recording = self.recording {
      //TODO: Kick off the transcription
    }
  case .denied:
    print("Speech recognition authorization denied")
  case .restricted:
    print("Not available on this device")
  case .notDetermined:
    print("Not determined")
  }
}

同样的在 Info.plist 中添加对应的 key Privacy - Speech Recognition,在鉴权窗口中显示给用户的请求说明:"I want to write down everything you say"

选择一个音频,然后点击 Transcribe 按钮

下面来实现具体的音频转换

Transcribing the file

回到 RecordingViewController.swift 添加一个转换方法,它接收一个 url 参数

fileprivate func transcribeFile(url: URL) {  
// 1 SFSpeechRecognizer 初始化是否成功
  guard let recognizer = SFSpeechRecognizer() else {
    print("Speech recognition not available for specified locale")
    return
  }
  if !recognizer.isAvailable {
    print("Speech recognition not currently available")
    return
  }
  // 2 更新 UI 动画
  updateUIForTranscriptionInProgress()
  let request = SFSpeechURLRecognitionRequest(url: url)
  // 3 执行转换过程
  recognizer.recognitionTask(with: request) {
    [unowned self] (result, error) in
    guard let result = result else {
    print("There was an error transcribing that file")
    return
  }
  // 4 转换完了再次更新 UI
  if result.isFinal {
    self.updateUIWithCompletedTranscription(
      result.bestTranscription.formattedString)
    }
  } 
}

在之前的 handleTranscribeButtonTapped(_:) 方法中调用上面实现的转换方法(替换 //TODO:

self.transcribeFile(url: recording.audio)  

再次运行,选择一个音频文件,点击 Transcribe 按钮,等菊花转一会就能看到转换结果了

Transcription and locales

转换的结果还不错,但 Speech Recognition 还支持对一个音频文件结合其区域(Locale)特性进行转换,比如设备语言为英文,但想要转换中文语音文件,就要传入中国的 Locale 参数。

我们可以修改之前的 transcribeFile 方法,增加一个 Locale 类型的参数

fileprivate func transcribeFile(url: URL, locale: Locale?) {  
    let locale = locale ?? Locale.current
    guard let recognizer = SFSpeechRecognizer(locale: locale) else {
      print("Speech recognition not available for specified locale")
      return
    }
    ......

点击按钮调用时 handleTranscribeButtonTapped(_:),也别忘了修改过来

self.transcribeFile(url: recording.audio, locale: recording.locale)  

我们试着把一首泰文歌转成成文字版本

Live speech recognition

实时识别转换其实和将文件转换成文字类似,不同之处在于:不同的请求类型。实时识别使用的是 SFSpeechAudioBufferRecognitionRequest。正如其名,这次是从缓存 buffer 中读取音频流。

你的任务是为这个音频 buffer 添加一个源,一旦连通,转换过程就开始了。另一个需要考虑的就是如何停止转换,这就又需要 SFSpeechRecognitionTask 来跟踪状态,在适当实时语音结束后停止转换。

关于这个 Gangstribe 应用,我们还要添加一项很酷的功能,就是根据用户实时的口头表述,识别情绪关键字,并用对应的 emoji 表情包来替换实时视频中的用户头像,这里用到了 FaceReplace 这个库

Connect to the audio buffer

首先我们来配置 audio engine,然后链接识别请求。同样前面一样的,在开始转换前需要鉴权。打开 LiveTranscribeViewController.swift 在 viewDidLoad() 中请求权限:

SFSpeechRecognizer.requestAuthorization {  
  [unowned self] (authStatus) in
  switch authStatus {
  case .authorized:
    self.startRecording()
  case .denied:
    print("Speech recognition authorization denied")
  case .restricted:
    print("Not available on this device")
  case .notDetermined:
    print("Not determined")
  }
}

鉴权通过后才开始音频录制

LiveTranscribeViewController 中添加四个属性

let audioEngine = AVAudioEngine()  
let speechRecognizer = SFSpeechRecognizer()  
let request = SFSpeechAudioBufferRecognitionRequest()  
var recognitionTask: SFSpeechRecognitionTask?  

这里重点说明下 AVAudioEngine,它定义了一组连接着的 AVAudioNode 对象,你使用这些 audio nodes 用来生成音频信号,并可以处理它们,也能执行输入和输出操作。简单来说这个 audioEngine 从麦克风处理输入的音频流。

具体来看下音频录制的代码,找到 startRecording() 方法

// 1 获得和麦克风相关的输入音频节点(node),以及它的输入格式
guard let node = audioEngine.inputNode else {  
  print("Couldn't get an input node!")
return  
}
let recordingFormat = node.outputFormat(forBus: 0)  
// 2 在输出 bus 的 node 上安装一个水龙头,音频格式保持不变;
// 缓存 buffer 填满后,会将其封装为一个闭包的参数返回,在闭包中将装满音频数据的 buffer 添加到
// SFSpeechAudioBufferRecognitionRequest 中,这样就完成了请求和输入流的绑定
node.installTap(onBus: 0, bufferSize: 1024,  
              format: recordingFormat) { [unowned self]
self.request.append(buffer)  
  (buffer, _) in
}
// 3 准备好并开始录制音频
audioEngine.prepare()  
try audioEngine.start()  

很像是给音频源接了个水龙头将水引流到一个蓄水池,池子满了以后再通过回调方法,将水引到我们需要用水的地方去

因为启动 audio engine (audioEngine.start())可能会抛出异常,我们修改下 startRecording 的定义

fileprivate func startRecording() throws {  

然后调用它时也做一些错误处理

do {  
  try self.startRecording()
} catch let error {
  print("There was a problem starting recording: \(error.localizedDescription)")
}

最后一步别忘了请求麦克风的权限,框架已经替你做了。但还需要给 Info.plist 里加一个 key

这样一载入 LiveTranscribeViewController 的界面,就会开始记录用户的语音,接下来继续在 startRecording() 的底部添加将语音转成文字的代码

recognitionTask = speechRecognizer?.recognitionTask(with: request) {  
  [unowned self]
  (result, _) in
  if let transcription = result?.bestTranscription {
    self.transcriptionOutputLabel.text = transcription.formattedString
  }
}

我们尝试停留在这个界面,并开始录音稍长一点时间,应用就崩溃了,这是因为苹果做了限制,只允许实时转换不超过一分钟的实时语音,毕竟这是要发送到服务器去做计算的。这样我们需要适时停止录音:

fileprivate func stopRecording() {  
  audioEngine.stop()
  request.endAudio()
  recognitionTask?.cancel()
}

我们希望按下 Done 按钮 Dissmiss 此 VC 时,停止录制。所以放到 handleDoneTapped(_:) 方法中

stopRecording()  

Transcription segments

最后我们来实现语音控制表情包实时替换人脸。首先要理解返回的转换结果 SFSpeechRecognitionResult 中包含的 SFTranscription 对象。

SFSpeechRecognitionResult 有两个属性:

  • SFTranscription 的数组,里面存放了所有识别的结果,按照评分等级排序(得分高的排在前面)
  • bestTranscriptionSFTranscription 对象)根据评分得到的最优结果

SFTranscription 对象有一个 segments 属性,他是一个包含了 SFTranscriptionSegment 对象的数组,当我们对着麦克风说话时,每个 SFTranscriptionSegment 对象其实对应了转换后的一个单词,只不过它包含了更丰富的信息,比如每个单词的时间戳等。

举个例子,比如我们说一句话:"What time is it",那么第一个 segment 就是 "What",它的 substringRange 值是 {0,4},duration 是 0.6 秒,timestamp 是 1.0。可以解释为 "What" 开始于整句话的位置 0,并占了四个字符的位置(长度为 4),说这个单词花了 0.6 秒。时间戳 timestamp 一般是用来判断次序的,也可以和 duration 一起使用,比如第二个单词 "time" 的时间戳 timestamp 是 1.6,那么我们就知道在说完第一个单词 "What" 之后,紧接着说了 "time" 这个单词。

所以要实现语音控制表情包实时替换人脸,需要拆分用户从麦克风输入的语音,找出对应的关键词。

回到工程中来,首先定义一个持续时间属性

var mostRecentlyProcessedSegmentDuration: TimeInterval = 0  

因为每次我们录制音频都是从头开始的,在 startRecording() 的一开始,将其置 0

mostRecentlyProcessedSegmentDuration = 0  

添加语音控制表情包实时替换人脸的核心方法

fileprivate func updateUIWithTranscription(_ transcription:  
SFTranscription) {  
  self.transcriptionOutputLabel.text = transcription.formattedString

  if let lastSegmen
t = transcription.segments.last,  
    lastSegment.duration > mostRecentlyProcessedSegmentDuration {
    mostRecentlyProcessedSegmentDuration = lastSegment.duration
    // 调用 Face Replace 框架,根据传入的 emoji 字符串替换
    faceSource.selectFace(lastSegment.substring)
  } 
}

上面的方法其实很简单,找出最后一个单词,然后将其对应的字符串传给 FaceReplace 框架的 faceSource.selectFace 方法即可

运行一下,我们尝试说一句:"Don't make me cry",就会看到一个哭脸表情覆盖到了人脸上面。

这里有个限制,关于情绪单词必须放在一句话的末尾,因为我们简单粗暴地传入的是最后一个单词 transcription.segments.last

Usage guidelines

因为 Speech recognition 是一个基于网络的服务,所以苹果还是做了一些限制:

  • 每台设备每天都会有限制
  • 每个 App 每天都会有限制(这是全局限制,针对所有用户加起来的总量)
  • 每次语音识别只能持续一分钟

-EOF-

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!