在某些场景下进行图形交互显得有些困难、甚至危险,比如驾驶汽车。那么在这些场景下可以适当加入语音交互,在解放手眼的同时可以增强安全、避免分心。
语音交互并不是一个新事物,很早就有了。比如 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 完成几个语音交互的示例。
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 逻辑,最终隐藏了目标歌曲。
除了借助 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 可以设计如下流程进行语音选择:
另外要注意,选择后有其特有的回调即 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 的逻辑。
除了用于确认的 ConfirmationRequest、用于选择的 PickOptionRequest,还有其他 Request:
onCommandResult
() 里回调,命令执行与否在 isCompleted 参数中体现onCompleteResult
() 回调里可以关闭 ActivityonAbortResult
() 回调里可以开启传统的 UI 操作 Activity 以继续完成交互。如同 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 才是语音交互的精髓,后续将从原理、实战进行更完整地探讨。