在某些场景下进行图形交互显得有些困难、甚至危险,比如驾驶汽车。那么在这些场景下可以适当加入语音交互,在解放手眼的同时可以增强安全、避免分心。
概述
语音交互并不是一个新事物,很早就有了。比如 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 没有打开。
重试之后可以听到系统发出 “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
一顿折腾之后,模拟器能够科学上网了,再试果然成功了。
录屏可以看到点击了 “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 可以设计如下流程进行语音选择:
App 将界面内歌曲列表传递给 VIA 让其播报出来,通过语音提示驾驶员
当驾驶员听到满意的歌名之后,将其念出来
VIA 将自动识别并匹配上其索引,最后回传给 App
进而 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() } } }
可以看到点击 “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 实战的时候需要留意如下几点:
确保麦克风打开
确保扬声器音量足够大
确保网络正常,可以下载必要的语音包的
尽量科学上网,否则可能无法识别语音(虽然我觉得基础指令的解析本可以在本地完成)
确保设备中存在 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 才是语音交互的精髓,后续将从原理、实战进行更完整地探讨。