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

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

概述

语音交互并不是一个新事物,它已经存在很长时间。例如,Apple 设备的 Siri、Amazon 的 Alexa、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 的回调。

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

重试之后可以听到系统发出 “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” 的语音后,被成功识别,并回调了我们的删除逻辑,最终隐藏了目标歌曲。

Pick Option Request

除了借助 VI 来处理 YES 或 NO 的判断,还可以通过 PickOptionRequest 来实现选择题。发起和回调的处理方式类似,区别在于 Request 的部分需要传入选项数组。

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()
            }
        }
    }
}

可以看到点击 “Choose a song” Button 之后,Google Assistant 弹出了 “Which song do you want?” 的 UI 提示和相应的语音提示。

当发出了 “dances with wolves” 的语音后,尽管它进行了模糊识别(可能是我的英语发音不标准 😂),但成功回调了选择目标项的逻辑。

其他 Request

除了用于确认的 ConfirmationRequest 和用于选择的 PickOptionRequest,还有其他请求类型:

  • Command Request:用于向 VIA 发送预设的 Command String(如控制导航、媒体、车辆、通信等特殊命令),可在 onCommandResult() 里回调,命令执行与否在 isCompleted 参数中体现。

  • Complete Voice Request:用于通知 VIA 已经成功通过 Voice Interaction 完成交互逻辑,在 onCompleteResult() 回调中可关闭 Activity。

  • Abort Voice Request:用于通知 VIA 无法通过 Voice Interaction 完成交互,在收到 onAbortResult() 回调后,可以开启传统的 UI 操作 Activity 以继续完成交互。

VI Flow

语音交互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 会绑定 VoiceInteractionSessionService 并开启一个 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 才是语音交互的精髓,后续将从原理、实战进行更完整的探讨。

参考资料

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐