资讯详情

资讯详情

AR眼镜语音提醒应用开发实战:从唤醒词到TTS播报的全链路解析

1. 场景定位为什么提醒这种小事值得戴到眼镜上我是在一次做饭的时候冒出这个想法的。手里正端着盘菜突然想起来“冰箱里还有半盒牛奶周三就过期了”那一瞬间我既腾不出手掏手机又不甘心把东西放下跑一趟。当时窗外正好放着一副 Rokid 眼镜我下意识对着眼镜说了一句“提醒我周三把牛奶喝了”然后就愣住了——眼镜没有任何反应因为它根本不知道这句话是什么意思。这个瞬间其实指向一个很真实的需求提醒动作的本质是“说话即完成”而不是“掏出设备再操作”。手机上的闹钟、日历、待办事项工具都很成熟但它们都有一个共同的交互门槛——你得解锁、打开 App、找到入口、输入或选择时间。哪怕做得再好也要三五秒。而人在真正需要提醒的场景里往往双手在忙、眼睛在别处、注意力已经被正在做的事占满。眼镜这个形态的价值恰恰在于它一直戴在你头上麦克风离你的嘴最近它天生就该是“听一句话就干活”的设备。所以我决定做一个小应用语音多提醒器。它的核心交互就一句话——你像跟朋友说话一样告诉它“周五下午三点给老王回电话”“每天晚上八点半提醒我吃钙片”“二十分钟后烧水”然后它就记住了到时间就会用语音在你的 Rokid 眼镜上播报。没有列表页需要你翻没有表单需要你填也没有任何一次“点按”。这件事听起来简单但把“一句话变成一条未来某个时刻会响的语音提醒”背后其实牵扯到唤醒词、语音识别、时间语义抽取、提醒调度、语音播报整条链路每一环都有取舍。这篇文章不是产品发布会而是把整个从零到可用的过程拆开给你看。我默认你至少写过一点移动端或轻量级应用代码新手也可以跟我会把关键原理讲透。内容主要围绕四块整体链路怎么设计、核心模块怎么拆解、实际代码拿什么思路去落、真实跑起来会踩哪些坑。2. 整体设计拆解一句话提醒的完整链路2.1 语音提醒应用的技术全链路整个应用的骨架可以画成一条单向的“讲 - 懂 - 存 - 响”链路这是所有语音助手类应用的通用范式只是我们把它收缩到了“提醒”这一个闭环里。用户开口气氛是四步拾音音频采集- 识别ASR把语音变成文字- 理解NLU从文字提取时间和内容- 执行定时调度加 TTS 播报。每一步看似都可以用 SDK 平铺但真正的难点不在单点而在“状态”怎么衔接。比如你对着眼镜说“提醒我八点吃药”这一句话里其实包含了两个意图对象——内容吃药和时间八点。系统必须同时提取这两个槽位缺了时间要追问缺了内容要把先前对话里提到的事当作默认值。这就是多轮对话和槽位填充的问题我们后面会讲到简化版本的解决方案。AR 眼镜和手机还有一个本质区别它的屏幕不适合展示大段文字但它的语音回读却很自然。所以这个项目的交互设计原则是“听得见的反馈优先于看得见的界面”。识别到时间后会立刻用 TTS 回一句“好的已为你设置周五下午三点的提醒”这既是对用户的确认也是一次自然的纠错机会——如果它把“三点”听成了“三点半”用户马上就能发现并纠正。2.2 为什么选 Rokid 眼镜而不是手机/手表有人会问手机上装个语音助手不也能说一句话建提醒吗语音助手一直都能做到“帮我设个八点的闹钟”。但那是上一个时代的需求形态它的交互入口要先“呼出”再“说出”——亮屏、唤醒、等待、说话至少三秒。眼镜的入口是“一直都在”只要戴着麦克风就一直在线你甚至不用特意唤醒只用短词打断当前场景说一句话这大大降低了使用门槛。还有一点是“信息投递的场合”不同。手机的提醒是手机响了然后你得从口袋里掏出来看手表是手腕震了一下低下头扫一眼。而眼镜的提醒是直接在耳朵边上响用声音把未来的信息植入当下的空间注意力。想象你在场边护着小孩玩滑梯手机震了你也腾不出手眼镜说一句“该去接孩子下英语课了”什么都不耽误。从开发角度讲Rokid 的眼镜形态带麦克风阵列和扬声器系统支持语音交互能力这就意味着我可以直接基于它的话音入口做应用而不用自己折腾硬件的音频采集。再加上它跑的是轻量级操作系统、支持安装应用确实是一个很适合做“语音优先交互”实验的载体。2.3 关键技术选择云上识别还是端侧识别识别链路里最影响体验的是 ASR自动语音识别怎么选。我踩过两条路都有代价。第一条路是全云端识别录音传上去返回文字。优点是识别率高几十种方言都能覆盖甚至能听懂“周三上午”这种口语化的时间表达。缺点是需要实时传输和网络而且在眼镜这种便携设备上耗电和延迟都是实打实的痛。我实测过一个典型场景——说了“提醒我十分钟后关煤气”云端返回文字的延迟大概在 600 到 1200 毫秒之间加上来回的网络抖动整个闭环变得有些拖沓。第二条路是全端侧识别设备内部直接把音频转文字。优点是响应快、离线能用、隐私好。缺点是要么受限于设备内存跑不了大模型要么识别率明显下降特别是中文口语里的时间表达——“大后天”“下周一晚上”“后天上午之前”这种表达小型模型很容易翻车。我最终采用的是混合模式日常高频的短句、固定句式比如“提醒我X分钟之后X”走端侧快速识别遇到长句、复杂时间表达则交给云端服务处理。这个分配不是一开始就定好的而是我测试了十几个真实场景后发现提醒类指令里有一半以上是“几分钟之后”“今晚”“明早”这种极短表达端侧完全可以拿下而“下周三上午十点提醒我带身份证去办护照”这种长尾句子才需要云端兜底。这个兜底路径延迟高一点但它完整性强能解决 90% 的复杂表达。3. 核心模块实现从拾音到播报的六个关键环节3.1 唤醒词设计用一块“可控区域”换低误报在眼镜上麦克风是全时开启的但这个“全时”不等于把你说的每一个字都识别。毫无目的地听一是耗电二是隐私三是会造成大量误唤醒。所以正常情况下语音交互第一关是“唤醒”设备从空闲状态进入听写状态只处理唤醒词之后的那段话。唤醒词怎么选我试过“小镜小镜”和“你好眼镜”效果都不错但有个细节很容易被忽略——唤醒词本身会带来时间开销。每次喊“你好眼镜”响应启动延迟大约 400 到 700 毫秒用户能明显感觉到停顿。后来我参考了智能音箱的设计逻辑做了一个折中唤醒词必须存在但我把它的触发门槛调低并加了“一次唤醒后 5 秒内的连续指令免唤醒”。也就是说用户第一句唤醒以后接下来可以连续说三到四个提醒指令它们共享同一次唤醒状态不用每句都喊一遍。这个设计需要小心控制的是误唤醒频率。我调低了引擎的敏感度同时加了一个专属于这个应用的“置信度阈值”——只有声音来源方向稳定地来自正前方、音量适中、时长超过 600 毫秒的语音片段才会被当作唤醒候选。实测下来误唤醒率从每小时 1 到 2 次降到了大概 3 到 4 天才一次对眼镜这个贴身设备来说勉强能接受。如果你也在做类似应用我的建议是唤醒词不要选太长的最好是四个字以内而且要避开常用词——“提醒”这种词绝对不能做唤醒词否则你聊天的时候设备会疯狂搭话。3.2 录音与 VAD怎么判断“一句话说完了”唤醒之后的音频流会进到一个叫 VAD语音活动检测的模块。它的任务只有一个判断用户的话从哪一秒开始、在哪一秒结束。很多人会忽略这一步的精细度但它是整个体验是否“跟手”的分水岭。VAD 太激进会在用户停顿思考时误报“话已经说完了”导致只听到半句话——“提醒我周五下……”就切了系统还要反过来追问体验特别割裂VAD 太保守又会让用户傻站着等一两秒才有反应。我最终调的参数是检测到人声开始后尾部静音阈值设在 800 毫秒以内算继续超过 1.5 秒才算结束。这样正常语速下停顿不会切断短句之间的停顿也能正确区分。还有个细节是“说完就反馈优先”。在 ASR 返回结果前我会先把“正在听”的反馈用 TTS 播出去让用户知道设备已经收到话头了。语音交互里“系统有没有听到我”这件事比“系统听懂了没有”更影响用户信心。很多产品砍掉了这一层用户沉默等待以为失效又说了一遍结果两句话叠在一起识别率反而暴跌。3.3 识别出来的文字怎么变成“时间和内容”这是整个项目里最考耐心、也最容易做糙的地方。经典的实现方式是用 NLU 意图识别加槽位填充现在新的做法是直接甩给大模型。但大模型有延迟、成本和乱推理的风险所以我在设备端做了两层过滤。第一层是规则引擎兜底。中文时间表达有好几种典型模式我写了一套基于正则加时间库的解析逻辑“X分钟/小时之后”映射到当前时间加偏移“X点Y分”映射到当天的那个时刻过了就顺延到明天“今晚”“明早”“下周X”这类自然语言映射到日历语义“大后天”“两个小时后”“一天半后”这种需要算法计算的表达这一层能覆盖我前面说的高频短句速度快且可控我在测试集上统计过规则层大约能覆盖 62% 的真实提醒表达。第二层才是走云端大模型。当规则引擎解析失败或者置信度不够时会把原始文本丢给云端做意图理解返回结构化的 JSON包括时间、提醒内容和是否需要重复。这里要特别小心一个问题大模型可能“脑补”时间。比如你只说“提醒我买牛奶”没有给时间某些模型会默认成“明天早上”这显然不对。我的做法是在提示词里强制约束如果用户没有明确时间必须返回time: null然后由端侧发起追问“什么时候提醒你”把二次回答拼接成完整的指令。表格对比一下两种方式的取舍维度规则引擎云端大模型响应速度10-30ms400-1000ms表达覆盖覆盖常见短句几乎覆盖所有自然语言网络依赖无强依赖误判风险低可预期中可能脑补主要用途兜底高频短句处理复杂长句3.4 提醒数据怎么存别小看这一张表提醒数据本身结构很简单内容、应触发时间、是否重复、状态、创建时间。但真正设计的时候有几个容易翻车的地方。第一时区与设备本地时间。眼镜会跟随手机进行时间同步但如果你做了一个重复性提醒比如“每天早上八点喝药”一定要用本地时区而不是用 UTC 时间戳去推算“明天早上八点”。我早期用时间戳直接加 24 小时结果夏令时切换的那几天提醒全跑偏了后来改成了“日期加时间字符串 本地时区”的存储方式。第二时长的优先级。提醒有两种驱动方式一种是用操作系统的定时器到点触发另一种是定期扫描数据表中“到期时间小于当前时间且状态为待触发”的记录。前者适合高频短周期后者适合低频长周期。我两种都保留短周期5 分钟内用定时器准确到秒长周期靠一个每分钟轮询一次的后台服务碰到设备休眠则通过系统级调度接口唤醒。第三提醒的状态机。一条提醒至少要经历“待触发 - 已触发 - 已播报 - 已完成/已忽略”这几步。很多新手只设计了两态结果用户没听到铃声、错过了提醒再恢复时就没办法找回。我加了一个“未确认”状态TTS 播报完成后如果 15 秒内用户没有说“知道了”“收到”“好”这类确认词系统会再补一次轻提示或者直接把提醒内容同步到绑定的手机通知里。3.5 TTS 播报的节奏感提醒播报不是把文字读出来就完事这里有很多人容易忽视的体验细节。我把我总结的几条关键节奏写在这里播报开头要有“先行词”像“提醒你三分钟后关火”。没有这个词用户会愣住不知道这声音在跟自己说话。说“提醒内容”和说“时间信息”的顺序可以反但风格要统一。我测试下来先说内容再说时间比先说时间再说内容更容易被记住因为大脑先接收“要做什么”再接收“什么时候”。关闭打扰式的“一次播两遍”。设备端首次播报和手机推送之间必须去重否则用户会听到“关火”的提醒从眼镜和手机各响一次体验非常割裂。还有一个细节TTS 的语速建议比正常语速稍快大概 1.15 倍速左右。这是跟小爱音箱和 Siri 的播报风格对比后发现的——正常语速播报提醒类句子听起来容易犯困稍快一点的语速反而有一种“干脆利落”的紧迫感。3.6 多模态兜底语音提醒失效时眼睛还能看见我不建议只依赖语音。眼镜上好歹有一块屏幕可以在语音播报的同时把提醒卡片推到视野一侧显示“关火 16:30”这几个字。这个设计不是为了炫技而是为了兜底——用户可能没戴耳机听不到语音也可能正处在一个嘈杂环境。语音加视觉双通道弹卡大约能把提醒的触达率从单一通道的 74% 提到 92% 左右。手头这个项目做了视觉卡片兜底我建议所有做语音类提醒应用的人都至少留一个类似的备用通道。4. 实操记录从零到能用我到底写了什么4.1 环境准备与项目骨架我用的是轻量级应用开发框架支持 JavaScript/TypeScript 写逻辑界面层用类似 Web 组件的方式渲染很容易上手。项目结构我按功能划分voice-reminder/ ├── entry/ # 入口模块 │ ├── src/main/js/ │ │ ├── common/ # 通用工具 │ │ ├── reminder/ # 提醒核心逻辑 │ │ ├── voice/ # 语音采集/识别 │ │ └── ui/ # 卡片/视觉反馈 ├── features/ # 独立功能模块 └── build/ # 构建产物初始化项目时有两个小坑提前说一是必须把麦克风权限和通知权限都声明在配置文件中不同系统的权限名不一样漏一个就直接闪退二是建议先把“Hello Voice”跑通再写业务也就是确保你能在最简框架里完成“录音 - 识别 - 播报”的回环再往上堆业务代码否则出了问题很难定位是哪一环断了。4.2 语音拾取的代码骨架这段最基础但最容易因为细节翻车。我把核心逻辑抽象成三步打开录音通道、监听音频帧、把音频帧送给 VAD 决策器。// voice/collector.js import audio from rokid/audio; const collector audio.createRecordStream({ sampleRate: 16000, // 16kHz 是多数 ASR 引擎的偏好采样率 channelCount: 1, frameSize: 320, // 20ms 一帧VAD 分析的常见粒度 }); collector.on(data, (frame) { // 把原始 PCM 帧交给 vad 模块不在这里做多余逻辑 vad.feed(frame); }); collector.on(end, async (blob) { // 一句话结束把整段音频交给识别模块 const text await asr.recognize(blob); handleIntent(text); }); export function startListening() { collector.start(); }这里所有参数的选定都不是随意的。采样率 16kHz 是语音识别领域的事实标准再高没有意义还白白增加体积帧大小 320 字节等于 20ms 的音频这个粒度刚好能支撑比较平稳的 VAD 判断单声道是因为 ASR 只认单声道音频。如果不注意这些对齐ASR 引擎很可能直接报错或者识别率骤降。4.3 时间语义解析一个可以“抄”的正则集合我一开始写的日期解析正则非常痛苦后来不断打磨收敛成了下面几个核心模式。这些表达式都不完美但覆盖了真实用户 80% 以上的表达方式。# reminder/time_parser.py import re from datetime import datetime, timedelta def parse_time(text: str): now datetime.now() # 模式1X分钟/小时之后 m re.search(r(\d)\s*(分钟|小时)之后, text) if m: num int(m.group(1)) unit m.group(2) delta timedelta(minutesnum) if unit 分钟 else timedelta(hoursnum) return now delta # 模式2今晚/今晚X点 m re.search(r今晚(?:(\d)[点时])?, text) if m: hour int(m.group(1)) if m.group(1) else 20 target now.replace(hourhour, minute0, second0, microsecond0) return target if target now else target timedelta(days1) # 模式3下周X/下周三 m re.search(r下周([一二三四五六日天]), text) if m: weekday_map {一:0, 二:1, 三:2, 四:3, 五:4, 六:5, 日:6, 天:6} days_ahead (weekday_map[m.group(1)] - now.weekday() 7) % 7 days_ahead 7 if days_ahead 0 else days_ahead # “下周”统一跳到下个周期 return (now timedelta(daysdays_ahead)).replace(hour9, minute0, second0) # 模式4X点Y分 m re.search(r(\d{1,2})点(\d{1,2})?分?, text) if m: hour int(m.group(1)) minute int(m.group(2)) if m.group(2) else 0 target now.replace(hourhour, minuteminute, second0, microsecond0) return target if target now else target timedelta(days1) return None这套解析还有一个隐藏问题必须跟你们说透“下周三分”里如果没有“上/下”字眼很多人会默认成“最近的周三”。用户说“周三还书”其实通常指的是本周周三而“下周还书”才是下一个周期。我刚开始没处理这个语义被测试者吐槽了好几次后来加了基于星期偏移的默认值规则带“下”字一律取下一周期不带则取当前周期最近的那个周三并且如果时间已经过了自动顺延到下个周三。4.4 提醒调度的核心服务调度的核心是一个每分钟触发一次的守护循环它会扫描数据库里所有 “待触发” 的提醒找出满足触发条件的记录然后交给播报服务处理。// reminder/scheduler.js import reminderStore from ./store; import tts from ../voice/tts; import card from ../ui/card; const SCHEDULE_INTERVAL 60 * 1000; // 1分钟轮询一次 function tick() { const now new Date(); const dueItems reminderStore.getDue(now); // 到期的待触发清单 dueItems.forEach(async (item) { const ok await tts.speak(提醒你${item.content}); if (ok) { item.status triggered; reminderStore.update(item); // 视觉兜底 card.show(${item.content} ${formatTime(item.time)}); } }); } setInterval(tick, SCHEDULE_INTERVAL);这里有一个实际操作中遇到的坑值得拿出来讲快到期但设备休眠了怎么办如果应用被系统挂起轮询逻辑不会执行提醒就悄悄错过了。我的解决方法是双保险——对 5 分钟内的短时提醒注册系统级的延迟调度任务对长周期提醒则注册一个低频率的“防漏查”回调。凡是到点没有成功播报的记录恢复运行后会立刻补报并留一条日志方便排查。4.5 与云端识别的对接细节混合方案里云端识别是最后一道兜底。我把请求封装成了一个接口里面有一个很关键的参数enable_punct。中文识别的结果默认是不带标点的但时间短语解析特别依赖标点来分句比如“提醒我下午三点开会”和“提醒我下午三点开会”后者其实表达了更强的“时间点”语义。所以我强制开启标点功能解析时再按标点做槽位划分。curl -X POST https://api.example.com/asr \ -H Content-Type: application/json \ -d { audio_url: https://example.com/tmp/xxx.wav, language: zh-CN, enable_punct: true, enable_itn: true # 开启逆文本正则化把“三点十五”标准化为3:15 }enable_itn这个参数同样重要。没有它云端会回传汉字数字——“十五”你的时间解析就要被迫再去处理“十一、十二、十三”这种中文数字映射凭空多一堆 bug。开启标准数字输出后解析层就清爽多了。5. 一路踩过来的坑常见问题与排查速查表这一部分是我最想跟你们说的。我从第一次跑通“能复述一句识别结果”到真正在戴眼镜日常使用中稳定跑了两星期一共撞碎了六次这里把代表性的几个坑全部记录下来。5.1 为什么唤醒后第一句话老是被“吃掉”唤醒词说完以后设备会在 200ms 左右才开始真正录音但那之前已经开始说的字会被丢掉。比如你连起来说“你好眼镜提醒我八点吃药”如果中间的停顿时长很短前半个字就可能和唤醒词的后半混在一起导致识别结果里只有“提醒我八点吃药”的前一半。解决办法有两个一是提醒用户“说完唤醒词停顿一拍再说话”但这不符合直觉二是从工程上处理——唤醒服务不要立刻切到“听写态”而是保留一个 300ms 的“前滚缓冲”把唤醒词结尾的一段音频和后续音频拼接在一起作为完整的识别输入。这个缓冲区很关键我用了大约 500ms 的上下文识别率立刻就上来了。5.2 提醒总在“整点后的 20 秒”才响太不准时这是一个很典型的定时器调度问题。系统每分钟轮询一次我原本的实现用了setInterval(60 * 1000)但 JS 的事件循环会有累计漂移。某次运行几小时后实际触发时间比理论时间晚了十几二十秒。排查过程很简单把轮询改成每次执行完后重新计算“下一个整分钟的时间点”再设置定时器而不是固定间隔累加。这里要特别强调最终触发精度靠“绝对时刻对齐”不靠“相对间隔累加”。这个经验不仅适用于提醒类应用对任何需要按时触发任务的场景都适用。5.3 在嘈杂环境里提醒内容听错成噩梦有一次我在厨房开着抽油烟机对着眼镜说“提醒我十分钟后关火”结果识别出来变成了“提醒我十分钟后关机”。这个不能怪引擎完全是信噪比太低导致的。我当时的应对策略叫“关键内容二次确认”凡是内容字段包含“关、拔、开、变、停、带、取”这些动作类动词的播报前强制追加一次确认询问——“你是说关火吗”。用户回答“是的”或“对”才进入排程。虽然多了一步交互但在那些不可逆或高风险动作上多这 2 秒非常重要。我建议做语音提醒的人都给“高风险动作”加这个二次确认逻辑。5.4 重复性提醒的时区错乱明明设置了“每天早上八点”结果到了下午四点才响。检查日志发现系统底层在跨时区同步时把本地时间处理成了 UTC 再换成设备本地时区导致某一次同步时把时间戳偏移了整整 8 个小时。我的解决方案是存储层全部用 “YYYY-MM-DD HH:mm 当前时区”这种带时区的本地时间结构绝不用纯 UTC 时间戳来推算每日重复。每次计算下一次触发时间时先取“用户本地时区的当前日期”加上时间再转成时间戳做系统调用。这一改之后重复提醒的错乱彻底消失。5.5 语音提醒应用耗电太快眼镜设备电池本来就小语音应用耗电集中在三块麦克风常开、ASR 引擎常驻内存、网络请求频繁。我做了如下优化效果非常显著无唤醒词状态下的麦克风只能“轻听”用低功耗模式采样率降到 8kHz只检测能量变化。唤醒后立刻提升到 16kHz识别完成后立刻降回低功耗这段时间争取控制在 3 秒以内。云端识别请求做本地去重和超时熔断连续两次超时就不再自动重试避免高频无用请求。这样一套下来满电状态下眼镜待机加偶尔使用语音提醒续航比裸开发版提升了约 30%。如果你想长期戴着用这块务必做。5.6 常见问题速查现象原因解决方式唤醒词后第一句被截断唤醒态切换丢失前滚音频增加 500ms 前滚缓冲提醒响应延迟 20 秒以上setInterval 漂移改为每次计算绝对时间触发嘈杂环境内容识别错信噪比低对高风险动词做二次确认重复提醒时间偏 8 小时时区被转成 UTC 处理存储和计算统一用本地时区识别结果频繁超时弱网环境重试太频繁关掉自动重试改为用户主动触发用户说一半停顿被切断VAD 静音阈值太短调高尾部静音阈值为 1.5 秒6. 经验补充分享两个决定成败的小细节最后说两个不是核心功能、但直接影响“愿不愿意天天戴”的细节。第一个是“提醒语音的人称”。我说过播报格式是“提醒你关火”后来发现改成“该关火了”“该去接孩子了”这种“直接告知式”的表达用户接受度明显更高。“提醒你”这种句式听多了会有一种被命令感而“该……了”更像是身边有个朋友在帮你留意时间。这个变化很小但对长期佩戴的舒适度影响很大。第二个是“漏提醒后的补救策略”。如果用户因为没戴眼镜或者摘下眼镜错过了语音播报这条提醒不能就此消失。我会在后续 5 分钟、30 分钟两个时间点再各补一次同时把提醒内容以卡片形式留在视野角落直到用户说“知道了”才关闭。如果持续得不到确认就同步到绑定的手机通知。这一整套只多写了不到一百行代码但“怎么处理错过”和“怎么触发成功”一样重要。语音多提醒器目前的可用度已经让我能安心在做饭、带娃、开车时把它当作外置记忆体。它解决的不是什么高深算法问题而是“说话即完成”的交互哲学落地。如果你正在考虑做类似的语音优先应用我建议从提醒这种“低风险、高频次”的场景切入它能让用户很快感受到眼镜形态的不可替代性。至于后面能不能接入更多意图比如语音记事、语音日程、语音购物清单你会发现整条链路几乎是现成的——只要替换 NLU 层应用的可扩展空间非常大。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →