【AIUI 实战】ASR 从语音命名到“乐奇”唤醒
AIUI ASR 实战:从语音命名到“乐奇”唤醒
作者:张润胜
开源示例:sengmitnick/aiui-asr-pet-demo
适用环境:Rokid Craft / Rokid AI 眼镜
一、项目简介:把两种语音入口讲清楚
在 AI 眼镜上,“让用户说一个名字”和“等待用户唤醒”虽然都依赖 ASR,但实际上是两条截然不同的交互链路。
- 主动识别:用户已经进入明确的语音输入步骤,页面可以直接开始收音;
- 系统唤醒:页面平时保持安静,收到 AIUI 的唤醒事件后,再识别用户接下来的完整指令。
如果混用这两个入口,应用很容易出现重复创建识别器、旧回调覆盖新状态、页面离开后麦克风仍未释放等问题。
为此,我从赛博宠物的真实命名和唤醒场景中,抽取出一个最小可运行示例。它面向两类读者:一类是正在开发 Rokid AIUI 智能体的开发者,另一类是需要在眼镜上实现语音表单、角色命名、唤醒后对话等流程的产品设计者。示例仅申请麦克风权限,让每一步都易于理解、复现和验证。
二、场景与用户意图:什么时候主动听,什么时候等待唤醒
场景 1:首次进入应用,用语音完成命名
用户已进入命名页面,当前意图非常明确:说出一个名称并确认。页面无需再次要求唤醒,可以在渲染稳定后主动启动 ASR:
进入命名页 → 主动开始 ASR → 展示中间结果 → 用户确认或重说
中间结果仅用于告知用户“系统正在听”,最终结果才会成为候选名称。候选名称不会立刻写入业务数据,只有用户确认后才保存,避免一次误听直接修改名称。
场景 2:首页说“乐奇”,继续表达完整指令
首页不持续开启 SpeechRecognition,也不自行录一段语音后再判断 text.includes('乐奇')。唤醒词识别交给 AIUI 和设备层,页面只承接系统事件:
用户说“乐奇”
↓
AIUI / 设备层识别唤醒词
↓
页面收到 onVoiceWakeup(event)
↓
开启一轮 ASR,识别后续完整指令
这种分工让“何时开始交互”和“用户说了什么”各自拥有清晰边界,也减少了无效的麦克风占用。
三、方案设计:三个页面、两条链路、一套状态机
这个示例智能体由三个 Ink 页面组成:
pages/start/start.ink 启动封面
pages/onboarding/onboarding.ink 主动语音命名
pages/index/index.ink “乐奇”唤醒后的 ASR
启动页只保留“听见,便有回应”“开始体验”和“单击镜腿开始”三项信息。之后进入命名页,完成语音输入与确认,再进入首页体验系统唤醒。
两条语音链路共享四种可见状态:
idle → listening → result
↑ ↓
└──── error
idle:等待进入或等待唤醒;listening:正在识别,并持续展示中间文本;result:得到最终结果,允许用户确认下一步;error:识别失败,提供可理解的重试反馈。
能力调用关系如下:
页面主动进入命名 ─────────────┐
├→ SpeechRecognition → 页面状态 → 用户确认
AIUI onVoiceWakeup 系统事件 ──┘
应用清单只声明麦克风权限:
{
"pages": [
"pages/start/start",
"pages/onboarding/onboarding",
"pages/index/index"
],
"requiredPermissions": {
"microphone": true
}
}
相机、蓝牙和网络能力都没有加入示例,方便将问题定位在 ASR 入口、页面状态或资源生命周期,而不是其他业务模块。
四、AIUI 实现:Ink 页面与关键 API
1. 主动语音命名
命名页显示后短暂延迟,再启动识别,让页面切换和收音状态不会同时争抢首帧:
onLoad() {
this._startTimer = setTimeout(() => this.beginVoiceName(), 400);
}
识别器使用中文、单轮识别和中间结果:
const recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = false;
recognition.interimResults = true;
onresult 聚合识别文本;只有最后一个结果进入 isFinal 后,页面才切换到确认状态。确认后通过 localStorage 保存名称,选择“重新说”则释放旧实例并开始新一轮识别。
2. 系统唤醒后的单轮 ASR
首页只接收 AIUI 提供的 onVoiceWakeup:
onVoiceWakeup(event) {
if (this.data.listening) {
this.refreshAsrIdleTimer();
return;
}
this.beginVoiceTurn('wakeup');
}
这里没有再次判断“乐奇”文本。页面已经处于监听状态时,再次收到唤醒只刷新空闲计时,不创建第二个识别器。这是最直接的防重入保护。
3. 成功、失败、超时和退出统一释放
ASR 不只有“成功识别”一种结束方式。识别错误、15 秒无输入、页面退出都要走同一套释放路径:
recognition.onstart = null;
recognition.onresult = null;
recognition.onerror = null;
recognition.onend = null;
recognition.abort();
页面的 onUnload() 同样清理识别器和计时器,防止旧回调继续修改已经离开的页面。
4. 页面级按键映射
两个按钮菜单共用 actionFromKey(),将设备按键先转换成语义动作:
if (code === 'Enter') return 'CONFIRM';
if (code === 'ArrowUp') return 'PREVIOUS';
if (code === 'ArrowDown') return 'NEXT';
页面只处理 PREVIOUS / NEXT / CONFIRM,再由 _selected、moveSelection() 和 enterSelected() 更新焦点或执行操作。滑动过程中出现的左右中间事件不会进入业务状态机。
路由页面使用标准 .ink 页面 SFC 的 <page> 根节点。<template> 用于自定义组件,不能代替页面容器;需要替换宿主默认滚动或焦点行为时,在页面级 onKeyUp 中调用 event.preventDefault()。
五、交互与体验:让用户始终知道系统在做什么
完整体验路径只有三步:
- 单击镜腿进入体验;
- 说出名称,通过上下滑动选择“确认名字”或“重新说”,单击执行;
- 进入首页后说“乐奇”,继续说出完整指令,页面展示实时文本和最终结果。
Craft 完整交互演示
AIUI 语音交互
启动封面


命名页:监听、结果与选择


首页:等待唤醒与按钮焦点


眼镜显示对大面积光晕并不友好,因此页面移除了 box-shadow。按钮也不显示额外箭头:未选中项使用虚线边框和 0.8 透明度,选中项使用实线边框和完整不透明度。焦点清楚,但不会遮挡正文。
六、调试与验证:从 Craft 到真机
Craft 复现步骤
- 克隆开源仓库;
- 在 Craft 中选择“导入本地文件夹”;
- 运行智能体并进入 Interactive InkView;
- 在启动页单击“开始体验”;
- 进入命名页后说出一个名称,观察中间结果和最终结果;
- 使用方向键切换按钮,单击进入首页;
- 使用 Craft 的“触发语音唤醒”模拟系统事件;
- 继续说一句完整指令,观察实时文本和最终结果;
- 连续触发唤醒,确认页面没有创建第二个识别会话;
- 保持安静直到超时,确认页面恢复等待态。
真机运行验证
vid-20260825-154940-88
真机调试中解决的三个问题
问题一:滑动时焦点正确,手势结束又触发一次。
最初页面误用了 <template>。按照 AIUI 页面规范改为 <page> 后,页面级 onKeyUp 才能稳定接管宿主默认行为。
问题二:一次滑动包含多个底层方向事件。
解决方式不是增加时间锁,而是先做按键语义映射,只让最终的 ArrowUp / ArrowDown 进入菜单状态机,忽略左右中间事件。
问题三:阴影在模拟器里柔和,在眼镜上却形成大片光晕。
最终移除所有 box-shadow,使用虚线/实线和透明度表达未选中/选中状态。
仓库使用 Node.js 内置测试运行器:
npm test
目前共有 11 项自动化测试,覆盖应用权限、页面 SFC 根节点、两条 ASR 调用链、单实例保护、资源释放、按键映射、复合滑动和真机焦点样式。AIX 也可通过 aiui-aix pack 完成打包。
七、成果与复盘:把真实项目经验沉淀成最小示例
本项目已经形成一套可验证成果:
- 可直接导入 Craft 的完整示例;
- 启动、命名、系统唤醒三个 Ink 页面;
- Craft 运行截图和交互验证步骤;
- 11 项自动化测试;
- 可打包的 AIX 工程;
- MIT License 开源仓库:sengmitnick/aiui-asr-pet-demo。
这次实现最终沉淀出三条可复用原则:
- 系统事件决定何时进入交互:唤醒词交给 AIUI 与设备层;
- 页面状态决定如何承接交互:明确展示等待、监听、结果和错误;
- 生命周期决定功能是否稳定:单实例、防重入,并覆盖成功、失败、超时和退出释放。
相关资料:
更多推荐

所有评论(0)