如何打造车载语音交互:Google Voice Interaction 给你答案

2,672 阅读8分钟

在某些场景下进行图形交互显得有些困难、甚至危险,比如驾驶汽车。那么在这些场景下可以适当加入语音交互,在解放手眼的同时可以增强安全、避免分心。

概述

语音交互并不是一个新事物,很早就有了。比如 Apple 设备的 Siri、Amazon 的 Alxea、Google 的 Google Assistant 等等。

它们大多是系统的内置服务,由热词唤醒或按键触发,之后只通过语音指令即可完成完整的交互。可这些交互场景往往覆盖了系统服务或系统 App,而对第三方 App 的支持有限或者鲜少针对第三方 App 完成完整的语音交互逻辑。

第三方 App 除了被动等待系统语音服务的调度,当然可以选择主动支持。可是完全依靠自己实现的话,需要考虑监听、识别、理解、分析、调度等诸多复杂逻辑和流程,耗时耗力、可能还入不敷出。

那有没有简单办法来快速切入、试试水呢?

在 Android 生态当中,我们可以选择 Voice Interaction 来完成。Voice Interaction,简称 VI,是 Android 平台特有的语音交互 API,第三方 App 可以通过它来接入系统的语音服务。

这些服务称作 Voice Interaction App,简称 VIA。Android 设备一般都会内置一个或多个 VIA 服务,比如 Pixel 设备默认内置了 Google Assistant、Samsung 设备默认的 Bixby

当第三方 App 接入它们之后,可以便捷地实现一些语音交互功能。比如在删除某项数据的时候,App 可以调度这些服务发起语音提示,并等待用户发出确认或取消的语音指令,其识别之后自动将结果返回回来,App 接棒完成后续的处理。

后面将着重演示如何使用 VI API 在 Pixel 模拟器上调度 Google Assistant 完成几个语音交互的示例。

Confirmation Request

Android 的 Activity 组件提供了发起和停止 VI 调用的方法:startLocalVoiceInteraction() 和 stopLocalVoiceInteraction()。

 class VoiceInteractionActivity: AppCompatActivity() {
     ...
     fun onButtonClick(view: View?) {
         when (view?.id) {
             R.id.btn_confirm->{
                 val bundle = Bundle().apply {
                     putString("name", "Test Voice Interaction")
                 }
                 startLocalVoiceInteraction(bundle)
             }
         }
     }
 }

调用被发起后 Activity 的 onLocalVoiceInteractionStarted() 会被回调,在这里 App 可以获取到向 VIA 请求的入口即 VoiceInteractor

 class VoiceInteractionActivity: AppCompatActivity() {
     ...
     override fun onLocalVoiceInteractionStarted() {
         val request = testConfirmation()
         voiceInteractor.submitRequest(request)
     }
 }

接着可以创建 Request 实例,并使用得到的 VoiceInteractor 向系统发出去。

Request 的类型有很多,比如适用于上面提到的确认交互场景的 ConfirmationRequest。而且为便于用户准确理解,Request 还可以指定友好的提示说明,用 Prompt 实例构建。

 class VoiceInteractionActivity: AppCompatActivity() {
     ...
     private fun testConfirmation(): VoiceInteractor.Request {
         val prompt = VoiceInteractor.Prompt(resources.getString(R.string.vi_confirmation_prompt))
 ​
         return object : VoiceInteractor.ConfirmationRequest(prompt, null) { ... }
     }
 }

系统收到 Request 后会按照提示调用 TTS 进行朗读,并等待用户的后续语音指令,当用户发出不同指令或指令超时的时候,Request 的相应回调将被系统触发:

  • YES:onConfirmationResult() 被回调并且 confirmed 参数为 true
  • NO:onConfirmationResult() 被回调但 confirmed 参数为 false
  • 超时:onCancel() 被回调

这里演示当点击删除 Button 之后,App 通过 VIA 发出询问用户是否要删除该首歌曲的语音提示。用户发出 Yes 之后弹出 Toast 的同时将该首歌曲的 TextView 隐藏。

 class VoiceInteractionActivity: AppCompatActivity() {
     ...
     private fun testConfirmation(): VoiceInteractor.Request {
         val prompt = VoiceInteractor.Prompt(resources.getString(R.string.vi_confirmation_prompt))
 ​
         return object : VoiceInteractor.ConfirmationRequest(prompt, null) {
             override fun onConfirmationResult(confirmed: Boolean, result: Bundle?) {
                 val stringId =
                     if (confirmed) R.string.vi_confirmation_confirmed else R.string.vi_confirmation_cancelled
 ​
                 Toast.makeText(
                     this@VoiceInteractionActivity,
                     stringId,
                     Toast.LENGTH_SHORT
                 ).show()
 ​
                 if (confirmed)
                     confirmTv?.visibility = View.INVISIBLE
 ​
                 stopLocalVoiceInteraction()
             }
 ​
             override fun onCancel() {
                 Toast.makeText(
                     this@VoiceInteractionActivity,
                     R.string.vi_confirmation_timeout,
                     Toast.LENGTH_SHORT
                 ).show()
 ​
                 stopLocalVoiceInteraction()
             }
         }
     }
 }

一开始发现点击 Button 之后没有任何反应:虽然日志上显示 onLocalVoiceInteractionStarted() 能回调,但既没有收到系统的语音提示,发出 YES 或者 NO 也没有收到 Request 的回调。

经过调查发现模拟器的音量和 Microphone 没有打开。

emulator-mic-settings.png

重试之后可以听到系统发出 "Are you sure you want to delete this song?" 的语音提示了,但我发出的指令仍然没有反馈。

在模拟器上打开了 Online Test Mic 发现发出的语音模拟器是能收到的,即麦克风没有问题。那么必然是识别那块除了问题。重新取了日志,果然发现了问题:ASR 识别连接发生了错误,虽然我已经连上了网。

 06-21 22:41:51.307  1506  8756 W ErrorReporter: reportError [type: 211, code: 65561, bug: 0]: errorCode: 65561, engine: 2
 06-21 22:41:51.307  1506  8756 I NetworkRecognitionRnr: Using pair HTTP connection
 06-21 22:41:51.311  1506  7017 I PairHttpConnection: [Upload] Connected
 06-21 22:41:51.317  1506  1990 W CronetNetworkRqstWrppr: Upload request without a content type.
 06-21 22:41:51.324  1506  1972 I S3RecognizerInfoBuilder: S3PreambleType 0

一顿折腾之后,模拟器能够科学上网了,再试果然成功了。

VI_Confirmation.gif

录屏可以看到点击了 "Delete that song" Button 之后,Google Assistant 弹出了 UI 说明,GIF 无法展示,事实上还播放了对应的语音提示。

在此之后,当发出了 “Yes” 的 Voice 之后,被它成功地识别了,并回调了我们的 Delete 逻辑,最终隐藏了目标歌曲。

Pick Option Request

除了借助 VI 帮忙做 YES 或 NO 的判断题,还可以通过 PickOptionRequest 让 VI 帮忙做选择题。发起和回调的处理差不多,区别在于 Request 的部分,需要传入选项 Array

 class VoiceInteractionActivity: AppCompatActivity() {
     ...
     private fun testPickup(): VoiceInteractor.Request {
         val prompt = VoiceInteractor.Prompt(resources.getString(R.string.vi_pick_prompt))
 ​
         val optionList = arrayOf(
             VoiceInteractor.PickOptionRequest.Option(optionsArray[0], 0),
             VoiceInteractor.PickOptionRequest.Option(optionsArray[1],1),
             VoiceInteractor.PickOptionRequest.Option(optionsArray[2], 2)
         )
 ​
         return object : VoiceInteractor.PickOptionRequest(prompt, optionList, null) { ... }
     }
 }

这里模拟一个场景,当驾驶员搜索或者打开歌单出现一堆歌曲的时候,App 可以设计如下流程进行语音选择:

  1. App 将界面内歌曲列表传递给 VIA 让其播报出来,通过语音提示驾驶员
  2. 当驾驶员听到满意的歌名之后,将其念出来
  3. VIA 将自动识别并匹配上其索引,最后回传给 App
  4. 进而 App 可以依据索引直接选择对应歌曲进行播放

另外要注意,选择后有其特有的回调即 onPickOptionResult()。

 class VoiceInteractionActivity: AppCompatActivity() {
     ...
     private fun testPickup(): VoiceInteractor.Request {
         ...
         return object : VoiceInteractor.PickOptionRequest(prompt, optionList, null) {
             override fun onPickOptionResult(
                 finished: Boolean,
                 selections: Array<out Option>?,
                 result: Bundle?
             ) {
                 if (finished && selections?.size == 1) {
                     val index = selections[0].index
 ​
                     Toast.makeText(
                         this@VoiceInteractionActivity,
                         "${resources.getString(R.string.vi_pick_selected_prefix)} ${optionList[index].label}",
                         Toast.LENGTH_SHORT
                     ).show()
 ​
                     var selectedItem: View? = when (index) {
                         0 -> optionTv1
                         1 -> optionTv2
                         2 -> optionTv3
                         else -> { null }
                     }
 ​
                     selectedItem?.isPressed = true
                 }
 ​
                 stopLocalVoiceInteraction()
             }
 ​
             override fun onCancel() {
                 Toast.makeText(
                     this@VoiceInteractionActivity,
                     R.string.vi_confirmation_timeout,
                     Toast.LENGTH_SHORT
                 ).show()
 ​
                 stopLocalVoiceInteraction()
             }
         }
 }

VI_PickOption.gif

可以看到点击 "Choose a song" Button 之后,Google Assistant 弹出了 "Which song do you want?" 的 UI 提示,以及同等的语音提示。

当发出了 “dances with wolves” 的 Speech 之后,它不仅听到了还进行了模糊识别(谁叫自己英语发音不标准呢 😂)并成功回调了 Select 目标 Item 的逻辑。

其他 Request

除了用于确认的 ConfirmationRequest、用于选择的 PickOptionRequest,还有其他 Request:

  • Command Request,用于向 VIA 发送预设的 Command String(比如控制导航、媒体、车辆、通信等特殊 Command),可在 onCommandResult() 里回调,命令执行与否在 isCompleted 参数中体现
  • Complete Voice Request,用于通知 VIA 已经成功通过 Voice Interaction 完成交互逻辑,在 onCompleteResult() 回调里可以关闭 Activity
  • Abort Voice Request,用于通知 VIA 无法通过 Voice Interaction 完成交互,在收到 onAbortResult() 回调里可以开启传统的 UI 操作 Activity 以继续完成交互。

VI Flow

语音交互操作方

如同 AccessibilityService,VIA 的核心服务 VoiceInteractionService 依赖 SystemService 的调度,该服务名为 VoiceInteractionManagerService

在 VIA 设置为 Default Digital Assistant App 之后或重启之后,VoiceInteractionManagerService 会绑定 VIA 的 VoiceInteractionService 并进行 ASR、NLU、NLG、TTS 等服务或 Engine 的初始化,同时开启对 Hotword 的探测。

当 Client App 通过 VI 发出 Request 后,VoiceInteractionManagerService 会绑定 VoiceInteractinoSessionService 并开启一个 VoiceInteractionSession 进行处理。

该 Session 收到具体的的 Request,在展示 UI 的同时会依据传入的 Prompt 文本调用 TTS 进行朗读。之后调用 MediaRecorder 进行录音,并将数据交由 ASR 和 NLU 进入语音识别和语义分析。

当识别到的结果和目标意图符合或模糊匹配上的话,将会回调 Request 的相应 Callback。

注意点

在使用 VI API 实战的时候需要留意如下几点:

  1. 确保麦克风打开
  2. 确保扬声器音量足够大
  3. 确保网络正常,可以下载必要的语音包的
  4. 尽量科学上网,否则可能无法识别语音(虽然我觉得基础指令的解析本可以在本地完成)
  5. 确保设备中存在 VIA 并且设置为默认的 Digital Assistant App(如果设备中没有,可以考虑下载、安装 Google Assistant & Google,并设置为默认 App)

如果在实战过程中发现一些问题,可以查看如下日志以帮助分析失败的原因:

 adb logcat -s GoogleTTSServiceImpl -s VoiceDataDownloader -s VoiceDataManager -s VoiceGenerator -s TextToSpeech -s GoogleTTSService -s GoogleTTSServiceImpl

结语

和语音助手一样,Voice Interaction API 也早就出现了,准确的是在 Android 6 推出的,可是鲜少有朋友了解或使用过。

VI 这套 API 可以免去自行集成 ASR、NLU、NLG、TTS 这些复杂模块的步骤,而且随着 AOSP 的版本升级未来还可以便捷地支持更多功能、无需自行扩展架构。

如果为了体验或者给 App 提供基础的语音交互功能,不妨从接入 VoiceInteraction 开始!当然作为 VI 的实现方 VIA 才是语音交互的精髓,后续将从原理、实战进行更完整地探讨。

参考资料