ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android语音唤醒接入指南:讯飞AIKit集成实战与踩坑记录

Android语音唤醒接入指南:讯飞AIKit集成实战与踩坑记录 简介面向安卓开发者的科大讯飞AIKit语音唤醒功能完整工程资源解决在Android Studio中从零接入AIKit SDK、配置唤醒词并调通语音唤醒的难题适合初学语音交互或需要快速落地唤醒功能的移动端工程师。资源包共647个文件压缩后45.56MB以xml/flat布局与配置、java/gradle源码与构建脚本、jar/aar/dex/so依赖库及动态库、json配置和ivw唤醒词资源为主并附带可直接安装调试的apk文件基本覆盖从工程构建到真机运行的全过程。已有709人学习下载。开发者拿到后可导入Android Studio直接编译体验参考其中对麦克风权限、初始化流程、唤醒词属性配置及误唤醒处理等关键环节的写法能帮助系统理解AIKit语音唤醒的集成思路并快速迁移到自有应用。 做语音唤醒功能这件事最早我以为很简单——麦克风监听识别到关键词就触发。真上手之后才发现从能识别到稳定唤醒之间隔着一条巨大的鸿沟。我这边的项目是用Android Studio做客户端需要用户把App退到后台后喊一句自定义唤醒词就能把应用重新拉起来。对比了一圈方案最后选了科大讯飞的AIKit语音唤醒原因很直接支持离线识别唤醒词可自定义SDK足够干净适合做纯净版集成。这篇文章就记录我从零到一接入AIKit语音唤醒的完整过程包括SDK结构、核心代码、以及几个我实际踩过之后才想明白的坑。1. 为什么选讯飞AIKit来做语音唤醒而不是自研或其它开源方案1.1 我实际遇到的场景和需求项目是一个工具类App用户使用频率很高但不会一直停在前台。产品提的需求是用户在任何界面甚至锁屏附近说一句你好小X应用就能从后台拉起自动进入语音指令模式。刚开始我想得比较简单找个开源唤醒方案跑通就行。但真正调研之后发现语音唤醒这个功能看起来是SDK的事实际上是一堆工程细节的事。候选方案大致有三种自己训练一个唤醒词模型然后移植到端侧。优点是完全可控、免费缺点是训练数据、模型压缩、端点检测、误唤醒调优工作量巨大。以我们团队的人力这个方案直接被否了。用Android原生的VoiceInteractionService。这玩意儿和系统绑定得很深需要做系统级应用普通第三方App很难把它当做一个独立的唤醒能力来用。用讯飞AIKit的语音唤醒模块。支持离线、支持自定义唤醒词、SDK按能力拆分、集成成本可控而且老MSC时代的资料虽然乱但AIKit版本的结构相对清爽很多。最终选择讯飞AIKit不是因为它功能最全而是它在这个需求场景下的综合成本最低。所谓纯净版最新版语音唤醒功能我的理解是只用最新SDK里跟唤醒相关的核心模块不把官方Demo里的示例UI、辅助管理类全部搬进工程保持代码干净未来维护起来也轻松。1.2 AIKit和传统讯飞SDK相比清爽在哪第一次接触讯飞的语音能力很容易被他们的SDK版本绕晕。老一代的MSC SDK把语音识别、合成、唤醒、语义理解全部揉在一个jar里体积大初始化逻辑耦合比较紧。后来讯飞推出AIKit强调的是按能力下载、按能力接入语音唤醒是独立模块SDK体积更小配置也更直观。这个按能力接入的特性正好和纯净版诉求匹配。你可以只集成唤醒模块不引入识别和合成的多余依赖依赖关系一目了然。我在集成时只保留了唤醒SDK对应的jar和so工程里没有出现官方Demo那一堆用不上的Activity和工具类。2. 开工之前先把SDK结构和环境准备理清楚2.1 申请AppID和开通语音唤醒服务不管用讯飞哪个SDK第一步都一样注册开放平台账号、创建应用。需要注意的是新建应用时平台会让你填写Android包名这个包名必须和Android Studio工程里的applicationId完全一致否则初始化时会报鉴权错误。创建完应用后还需要单独开通语音唤醒能力。部分能力是默认开通的但语音唤醒涉及唤醒词管理和离线资源需要在服务列表里手动添加。如果没开通后边代码跑起来会提示没有权限或返回错误码。我之前就吃过这个亏代码逻辑怎么查都没问题最后发现是服务没开。2.2 下载SDK与资源解压在平台的SDK下载页面勾选Android平台、语音唤醒模块会生成一个zip包。解压后大致是这个结构具体版本会有差异libs/目录下包含jar文件通常名字类似AiKitWakeuper.jar或MSC.jarlibs/armeabi-v7a、libs/arm64-v8a等目录下是一堆so文件比如libAIVad.so、libmsc.so、libwakeuper.soassets/目录下有唤醒词资源文件.bin或.jet格式的模型文件这一步最容易出问题的是so文件放错位置。Android Studio原生通过src/main/jniLibs/目录加载so所以我会把解压后的armeabi-v7a、arm64-v8a等目录整体复制到src/main/jniLibs/下。只保留真机需要的ABI不需要的架构不复制避免APK体积膨胀。2.3 Gradle和Manifest的基础配置在app/build.gradle里把jar包放进libs目录并通过依赖引用implementation fileTree(dir: libs, include: [*.jar]) android { defaultConfig { ndk { abiFilters arm64-v8a, armeabi-v7a } } }abiFilters这块值得多说一句。如果不加这行Gradle有可能把工程里所有依赖库的所有ABI都打进去常见问题就是另一个库的so和讯飞的so冲突或者编译时报重复的so。我这边只保留两种主流ABI开发和发布都比较稳定。Manifest里需要声明录音、网络等权限uses-permission android:nameandroid.permission.RECORD_AUDIO/ uses-permission android:nameandroid.permission.INTERNET/ uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE/ uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE/ uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE/注意RECORD_AUDIO在Android 6.0及以上属于危险权限光写在Manifest里不够运行时还需要动态申请。我第一次只加Manifest忘记动态申请结果代码不报错但就是不进唤醒回调排查了半天才发现是权限问题。3. 核心唤醒链路拆解与代码落地3.1 初始化引擎放在Application里做讯飞SDK的通用初始化方式是通过SpeechUtility.createUtility传入AppID。这一步我强烈建议放在自定义Application的onCreate里而不是放Activity。否则页面每次重建都有可能重复初始化浪费资源不说偶尔还会导致状态错乱。public class WakeApp extends Application { Override public void onCreate() { super.onCreate(); SpeechUtility.createUtility(this, SpeechConstant.APPID 你的AppID); } }如果初始化报错优先检查两件事平台应用里填的包名是否和工程一致是否在平台开启了语音唤醒服务。这两个问题占了排查链路的80%。3.2 加载唤醒词默认唤醒词与自定义词包讯飞唤醒SDK默认带了一个唤醒词不同版本可能不同一般是讯飞语点。如果产品不介意用默认词直接启动监听就能工作。但大多数场景下我们希望自定义唤醒词比如你好小X。自定义唤醒词需要去讯飞开放平台的语音唤醒页面训练模型。流程大致是上传或录制对应的唤醒词音频平台自动训练生成词包文件然后在SDK下载页或管理后台下载。下载后放进工程的assets目录再在初始化时告诉SDK词包位置SpeechUtility.getUtility().setParameter( SpeechConstant.IVW_RES_PATH, assets:/wake/wake.bin);代码里assets:/前缀是讯飞SDK约定的写法表示从assets目录读取。路径写错的话现象通常不是崩溃而是唤醒没反应这个比较难排查。建议先把文件路径打印到日志里核对别凭感觉写。3.3 创建Wakeuper并监听唤醒结果唤醒的核心类是Wakeuper不同SDK版本的包名可能不同我当前用的AIKit版本import路径类似com.iflytek.aikit.api.Wakeuper。创建实例、注册监听、启动监听三段式操作Wakeuper wakeuper Wakeuper.createWakeuper(this, new WakeuperListener() { Override public void onResult(WakeuperResult result) { // 唤醒成功 String wakeWord result.getWord(); Log.d(WakeupDemo, 唤醒词: wakeWord); runOnUiThread(() - { // 在这里跳转页面或触发业务逻辑 }); } Override public void onError(int errorCode) { Log.e(WakeupDemo, 唤醒错误: errorCode); } Override public void onBeginOfSpeech() { // 检测到语音开始 } Override public void onVolumeChanged(int volume) { // 音量变化常用于绘制音频波形 } }); wakeuper.startListening(null);这里有一个容易被忽略的细节startListening是一次性还是持续监听我的理解是讯飞唤醒默认走一次唤醒一次回调机制。唤醒成功回调onResult之后需要再次调用startListening才能监听下一次唤醒。如果希望一直处于可唤醒状态就在onResult里重新启动监听同时做好防抖处理避免连续触发。3.4 生命周期管理与资源释放Activity或Service销毁时一定要释放唤醒实例否则会一直占着麦克风导致其它应用录音异常Override protected void onDestroy() { if (wakeuper ! null) { wakeuper.stopListening(); wakeuper.destroy(); wakeuper null; } super.onDestroy(); }如果你的应用需要长期在后台监听建议把监听逻辑放到前台Service里而不是Activity。Activity一旦被系统回收监听就断了那不叫真正意义上的语音唤醒。4. 踩坑实录几个把我按在地上摩擦的问题4.1 初始化不报错但唤醒词永远不命中这是最头疼的一个坑。代码没异常音量回调也有但喊破喉咙就是不触发onResult。排查了一圈最后发现是唤醒词资源没生效。我当时把官方Demo里的唤醒词文件拷到了assets目录但文件名和初始化时设置的路径不对应SDK静默失败。遇到唤醒不命中建议按这个顺序排查先打印SpeechUtility.getUtility()是否为null为null就是初始化失败。打印你设置的IVW_RES_PATH确认assets目录下真实存在这个文件。关掉混淆后重新测试排除混淆影响。4.2 签名和包名不匹配导致鉴权失败讯飞SDK不少错误码都跟鉴权有关。比较常见的场景是本地用Android Studio默认的debug签名测试但平台配置的是release包名和release签名信息。虽然包名看着一样平台实际上会校验签名信息本地签名和后台登记的不一致照样拒绝服务。解决方式是在平台应用里把debug和release两种签名信息都登记上或者统一用正式签名再用keytool命令导出的SHA1值去平台后台绑定。测试阶段最省事的是先绑定debug签名的SHA1值。4.3 so文件与ABI不匹配导致运行时报错接入早期我遇到过java.lang.UnsatisfiedLinkError提示找不到libwakeuper.so。原因大致有两个so文件没有放到src/main/jniLibs/对应ABI目录下。构建时abiFilters过滤掉了需要的架构。解法也简单检查build.gradle里的abiFilters同时确认讯飞SDK给出的ABI目录名和AS要求的目录名一致。比如讯飞SDK解压出来是libs/armeabi-v7a移动到src/main/jniLibs/armeabi-v7a这种固定路径就行。4.4 开启混淆后回调失效Release包开启ProGuard/R8混淆后唤醒功能时而正常时而不正常甚至完全没反应。这是因为讯飞SDK内部通过反射读取回调对象类名或方法名被混淆后就找不到对应的回调入口了。处理方式是在proguard-rules.pro里加规则-keep class com.iflytek.** { *; } -dontwarn com.iflytek.**这属于典型的先加上就对了的配置。想深究的话可以打开混淆日志看哪些类被重命名了会发现全是讯飞SDK里的事件分发相关类。4.5 模拟器上永远唤不醒真机一切正常这个问题是分享给同事测试时发现的。模拟器特别是没有虚拟麦克风或音频输入被改写过的环境无法正常采集音频数据唤醒模块一直等不到音频流表现就是毫无反应。这不一定全是SDK的问题更多是模拟器音频通道的兼容性限制。语音唤醒相关功能建议一律用真机调试不要在模拟器上浪费时间。下面是两个走查时经常用到的关键对照点问题现象优先排查项初始化失败/报鉴权错误包名、签名、AppID、服务是否开通代码正常但不回调动态录音权限、唤醒词资源路径、混淆规则so相关异常abiFilters、jniLibs目录结构模拟器无反应换真机测试5. 从能唤醒到唤醒得舒服的几个优化点5.1 灵敏度与触发阈值讯飞唤醒SDK支持通过参数调节灵敏度。灵敏度调高容易误唤醒说一句包含类似发音的词就触发调低则可能喊好几遍才成功。建议根据使用场景取舍车载和高噪音环境可以适当调高安静环境尽量调低。具体参数名我在不同版本的SDK里见到过不完全一致的情况。接入时先翻一下官方文档的参数设置章节找到唤醒灵敏度的说明再结合真机表现去微调。不要盲目套用网上代码片段新旧版本的参数名经常不一样这也是标题里最新版三个字有分量的原因。5.2 防止多次唤醒与触发抖动刚说过onResult之后要自己重启监听。如果不做防抖可能出现的情况是用户喊了一次唤醒词回调回来了你立刻重启监听麦克风环境还残留着上一个词的尾音SDK又识别了一次导致页面连续跳转两次。我的处理是加时间戳锁private long lastWakeTime 0; private void handleWakeUp() { long now System.currentTimeMillis(); if (now - lastWakeTime 3000) { return; } lastWakeTime now; // 业务逻辑 wakeuper.startListening(null); }3秒这个值不固定根据产品交互习惯来。如果唤醒后要进入语音指令流程这个锁最好持续到整个交互结束避免唤醒和后续指令互相干扰。5.3 后台监听与省电均衡长期开着麦克风监听必然有电量消耗。我实测下来讯飞唤醒SDK在静默空闲时的耗电还好但如果要24小时监听最好配合系统电池优化白名单机制引导用户把应用加入白名单同时把监听放在前台Service里避免进程被杀。很多反馈唤不醒的case最后追查发现不是SDK坏了而是应用进程已经被系统回收。进程都没了代码逻辑再正确也没用。进程保活是语音唤醒落地最关键的最后一块拼图。5.4 唤醒成功后的UI反馈这个点看起来不起眼但对用户体验影响很大。唤醒成功后页面最快速度给出反馈震动、提示音或动画用户才能确认已经唤醒了。否则用户以为没成功继续重复喊反而导致连续触发。我在集成时用的震动加一个短暂的动画帧体验顺畅不少。最后再分享一个小习惯我每次集成讯飞这类SDK时都会把初始化、唤醒监听、生命周期管理全部封装到一个单例WakeupManager里业务层只需要调用start()、stop()、setListener()三个方法。这样即使以后更换唤醒方案业务代码改动量也最小。语音唤醒这种东西跑通Demo只是开始真正考验人的是异常情况处理和不断打磨交互细节。希望这篇文章能帮你少走点弯路。本文还有配套的精品资源点击获取
返回列表