亚马逊AWS官方博客

Alexa 车载 SDK 及云端 Skill 集成介绍

序言

Alexa 是亚马逊开发的一种智能语音助手,它可以通过语音识别技术来接收用户的指令并提供相应的服务。用户可以使用 Alexa 控制智能家居设备、听取新闻、播放音乐、设置提醒、查找信息等等。

Alexa 的硬件设备包括 Alexa 智能音箱和 Alexa 智能屏幕等,用户可以通过这些设备与 Alexa 进行交互。此外,Alexa 还可以在智能手机、平板电脑和电视等设备上使用,用户可以通过安装 Alexa App 与 Alexa 云端进行交互。

Alexa 自从 2014 年发布以来到今天,累计已经销售了 4 亿台设备。另外全球现有超过 3 亿台智能家居设备接入了 Alexa,交互数方面也来到了 10 亿次每周。除了上述提到的 Alexa 可以实现的功能以外,亚马逊官方以及第三方提供了超过 10 亿种skill (如订餐、订机票、播放三方音乐,等等),来丰富终端用户的 Alexa 使用体验 (2022 年 8 月统计)。

而在汽车行业中,Alexa 也有很高的占有率。在 2021 年 12 月,Frost & Sullivan 的一项研究中表明,在 46% 的车主心中,Alexa 是他们首选的车载语音助手。而在 Alexa 官方计划中,我们也可以看到如宝马、奥迪、福特、丰田等车企已搭载 Alexa 能力,并将在 23 年以及后续发布的车型中,继续集成 Alexa(请参考链接查看目前支持 Alexa 的全部公开车企案例)。

常见的 Alexa 与汽车集成的方式主要分为车内语音助手和车外家庭远控两种。通常来说车内语音助手需要承担几个关键任务:1. 本地车控,如开关空调、车窗;2.  内容支持,如新闻、音乐、天气查询等; 3. 导航支持,包括最近的加油站、充电站、餐厅等 ;4. 通用查询,针对客户抛出的随机问题给出智能的回答。而上述这些内容,依托于 Alexa 强大的 Speech Language Understanding(SLU)能力,以及完善的内容对接,丰富的三方技能支持,可以方便 OEM 用户快速对接海外生态。而车内语音助手的集成,主要通过在车机操作系统集成 Alexa Auto SDK 来实现。Alexa 将以 Android App 的形式存在在车机内。集成了 Alexa Auto SDK 的车机可以被认为是一台能够接入 Alexa 云端服务的设备,来处理并转发用户通过语音发起的请求到云端。当云端处理请求并返回指令给车机后,车机会根据响应来调用 Alexa Auto SDK 里的不同模块,来执行 Alexa 云端返回的指令。这些指令包括了 Alexa 基础能力,播放音乐,查询天气,车控指令,导航,电话,以及 Alexa 三方 skill。

在车外家庭远控这种场景,车主在家通过语音的方式与 Alexa 音箱发送交互,从而实现车辆电池状态查询,打开车辆预热功能等。这个场景是通过 Alexa Skills 与 OEM 的车联网集成来实现的。Alexa Skills 是一种可由第三方开发者创建的扩展程序(运行在 Amazon Lambda),可以让 Alexa 智能助手实现更多的功能。这些技能可以使 Alexa 与三方服务和设备集成,并使用户可以通过语音指令与 Alexa 进行互动。简单来说,车主具体的指令由 Alexa 云服务根据终端用户的语音指令解析得到并发送到 Lambda。而车厂在 Amazon Lambda 中调用车联网的接口实现控车。在创建了这个 skill 后,任何拥有 Alexa 账号以及音箱设备的终端用户就可以使用这个 skill,实现在家中,通过 Alexa 音箱来控车,查询车辆当前状态信息。另外,Alexa Skills 不光可以应用在车外环境,在车内用户同样可以调用 Alexa Skills 来获得车厂提供的定制化语音助手功能,比如查询车辆使用手册等。

车载语音集成

Alexa Auto SDK 以及其自带的 Demo 应用程序,可以帮助用户快速地构建起和Alexa Voice Service (AVS) 进行交互的车载 Alexa 语音助手。为了方便大家理解Alexa Auto SDK 都有哪些内容,本文先来介绍 AVS,以及 AVS 和客户端侧交互的方法是什么。

什么是 Alexa Voice Service(AVS)?

顾名思义,AVS 是一个语音服务,其最核心功能是接收用户的语音请求,并且返回文字、语音、图片、可交互式界面,甚至视频。

AVS 服务主要支持设备终端通过 Interface(HTTP/2),REST API 等方式接入。REST API 采用常规的 request/response 模式。而 Interface 则相对复杂一些,因为 Interface 不是简单的 request/response 模式。我们可以举两个例子:1. 当用户询问天气如何时,设备终端将语音信息发送到 AVS,即 request。AVS 在查询天气后,会将查询到的信息通过 Text-to-Speech(TTS)转换成 audio 数据返回给设备终端。于此同时,AVS 也会将对应的天气信息以图片的形式返回给设备终端,而这两次的返回,只有 audio 数据包是在 request/response 模型中得以返回,而将天气信息以图片形式传回实际是利用了 HTTP/2 的 server push 能力,从而在不需要 round trip 的情况下,直接将服务端认为的设备终端需要的信息返还。2. 当用户创建了一个定时告警任务,并且绑定了一个集成 Alexa SDK 的设备后(比如音箱或者车机),设置定时告警的任务可以是用户在 Alexa 手机 APP 上完成(底层通过 REST API)。然而设备终端仍然可以通过 interface 收到相应的 response。

通过以上的场景我们可以看到,针对设备终端通过 interface 来和 AVS 进行交互的方式,我们很难用 request/response 的模式进行理解。为了方便大家更容易了解 AVS 的 service model,Alexa 团队抽象出了 Directive/Event 模式。简单来说,event 是设备终端发送给 AVS 的信息,可以是语音的请求,也可以是客户端所在设备的状态上报信息。而对应的,directive 是 AVS 发送给设备终端的指令,可以是基于设备终端的语音请求做出的反馈,也可以是基于 AVS 的信息主动推送给设备终端的指令。这些指令以 HTTP response 的形式发送到设备终端上,由设备终端解析来完成相应的操作,这些操作可能包括播放 audio 文件,调整音量大小,甚至是改变对接 AVS 的 endpoint 等。为了方便理解,我们以一个场景为例,大致描述的过程是用户第一次在设备终端上登录 Alexa,并且在 Alexa 手机 App 上完成告警注册的一系列流程。为了让大家更清楚整个交互流程,所有的流程采用 terminal 或是 Postman 调用 HTTP 的方式进行。

这个场景包括:

  1. 登陆,换 token(OAuth)
  2. 打开一个设备终端到 AVS 的连接,这个连接处于 half-open 状态,需要定期 ping 保持
  3. 设备终端发送 event,确认 endpoint 是否为推荐的服务器地址(event/directive 模式)
  4. AVS 通过 directive 告知,endpoint 不正确,之后设备终端发送 event 要更新 endpoint
  5. 通过 REST API 创建 alarm 并绑定在该设备,返回报错设备没有设置 time zone
  6. 通过 event 的方式,将客户端 time zone 上报
  7. 重新通过 REST API 创建 alarm 并成功
  8. 此时设备终端收到 AVS 发来的 directive,包含明确的 alarm 信息,设备可以根据相关信息,在本地进行配置

现在我们通过在 terminal 中使用命令行的方式完成上述流程,加深我们的理解。首先我们需要注册相应的产品 (链接),将后续命令要用到的 client_id,productID替换为注册的内容,关于注册的详细信息,可以参考配套 workshop

(1)在 terminal 中输入下列命令,模拟 Alexa 登录认证:

curl --location 'https://api.amazon.com/auth/O2/create/codepair' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'response_type=device_code' \
--data-urlencode 'client_id=<STRING> ' \
--data-urlencode 'scope=alexa:all' \
--data-urlencode 'scope_data={"alexa:all":{"productID":"democar","productInstanceAttributes":{"deviceSerialNumber":"12345"}}}'

之后,用户会收到类似的 response,我们需要使用 Login with Amazon(LWA)来完成登陆,从而获得调用 API 的 Access token。

{
    "user_code": "**********",
    "device_code": "***************",
    "interval": 30,
    "verification_uri": "https://amazon.com/us/code",
    "expires_in": 600
}

在登陆完成后,我们继续在 terminal 调用 token 接口,来换取到 Access Token。

curl --location 'https://api.amazon.com/auth/O2/token' \
--header 'Host: api.amazon.com' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=device_code' \
--data-urlencode 'device_code=<device_code>' \
--data-urlencode 'user_code=<user_code>'

以上我们完成了 OAuth 认证的流程。

(2)打开一个 half open 连接

curl  -X GET  -H "Authorization: Bearer <access-token>" https://alexa.fe.gateway.devices.a2z.com/v20160207/directives

会得到如下效果,这个时候我们的 half-open 的连接状态已经打开。如果 AVS 有主动推给客户端的 directive,就会通过这个 connection 发送给客户端。

(3)发送 event,确认访问的 endpoint 是否正确

curl -X POST  --http2 --header "Content-Type: multipart/form-data; boundary=--------abcde123" --header "Authorization: Bearer <access-token>" --data-binary @verifyGateway.txt https://alexa.fe.gateway.devices.a2z.com/v20160207/events

其中 verifyGateway.txt 是 multipart/form-data 上传 body 的一种方式,其中的内容请参考示例。如果我们的 endpoint 是 Alexa 不推荐的接入点后,Alexa 会返回推荐的值,我们后续的连接也需要更改到这个新的接入点。

(4)模拟调用创建 alarm 的 REST API。注意,在第一调用的时候会返回设备终端 timezone 没有设置,所以在下一步我们需要通过客户端发送 event 的方式,将客户端的 timezone 告知给 AVS。

curl --location 'https://api.fe.amazonalexa.com/v1/alerts/alarms' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <token>'\
--data-raw '{
    "endpointId": "@self",
    "trigger": {
        "scheduledTime": "2023-03-12T23:42:00"
    },
    "assets":[
        {
            "type": "TONE",
            "assetId": "123ABC"
        }
    ]
 }'

(5)curl 请求

curl -X POST  --http2 --header "Content-Type: multipart/form-data; boundary=--------abcde123" --header "Authorization: Bearer <access-token>" --data-binary @verifyGateway.txt https://bob-dispatch-prod-na.amazon.com /v20160207/events

data-binary 使用示例

(6)我们再次调用 alarm api,这时我们可以看到正确的返回

(7)在完成了 API call 之后,除了收到 API 的 response 以外,我们在第二步开启的 half-open connection,也会收到来自 AVS 的 directive 推送

至此,我们完成了一套简单的客户端与 AVS 的交互逻辑。

什么是 Alexa Auto SDK?

Alexa Auto SDK 不是简单的 API 或者 interface 的封装,而是一套完整的框架。架构图如下:

这个框架中最核心的内容是 Engine 和 MessageBroker 以及 MessageBroker 使用的一套标准接口 AASB。简单来说,engine 负责和 AVS 通信,用户无需再关注 HTTP2 连接的建立。同时对于一些 AVS 通用的 event/directive,比如 Audio Input 的流转,如果完全通过编写底层 event/directive 的方法实现,需要大量的代码以及自行维护的状态机来实现这个场景,而有了 Alexa Auto SDK 后,这部分工作可以交由 Engine 来负责。

MessageBroker 起到的作用是连接 engine 和客户自定义的 handler。Engine 并不具备处理所有 directive 的能力,因为根据不同的需求场景和设备,同样的 directive 可能在终端可能会有不同的实现。就拿导航相关的 directive 举例,当用户要求:“Alexa, navigate to the nearest restaurant ”,Alexa 会发送包含了餐馆位置信息的 directive 到 Engine,而 Engine 会通过 messageBroker 将这个信息转发给用户编写的 NavigationHandler,在这个 handler 里,用户可以解析 Engine 转发的信息,并结合当前系统使用的导航 App,将相关的位置信息送入导航 App,并且完成 App 跳转等逻辑。具体的 handler 的逻辑我们将在后文中展开。

下面我们通过一段 c++ 伪代码,来说明如何快速使用 engine,messageBroker 来快速实现一个 Alexa 语音助手应用。

    // Create the engine object
    auto engine = aace::core::Engine::create();
    auto messageBroker = engine->getMessageBroker();

    // Audio Player
    auto audioPlayerHandler = alexa::AudioPlayerHandler::create(messageBroker);

    // Speech Recognizer
    auto speechRecognizerHandler =
        alexa::SpeechRecognizerHandler::create(messageBroker); 

    auto carControlHandler = carControl::CarControlHandler::create(messageBroker);

    engine->start();

其中,handler 为用户需要自行开发的类,比如 CarControlHandle,这个类本身写法比较简单。但是在介绍 handler 具体做什么之前,我们先快速拆解 AASB message,即 engine 发送给 messageBroker,最终被 handler 消费的消息。

{
    "header": {
        "version": "4.0",
        "messageType": "Publish",
        "id": {{String}},
        "messageDescription": {
            "topic": "CarControl",
            "action": "AdjustControllerValue"
        }
    },
    "payload": {
        "controllerType": "RANGE",
        "capabilityType": "RANGE",
        "endpointId": “yunfei_car_m3”
        "instanceId": “ac_front”,
        "delta": -3.0
    }
}

上述是一个 CarControl 相关的 AASB message。我们可以看到它和 event/directive 的结构非常相似,但是并不完全一致,这是因为经过 engine 之后,AASB 的数据会相应做一些 enrichment。在这段信息里,messageDescription 中的信息{"topic": "CarControl", "action": "AdjustControllerValue"}决定了被哪个 handler 进行解析。Payload 里的信息决定了 handler 解析信息后,具体需要如何做。以上面的例子为例,这个信息表明,我们的 handler 需要将车辆 yunfeilu_car_m3 的 ac_front(车前的空调)降低 3 度。所以,总结来看,需要用户去编写的 handler 主要有两个任务:

  • 消费 messageBroker 里的信息。至于这个信息如何在不同线程间传递,取决于用户编写的应用运行的平台。目前 Alexa Auto SDK 分别有基于 Android 和 Linux 开发的版本。比如 Android 平台中就使用的是 intent/broadcast receiver 机制,而 Linux 平台采用 C++,messageBroker 是以指针的形式实现消息传递的。
  • 当收到消息后,handler 需要去解析信息,做相应处理,比如对接车端 SDK,做出车控操作。另外有些从 engine 发送到 handler 的 AASB message,需要 handler 返回 response AASB message。这个消息通常被用来结束整个语音控制流程,比如控车失败,Alexa 会返回提示音告知失败,控车成功,Alexa 也会返回简短的提示。要了解不同的 AASB message 具体需要 handler 返回的 message 的类型,请参考链接

根据上述两点介绍,一个 handler 的 C++实现包含:

void CarControlHandler::subscribeToAASBMessages() {
    m_messageBroker->subscribe(
        [=](const std::string& message) { handleAdjustControllerValueMessage(message); },
        AASB_TOPIC_CAR_CONTROL,
        AASB_ACTION_ADJUST_CONTROLLER_VALUE);
}

void CarControlHandler::adjustRangeControllerValue(std::string message) {
    auto value = CarControlDataProvider::getRangeController(message).getValue();

    // TODO: call Cars SDK owned by OEM)

    bool success = true;
    msg.header.messageDescription.replyToId = messageId;
    msg.payload.success = success;

    m_messageBroker->publish(msg.toString());
}

Alexa Auto SDK Android 版本的扩展

上文主要通过 C++的示例介绍了 Alexa Auto SDK,但是在国内 OEM/Tier 1 中,绝大多数的车机使用 Android。不过这并不意味着上述内容不适合国内厂商。这是因为 Alexa Auto SDK android 版本运用了大量的 JNI 接口,比如前文提到的 Alexa Auto SDK 的核心组件 Engine,在 Android 版 SDK 中就是直接通过 JNI 调用,而没有再编写对应的 Java 版本。除此之外,我们可以看到,Android 版本的 SDK 在原有的 C++版本 SDK 的基础上,将 engine,broker 能力和 Android 的原生库比如 android.media 等打包,并且命名为一个新的服务 Alexa Auto Client Service (AACS) 。这个服务运行在车机上面,通过 broadcastReceiver 和 Intent 的机制来将 Alexa 功能和 Android 系统上的其他应用或者接口进行连接。这种模块化的设计,更方便我们快速迭代 Alexa 客户端的能力。下图我们可以看到,利用 AACS 的能力,客户可以通过尽量少的开发,实现完整的语音助手的能力。

如何快速体验 Alexa Auto SDK(Android 版)?

Alexa 官方提供了一个基于 AACS 构建的 Sample App,方便用户快速体验 Alexa 运行在车机上的效果,官方代码链接可以参考 alexa-auto-sdk。因为该 Sample App 涉及到 JNI,所以编译起来相对繁琐。总结以下 build 文件,方便新用户借鉴参考  buildspec.yaml (该 build 文件在 Alexa Auto SDK 4.1.1 版本进行测试)。

另外我们也可以通过 Alexa 中提供的工具先 build 相应的 Alexa Auto SDK 的 aar,然后再将 aar 文件导入 Android Studio 中。这样再后续的开发中,我们就可以使用 Android Studio 来完成一系列调试,编译工作。具体步骤:

(1)在 terminal 中输入

git clone https://github.com/alexa/alexa-auto-sdk.git
cd alexa-auto-sdk
./builder/build.py -p android -a x86_64
tar -xvzf /home/ubuntu/Projects/alexa-auto-sdk/builder/deploy/aac-dev-android_x86_64-release-**********.tgz

执行完毕,解压输出的 archive 文件,在 aac*/aar 文件夹下就可以得到编译 Sample App 所需要的 aar。


(2)复制所有 aar 文件到 aacs/android/service/core-service/libs

(3)寻找亚马逊解决方案架构师获得 aar(源码不开源,需要 Amazon 提供),将其放入 aacs/android/app-components/alexa-auto-voice-ui/libs

(4)在文件 aacs/android/sample-app/alexa-auto-app/gradle.properties 的末尾添加 skipDeps=true

(5)替换 aacs/android/sample-app/alexa-auto-app/src/main/assets/config/aacs_config.json 为文件,注意替换 clientId 与 productId

完成上述步骤后,我们可以打开 Android Studio,点击 open aacs/android/sample-app。

打开后,Android Studio 会开始 index 所有相关的 module。当 indexing 结束后,我们可以点击绿色箭头运行 apk(需要注意提前创建一个 android emulator)。按照 UI 的步骤提示,在完成设置 Alexa 为默认 voice assistant,以及登录 Amazon 账号,即可体验 Alexa。


Alexa Auto Sample App 集成实战总结

在 Android Studio 的模拟器上或者是 Android 手机/平板上, Sample App 通常会顺利运行。但是在集成到真实的车机系统时,我们总会遇到各种各样的问题。有些问题是由于 OEM 的 Android 系统会基于标准安卓系统进行裁剪,而 Sample App又基于这些被裁剪掉的 Android 标准库。有些问题是因为安全的考量,针对和客户的车控/车态 SDK 集成的场景,当应用发生修改重新安装到车机上时,需要进行系统签名,而在实际开发过程中,针对和车辆密切结合的部分,比如 CarControl 模块,我们需要持续的更新调试代码,频繁的申请系统签名(通常是跨 team,涉及到 Tier 1 与 OEM 合作的场景需要跨公司)这极大的降低了开发的速度。经过和多家 OEM 实施 Alexa 集成的过程中,我们总结出了一些解决办法和实践:

(1)重新安装某些被裁剪的库,比如 Settings 库等。

(2)针对一些 OEM 厂商要求原生系统不能做修改的情况,比如默认语音助手不可修改,我们通过增加桥接层的形式来实现 Alexa App 和车机系统的集成,具体的实现逻辑,可以参考代码

(3)对于应用调试过程中,需要频繁的签名的问题,我们发现类似的签名需求往往是一些对安全性比较高的 SDK,比如车控/车态查询 SDK。基于此场景,我们提出了一种新的软件架构方法,通过在车机系统中预埋一个 app,这个 app 作为 MessageBroker(区别于上述提到的 MessageBroker),来桥接车控 SDK 和 Alexa App。同时这个 MessageBroker 也可以承接和云端通信的任务,比如将从车控/车态 SDK 拿到的数据通过 AWS IoT Core 发送到云端。因为车控/车态 SDK 接口相对固定,上云的数据可以 raw data 的形式发送到云上再做处理,所以 MessageBroker 的修改频率是可控的。这种模式下,用户无需受到系统签名的制约,则可以快速小步地迭代 Alexa Auto App。具体的架构可以参考:

云端 Skill 集成

除了在座舱内集成 Alexa 以外,车厂也可以在云端开发 Alexa Skill 来实现通过手机或者 Alexa 智能音箱远程控车的场景。Alexa Skill 主要包含两部分:1. 交互模型 2. 后端代码。其中,交互模型是一系列规则,指定 Alexa 如何从输入语音中得到需要的信息,用户需要针对每一个 intent(意图)提供适量的语料,并且针对于语料的 entity(槽值)也要提供相应可能的内容,下图中截取部分交互模型中的配置文件作为参考。


后端代码主要运行在 Amazon Lambda 上,处理 Alexa Skill 解析出的 intent 和 entity。例如,客户希望在家里通过语音的方式控制车辆的空调,“Alexa, ask my car to turn on the ac”,此时 Alexa Skill 会解析出 intent: CarCtrlAirCondPwrIntent和entity:AC_PWR_ON,并交给 Lambda 处理,在 Lambda 中,我们可以自由编写逻辑,如将操作连接车联网 API,或者是发送消息给车联网数据网关如 AWS IoT Core。本文为了验证方便,在后续的动手实验部分,使用 DynamoDB 存储键值的形式,来记录车辆“状态”以达到验证的目的。

另外还需要注意的是,通常车联网相关的 skill,需要车厂和 Alexa 完成 account linking,即建立 OAuth flow,具体实现的内容如下图。通过此种方式,用户的信息,比如车辆的 vin 号,可以安全的送入 Lambda 内做进一步处理。本篇 blog 使用 Login With Amazon(LWA)代替车厂的 OAuth,完成 Account Linking 配置的验证。下图是一个常见的 Account Linking 流程。

介绍了完整的流程,我们进行一个简单的动手实验,帮助我们快速构建一个 Alexa Skill。

(1)首先我们部署相应 AWS 资源,这里我们用到 AWS 的 IaC 工具,CDK,注意替换 ACCOUNT-NUMBER 为自己的 AWS account id。

(terminal)

npm -g install typescript
npm install -g aws-cdk
cdk bootstrap aws://ACCOUNT-NUMBER/us-east-1
git clone https://github.com/aws-samples/alexa-auto-custom-integration.git
npm install
cdk deploy

(2)在本地创建对应的库包,并且上传到 Lambda Layer

mkdir python  && pip3 install --target ./python ask_sdk_core ask_sdk_model
zip -r python.zip python

(3)添加 Lambda Layer 到对应的 Lambda Function

(4)添加触发器,关联 Alexa skill

(5)在 Alexa Skill 侧使用交互模型

(6)创建 Login With Amazon,并且链接 Alexa Skill,具体操作请参考链接

(7)在创建好的 DynamoDB user_table 中添加记录,用来模拟用户和车辆 vin 号的信息绑定 。注意这里的信息 email_address 需要和我们最终绑定 Alexa 的亚马逊账号的邮箱保持一致。

(8)在创建好的 DynamoDB car_status_table 中添加记录,用来模拟车辆的相关数据。

(9)完成创建后,通过 Alexa 手机 App 完成 account linking 动作。在 Alexa console 页面进行测试,并且查看 DynamoDB 状态是否发生更改。

总结

因篇幅有限,在有些细节上面,本文可能没有足够的截图支撑,如果想了解更多操作细节,可以参考配套的workshop

参考

  1. Alexa 官方文档 https://developer.amazon.com/en-US/blogs/alexa

本篇作者

陆云飞

亚马逊云科技解决方案架构师。主要专注汽车行业解决方案的落地,如 Alexa 生态的技术落地,车联网的设计实施等。他拥有丰富的开发经验,曾参与 Amazon EC2 等核心产品及系统的研发及上线工作,对 DevOps 有深入的理解。

黄晟劼

亚马逊云科技生态解决方案经理。专注汽车行业解决方案结合 Amazon 生态内容的落地,结合智能座舱,AWS 云服务以及用户体验升级,帮助整车企业打造符合海外用户本地化需求的智能座舱产品。

陈岑

博泰资深研发工程师,拥有 10 年+ Android 系统开发经验。曾服务过国内多家自主品牌,合资品牌车企,助理智能座舱相关项目的落地。