☰
安卓智能语音对话机器人源码:唤醒识别到串口动作链路
2026/11/4 13:09:23 网站建设 项目流程

简介:智能语音对话机器人是一套面向安卓开发者与语音交互爱好者的完整项目源码,围绕语音识别、对话理解与机器人大脑接口展开,可用于学习如何把语音输入转为文本、再由自然语言处理生成应答,并驱动提醒、音乐、天气等日常功能。压缩包共179个文件,以java源码、class编译文件、xml布局与配置、png和jpg界面素材为主,另含jar依赖、so库及少量配置文件,整体约6.46MB,结构接近可直接运行与二次改造的安卓工程。已有2634人学习下载,说明其在语音助手类练手项目中具有一定参考热度。读者可从中梳理语音识别调用、聊天消息适配、网络请求封装、设置页面与表情动画等模块的实现思路,并借助机器人大脑接口示例理解对话流程的组织方式,适合作为课程设计、毕业设计或功能原型的对照材料。

1. 接一台会说话的安卓机器人,难点从来不在识别率

去年冬天我帮一个做展陈机器人的小团队看代码,他们那块安卓主板上装了三个语音 Demo,离线识别率标称都在九成以上,可到了展厅还是"装傻":用户说完话,屏幕上有字,底盘不动。最后查出来两件事——识别回调跑在子线程,指令塞进 UI 队列后被紧接着的播报打断;机器人接口的串口帧没有帧头校验,现场噪声一大整包就被丢掉。这套智能语音对话机器人源码就是围着这两个问题搭的:安卓端负责拾音、唤醒、识别、对话管理和播报,另一层机器人接口把结果翻译成底盘、舵机和灯效动作,适合想在自己硬件上做语音助手、又不愿意从零拼链路的开发者。

2. 工程骨架与选型:五个环节、三个接口、一棵目录树

2.1 一条语音从麦克风到舵机的五段链路

先把链路摊平,后面所有代码都挂在这条线上。用户开口说话,麦克风持续吐出 PCM 数据,唤醒模块在本地判断是不是在叫它;确认唤醒之后,同一份 PCM 转手交给识别引擎;识别出文本,对话管理把句子拆成意图和槽位;意图命中技能,技能再去调用机器人接口下发动作帧;动作执行的同时,播报模块把回复念出来。五个环节里,只有识别和播报是串行的,其余都可以并行。

环节输入到输出时间预算失败时怎么办
拾音麦克风到 20ms PCM 帧常驻读失败不退出,重开录音
唤醒PCM 帧到唤醒事件小于 300ms无唤醒词直接开麦,靠 VAD 收尾
识别PCM 流到文本首字 500ms 内超时降级成"没听清"
对话文本到意图 JSON小于 50ms兜底意图,不让状态机卡住
播报与执行文本到语音、到动作帧按动作时长超时抢占,急停优先

时间预算这件事在桌面调试时感受不到,装到机器人上就非常明显:唤醒晚半秒,用户会以为没听见,于是重复说一遍,识别结果就变成两句话黏在一起,意图解析直接失败。我一般会把唤醒和识别的首字延迟写进日志,跑一百次取中位数,超过预算就先查缓冲区大小,而不是先换引擎。

2.2 技术选型:为什么把拾音、识别、对话、播报拆成可替换的接口

新手常见的做法是买一套在线语音 SDK,把唤醒、识别、语义、播报全包进去,两周就能出 Demo。这套源码没有这么做,原因很实际:全包方案通常要求"一次会话内说完",而机器人对话是长连接的,用户可能说两句停一下、被环境音打断、或者中途要急停;一旦全包,麦克风、音频焦点、播报抢占这三件事都由 SDK 内部的黑匣子决定,出了问题只能等厂商发版。

所以它的边界切得很硬:拾音和唤醒是纯本地代码,识别、对话、播报各定义一个接口,默认实现走在线流式识别加本地规则对话,也保留了换离线引擎的余地。核心依赖就是系统自带的录音和 TTS 能力,加上一个第三方识别引擎,机器人接口侧只有串口、蓝牙、Socket 三种通道。这种切法带来的直接好处是:识别可以离线兜底,网络断了机器人还能执行最后一条已确认的指令;对话管理可以纯本地跑,意图不明确的句子才走云端。

线程模型也一并说清楚,这是后面排查问题的关键。整套代码里只有一个常驻的采集线程、一个识别回调线程池、一个主线程 Handler 做状态分发,动作队列是独立的一个写线程。凡是跨线程的地方都通过 Handler 或 BlockingQueue 转一次,绝不允许在识别回调里直接操作 UI 或直接写串口。

2.3 目录结构:真正需要读的只有七八个文件

app/src/main/java/com/example/voicebot/ ├── VoiceBotApp.java // 全局初始化:引擎注册、崩溃兜底 ├── core/ │ ├── AudioCapture.java // AudioRecord 封装,20ms 一帧 │ ├── WakeEngine.java // 唤醒词与 VAD │ └── DialogEngine.java // 四态状态机,所有事件入口 ├── asr/ │ ├── AsrEngine.java // 识别接口 │ ├── AsrSession.java // 单次会话、超时、线程切换 │ └── impl/OnlineAsrEngine.java // 在线流式实现 ├── nlu/ │ ├── IntentRouter.java // 意图到技能的注册表 │ ├── Intent.java // 意图与槽位数据结构 │ └── skill/ // 每个技能一个 handler ├── tts/SpeechQueue.java // 播报队列与打断 ├── robot/ │ ├── RobotTransport.java // 通道接口 │ ├── Protocol.java // 组帧与解帧 │ ├── ActionQueue.java // 动作队列、单飞、急停 │ └── transport/ // Serial / Ble / Tcp 三个实现 └── ui/MainActivity.java // 权限申请、调试面板

看这份结构能发现一个信号:asr、nlu、tts、robot四个包之间没有任何直接引用,全部靠DialogEngine串起来。这是二次开发最舒服的地方——想换识别引擎,只需要动asr/impl下一个文件;想加一个技能,只加一个 handler 类再注册一行。真要读源码,建议顺序是DialogEngine→AsrSession→Protocol,这三处读完,剩下的都是外围。

3. 核心链路的代码实现:唤醒、识别、对话与播报

3.1 音频采集与唤醒:20ms 一帧的缓冲区怎么算

采集这一层最容易写错,因为错的方式很隐蔽:代码不报错,只是延迟变大或者偶尔丢字。关键在于缓冲区大小和帧长的换算。16kHz 采样、16bit、单声道,一毫秒是 32 字节,20ms 一帧就是 640 字节。唤醒和 VAD 都需要稳定的小帧输入,一帧太大响应就慢,太小则系统调用过于频繁。

public final class AudioCapture { public static final int SAMPLE_RATE = 16000; // 16000 采样点/秒 * 0.02 秒 * 2 字节 = 640 字节 public static final int FRAME_BYTES = 640; private static final int BUFFER_BYTES = FRAME_BYTES * 5; // 100ms,用来吸收调度抖动 private AudioRecord record; private volatile boolean running; public void start() { int minBuf = AudioRecord.getMinBufferSize( SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); // 取最小缓冲和 100ms 里的较大值:太小会丢帧,太大会推高唤醒延迟 int bufSize = Math.max(minBuf, BUFFER_BYTES); record = new AudioRecord(MediaRecorder.AudioSource.VOICE_RECOGNITION, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufSize); record.startRecording(); running = true; new Thread(this::loop, "audio-capture").start(); } private void loop() { byte[] frame = new byte[FRAME_BYTES]; while (running) { int n = record.read(frame, 0, frame.length); if (n <= 0) continue; // 读失败不退出,避免一次抖动打死整条链路 WakeEngine.feed(frame, n); // 唤醒与 VAD 吃同一份 PCM AsrSession active = AsrSession.current(); if (active != null) active.feed(frame, n); } } }

这段代码有三个参数值得记住。MediaRecorder.AudioSource.VOICE_RECOGNITION会启用系统级的降噪和自动增益,比MIC更适合远场,但也意味着不同设备上的音量曲线不一样,后面做能量阈值唤醒时要重新标定。BUFFER_BYTES给到 100ms 是有意为之,安卓在高负载下调度会有几十毫秒抖动,缓冲太小会直接丢块。n <= 0那句不能写成break,真机上 USB 声卡热插拔会返回一次错误码,退出循环就等于麦克风彻底哑了。

3.2 语音识别封装:接口抽象、超时与线程切换

识别接口只保留五个方法,把"流式送数据"和"请求最终结果"分开,这样离线引擎(非流式)只要忽略feed也能接进来。

public interface AsrEngine { void start(AsrListener listener); void feed(byte[] pcm, int len); // 流式送 PCM,非流式实现可以为空 void finish(); // 用户说完了,请求最终结果 void cancel(); void release(); interface AsrListener { void onPartial(String text); // 中间结果,用来做"正在听"的反馈 void onFinal(String text); // 最终结果,唯一可信的 void onError(int code, String msg); } }

接口本身很简单,麻烦的是会话管理:引擎回调在哪个线程、超时怎么兜、重复回调怎么去重,这三件事全压在AsrSession上。

public final class AsrSession { private static final long HARD_TIMEOUT_MS = 8000; // 整段会话兜底 private final Handler main = new Handler(Looper.getMainLooper()); private final AsrEngine engine; private final DialogEngine dialog; private Runnable timeoutTask; public void start() { engine.start(new AsrEngine.AsrListener() { @Override public void onPartial(String text) { main.post(() -> dialog.onPartial(text)); } @Override public void onFinal(String text) { main.removeCallbacks(timeoutTask); // 回调线程直接改状态会崩在 UI 上,统一抛回主线程 main.post(() -> dialog.onUserSaid(text)); } @Override public void onError(int code, String msg) { main.removeCallbacks(timeoutTask); main.post(() -> dialog.onAsrFailed(code, msg)); } }); timeoutTask = () -> { engine.cancel(); dialog.onAsrTimeout(); }; main.postDelayed(timeoutTask, HARD_TIMEOUT_MS); } }

HARD_TIMEOUT_MS这条兜底在 Demo 里经常被省掉,装到机器人上就成了血泪经验:网络一抖动,识别引擎既不回最终结果也不报错,状态机永远停在 THINKING,用户后面说什么都没反应。硬超时必须有,而且超时后一定要先cancel()再切状态,否则下一次start()会因为会话没释放而抛异常。另外onFinal只认第一次,重复回调在dialog里做去重,别指望引擎守规矩。

3.3 对话管理:意图、槽位与一个四态状态机

对话管理是整个项目的中枢,它只做两件事:把文本变成意图,把意图派给技能。状态机只有四个态,够用且好排查。

public enum DialogState { IDLE, // 没在听 LISTENING, // 正在收音/识别 THINKING, // 正在解析或执行 SPEAKING // 正在播报 }

意图的数据结构用 JSON 最省事,调试时能直接打印,也方便以后换成云端语义。

{ "intent": "robot.move", "slots": { "direction": "forward", "distance": 1.0, "speed": 0.4 }, "raw": "往前走一米" }

分发逻辑用注册表,不要写一长串 if-else。技能各自实现一个 handler,注册进来就行。

public final class IntentRouter { private final Map<String, IntentHandler> handlers = new LinkedHashMap<>(); public void register(String intent, IntentHandler h) { handlers.put(intent, h); } public void dispatch(Intent it) { IntentHandler h = handlers.get(it.intent); if (h == null) { tts.speak("这个我还不会"); dialog.toIdle(); return; } try { // 槽位缺失就回问一句,比直接失败体验好得多 String missing = h.validate(it.slots); if (missing != null) { tts.speak("要往哪个方向?"); dialog.toListening(); return; } h.handle(it.slots); } catch (Exception e) { // 技能抛异常必须兜住,否则状态机就停在 THINKING 出不来了 Log.e("IntentRouter", "skill failed: " + it.intent, e); tts.speak("出了点问题"); dialog.toIdle(); } } }

这里有两个设计取舍值得说。一是槽位缺失走"反问"而不是"猜",机器人场景里猜错方向的代价比多问一句高得多。二是THINKING状态必须自带超时迁移,任何进入THINKING的路径都要么在有限时间内跳到SPEAKING,要么回IDLE,绝不允许有第三条路径,这是我在好几个项目里踩过同一个坑之后定下的规矩。

3.4 TTS 播报:队列、打断和麦克风抢占

播报看起来最简单,实际是整条链路里最容易出玄学问题的地方,因为它和采集抢同一个硬件。做法是把 TTS 包成队列,统一请求音频焦点,并在播报前后显式释放麦克风。

public final class SpeechQueue implements TextToSpeech.OnInitListener { private final ArrayDeque<String> queue = new ArrayDeque<>(); private boolean speaking; public void speak(String text, boolean interrupt) { if (interrupt) { tts.stop(); queue.clear(); } // 新指令优先,旧播报直接掐 queue.add(text); drain(); } private void drain() { if (speaking || queue.isEmpty()) return; speaking = true; focus.requestAudioFocus(); // 播报前抢焦点,避免被背景音乐盖住 audioCapture.pause(); // 让出麦克风,减少自己的声音被识别回去 tts.speak(queue.poll(), TextToSpeech.QUEUE_FLUSH, null, "utt"); } @Override public void onDone(String utteranceId) { speaking = false; focus.abandonAudioFocus(); audioCapture.resume(); // 播报结束立刻还麦,否则唤醒会"聋"两秒 drain(); } }

pause()和resume()这一对是回声问题的低成本解法。安卓上做 AEC 需要设备厂商支持,通用做法是播报期间不收音,代价是用户在机器人说话时打断不了。想要支持打断,就得保留收音并加一层能量比较,只在用户音量明显高于播报残留时才切状态,这个阈值在每台机器上都不一样,必须现场调。onDone里那句resume()千万别漏,漏了就会出现"第一次对话正常,第二次怎么喊都没反应"的经典现象。

4. 机器人接口层:协议帧、通道抽象与动作队列

4.1 指令帧设计:帧头、长度、命令字与校验

机器人主板通常是个单片机,跑不了 HTTP,串口帧是最通用的约定。这套源码用的帧格式很朴素,但每个字段都有存在理由。

字段字节数说明
帧头2固定 0xAA 0x55,用来在噪声里重新对齐
长度1只算载荷长度,不含帧头与校验
命令字10x01 移动、0x02 转向、0x03 灯效、0x0F 急停
序号10 到 255 循环,用来对回执
载荷N具体参数,移动是方向加距离加速度
校验1从长度字节累加到载荷末字节
public static byte[] buildFrame(int cmd, int seq, byte[] payload) { int len = payload == null ? 0 : payload.length; byte[] f = new byte[6 + len]; f[0] = (byte) 0xAA; f[1] = 0x55; f[2] = (byte) len; f[3] = (byte) cmd; f[4] = (byte) seq; if (len > 0) System.arraycopy(payload, 0, f, 5, len); // 校验覆盖 长度+命令字+序号+载荷,不含帧头,方便下位机从长度字节开始算 f[5 + len] = checksum(f, 2, 3 + len); return f; } private static byte checksum(byte[] b, int from, int count) { int sum = 0; for (int i = from; i < from + count; i++) sum += b[i] & 0xFF; return (byte) (sum & 0xFF); }

校验覆盖范围一定要写进文档并让上下位机对齐,这是最容易翻车的地方:上位机算到载荷末字节,下位机算到校验位之前,差一个字节就永远校验不过,而且现象是"偶尔能通",因为某些载荷下两种算法结果恰好相同。我的习惯是给组帧函数配一个单元测试,用固定输入断言输出的十六进制串,改动协议时先看测试红不红。

4.2 通道抽象:串口、蓝牙、TCP 换一个实现就能跑

不同批次的机器人主板接口不一样,有的留 USB 串口,有的只有蓝牙,有的走 WiFi。把通道抽出来,上层就完全不用关心底层是什么。

public interface RobotTransport { boolean open(RobotConfig cfg); void send(byte[] frame); boolean isConnected(); void close(); interface Listener { void onFrame(byte[] frame); // 下位机回执 void onDisconnected(String reason); } }

串口实现要处理 USB 权限广播和波特率匹配,蓝牙实现要注意 Android 12 之后的新权限,TCP 实现要处理粘包——因为帧头固定,粘包其实是最好解的一种,读到一个缓冲区之后从头扫0xAA 0x55,按长度字段切就行。工厂按配置里的类型名反射创建,加一种新通道只需要新增一个类加一行注册,不用改ActionQueue。

常见做法是给每个通道加一个写线程,send只是把帧丢进队列,真正的write在写线程里串行执行。串口在安卓上并发写会直接把帧拆散,而且不会报错,只会表现为"机器人偶尔抽风"。

4.3 动作编排:队列、单飞与急停抢占

语音指令和机器人动作不是一对一。用户说"往前走然后转个圈",实际是两条动作要排队执行;用户说到一半喊"停",必须立刻打断正在跑的动作。所以动作层要有一个队列,外加一个不走队列的抢占通道。

public final class ActionQueue { private final BlockingQueue<RobotAction> queue = new LinkedBlockingQueue<>(); private final RobotTransport transport; private RobotAction running; public void submit(RobotAction a) { if (a.priority == Priority.EMERGENCY) { // 急停无条件插队,并清空后续 queue.clear(); transport.send(a.frame); running = null; return; } queue.offer(a); } // 单飞:同一时刻只允许一条动作在跑,靠完成回执或超时推进 private void loop() throws InterruptedException { while (true) { running = queue.take(); transport.send(running.frame); awaitDoneOrTimeout(running); // 回执、超时,二者取先到 running = null; } } }

awaitDoneOrTimeout是必须的。如果只等回执,一旦丢帧整个队列就永久阻塞,机器人会保持最后一个动作一直跑,这个后果在现场很严重。超时时长按动作本身的时长估,比如转圈两秒就设三秒超时,超了就当失败继续下一条,同时打一条日志。急停之所以绕过队列,是因为它要求的是"立刻",排队等前一条动作跑完就失去意义了。

5. 真机跑通与排查:五条现象对应的定位路径

5.1 编译环境与最小可跑步骤

工程是标准的单模块 Gradle 结构,minSdk给到 24,主要是为了串口和权限 API 的可用性;compileSdk按当前主流版本给就行,改高改低不影响核心逻辑。第一次跑通别急着接机器人,先用手机验证语音链路。

# 1. 先定位所有需要填配置的地方,别急着编译 grep -rn "APP_ID\|APP_KEY\|SERIAL_BAUD\|ROBOT_HOST" app/src/main/java app/build.gradle # 2. 编 debug 包 ./gradlew :app:assembleDebug # 3. 安装并只盯三个 tag 的日志 adb install -r app/build/outputs/apk/debug/app-debug.apk adb logcat -s VoiceCore:V RobotIo:V AsrEngine:V

日志过滤这三个 tag 是有讲究的:VoiceCore打状态迁移,AsrEngine打识别中间结果和错误码,RobotIo打收发的十六进制帧。排查顺序永远是先看状态迁移对不对,再看识别有没有结果,最后看帧有没有发出去——顺序反过来会在无关的层浪费时间。

5.2 权限与清单:麦克风、蓝牙、USB Host 各自要什么

<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <!-- Android 12 起蓝牙要新权限,老权限声明了也不生效 --> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/> <!-- USB 串口方案的必要声明,机器人主板走 USB 时不能少 --> <uses-feature android:name="android.hardware.usb.host" android:required="false"/>

麦克风是运行时权限,第一次点开就得申请,别等用户点了"开始对话"再弹,那时候采集线程已经调过startRecording了,会静默失败。蓝牙权限要按系统版本分支:低版本用老的BLUETOOTH和BLUETOOTH_ADMIN,高版本必须申请BLUETOOTH_CONNECT。USB 串口除了usb.host特性,还要配一个 device filter 文件,否则设备插上去枚举不到。

5.3 配置与密钥注入:别把 appId 写死在 Java 里

识别引擎的 appId、密钥、串口波特率、机器人地址这几项,一律走构建配置注入,源码里只留占位符。这样同一份代码可以出展厅版和测试版,也不会出现把密钥提交进版本库的事故。

// 读 local.properties 或命令行 -P 参数,缺配置直接让构建失败 def prop(String key, def fallback = null) { def v = project.findProperty(key) ?: fallback if (v == null) throw new GradleException("缺少配置项: ${key}") return v } android { defaultConfig { buildConfigField "String", "ASR_APP_ID", "\"${prop('asr.appId')}\"" buildConfigField "String", "ASR_APP_KEY", "\"${prop('asr.appKey')}\"" buildConfigField "int", "SERIAL_BAUD", "${prop('robot.baud', 115200)}" buildConfigField "String", "ROBOT_HOST", "\"${prop('robot.host', '192.168.1.50')}\"" } }

local.properties本身不提交,团队里每个人自己填;CI 上通过命令行-P传。缺配置时抛异常而不是给默认空串,这一点很重要——空串会让识别引擎初始化成功但不返回结果,排查起来比直接构建失败费时得多。

5.4 五条踩坑记录:现象、原因、解决

第一条,唤醒后识别一直返回空文本。现象是日志里onPartial从来不打印,只有onError的 101 码。原因是播报结束时没有resume()采集,AudioRecord处于停止状态,识别引擎收到的是空流;另一种情况是采集对象被重复start(),内部抛了IllegalStateException被吞掉。解决是把麦克风做成单例,加一个状态断言,start时如果已在录音就直接返回,播报前后严格配对调用。

第二条,真机上识别率掉一半,模拟器上却正常。现象是同样一句话,手机上能识别,装到机器人主板上就变成乱码或空。原因是采样率不匹配,主板默认输出 44.1kHz,代码没做重采样直接送给只认 16kHz 的引擎;另外用了VOICE_COMMUNICATION音源,远端通话降噪把近场人声也压掉了。解决是固定 16kHz 采集,换回VOICE_RECOGNITION,并在调试面板加一个 PCM dump 按钮,把原始数据导出来对比波形。

第三条,机器人偶尔乱动或者干脆不动。现象是动作帧在日志里发出去了,下位机没反应,或者执行了错误的动作。原因有两个:一是串口写操作被多个线程并发调用,帧被拆散;二是校验和的覆盖范围上下位机理解不一致,某些载荷下恰好通过,看起来像"偶发"。解决是给写操作加单线程串行化,把组帧函数锁死并配单元测试,用固定输入断言输出的十六进制串。

第四条,聊两句就卡住,语音助手变哑巴。现象是第二次对话开始没有任何反应,日志停在 THINKING。原因是某个技能 handler 抛了异常或者忘了回调,状态机没有出口。解决是给每个状态加超时迁移,dispatch整体包 try-catch,兜底一律回 IDLE,同时把状态迁移全部打日志,出问题时看最后一条迁移就知道卡在哪。

第五条,Android 12 以上蓝牙连不上,USB 串口枚举不到设备。现象是open()返回 false,没有任何异常栈。原因是只声明了老的蓝牙权限,新系统要求运行时申请BLUETOOTH_CONNECT和BLUETOOTH_SCAN;USB 侧则缺少 device filter,插上设备后拿不到权限广播。解决是按版本分支申请权限,补上 filter 文件,并把open()的失败原因细分到日志里,别只返回一个布尔值。

6. 二次开发:换引擎、加技能、写一条自己的机器人通道

这套代码的价值不在 Demo 本身,而在于四个接口把可变的部分圈出来了。真要动手改,按下面三个方向走,基本不用碰核心状态机。

改造目标需要动的文件工作量注意点
换成离线识别引擎asr/impl下新增一个实现类半天非流式引擎忽略feed,在finish里一次性送数据
加一个自定义技能nlu/skill下新增 handler一小时必须实现validate,槽位缺失要能回问
接自家机器人主板robot/transport下新增通道一天写操作必须串行,协议校验范围写进测试

加技能是最常见的需求,比如让机器人"跳个舞"。写一个 handler 注册进去就行:

public final class DanceHandler implements IntentHandler { private final ActionQueue actions; private final SpeechQueue tts; @Override public String validate(Map<String, Object> slots) { return null; // 跳舞没有必填槽位 } @Override public void handle(Map<String, Object> slots) { tts.speak("好的,看我跳一段", true); actions.submit(RobotAction.of(Cmd.DANCE, Payload.dance(2))); // 跳两轮 } } // 在 DialogEngine 初始化处注册,一行搞定 router.register("robot.dance", new DanceHandler(actionQueue, speechQueue));

注册完之后记得验证两件事:一是这句指令在播报期间说会不会被吞掉,二是跳舞动作跑起来时来一条急停能不能立刻生效。第二个尤其要测,因为DanceHandler提交的是长动作,急停走的是抢占通道,只要ActionQueue实现没被改坏就一定能插进去,但很多人为了"支持连续动作"把EMERGENCY分支删了,后果就是用户喊停机器人还在转。

换识别引擎的时候有个隐藏约束:AsrSession的超时逻辑是假设引擎最终一定会回调的,如果你接的引擎在静音时压根不回调onFinal,那就得在 VAD 判定静音后主动调finish(),否则每次都要等满八秒硬超时。我一般的做法是先在onPartial里刷新一个"最后活动时间",超过 1.2 秒没新中间结果就主动收尾,这样交互节奏才像人说话。

从那以后我每次接新的机器人硬件,都强制先跑一遍"三帧测试":发一条移动帧看底盘动不动、发一条错误校验帧看下位机是否丢弃、发一条急停帧看能不能打断上一条动作。这三步过了,上层语音链路才有意义。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询