适合谁看
正在做鸿蒙语音识别 + Flutter 对接的人
纠结该返回中间结果还是最终结果的人
想理解语音识别回传策略对 AI 体验影响的人
问题背景
鸿蒙 CoreSpeechKit 的SpeechRecognitionEngine在识别过程中会产生多个回调:
回调 | 触发时机 | 数据 |
|---|---|---|
| 引擎开始识别 | sessionId, eventMessage |
| 识别过程中的事件 | eventCode, eventMessage |
| 识别到文本片段 | result.result, result.isLast |
| 识别完成 | eventMessage |
| 出错 | errorCode, errorMessage |
关键点在于onResult回调中的result.isLast字段:
isLast = false:这是中间结果,文本可能还会变(比如"我想吃" → "我想吃鸡" → "我想吃鸡蛋")isLast = true:这是最终结果,文本不会再变
Flutter 侧该消费哪一种?这是一个需要根据场景权衡的设计选择。
项目中的真实场景
食界探味当前选择了"只返回最终文本"。鸿蒙侧的处理逻辑:
// SpeechRecognitionPlugin.ets onResult: (sessionId, result) => { console.info(TAG, `onResult: ${JSON.stringify(result)}`); if (result.isLast && this.pendingResult) { this.pendingResult.success(result.result); // 只在 isLast 时回传 this.pendingResult = null; this.shutdownEngine(); } }Flutter 侧的接收方式:
// speech_recognition_channel.dart static Future<String> startListening({String language = 'zh-CN'}) async { final result = await _channel.invokeMethod<String>( 'startListening', {'language': language}, ); return result ?? ''; // 返回的是完整的最终文本 }协调器拿到最终文本后直接提交给 AI:
// ai_explore_coordinator.dart Future<void> startVoiceInput() async { state = state.copyWith(status: AiSessionStatus.listening); final text = await SpeechRecognitionChannel.startListening(); if (text.isNotEmpty) { await submitQuery(text); // 最终文本直接作为 AI 输入 } }整个链路是:鸿蒙识别 → 等待最终结果 → 一次性回传 Flutter → 直接提交 AI。
核心实现
先说结论:
在 AI 助手场景下,返回最终文本比流式片段更合适。因为 AI 需要的是完整的语义,而不是识别过程中的碎片。
一、为什么食界探味选择了最终文本
当前设计选择最终文本的原因有 3 个:
1. AI 需要完整语义
用户说"想吃鸡蛋做的,清淡一点",AI 需要理解的是完整句子的含义。如果返回流式片段:
"想" → "想吃" → "想吃鸡" → "想吃鸡蛋" → "想吃鸡蛋做的" → "想吃鸡蛋做的,清淡一点"每一片段的语义都不完整。AI 拿到"想吃"时根本不知道用户想吃什么。只有拿到最终文本"想吃鸡蛋做的,清淡一点",AI 才能正确提取意图。
2. 避免中间状态干扰
如果返回流式片段,协调器需要处理:
每个片段都更新 UI 状态
片段之间文本会变化("想吃鸡" → "想吃鸡蛋")
需要判断什么时候片段稳定了
和 AI 的流式输出可能产生冲突
当前设计完全避免了这些问题:识别过程中页面只显示"正在聆听...",识别完成后才显示最终文本并提交 AI。
3. 简化状态管理
// 当前设计:只有两种状态 // 1. listening(识别中)→ 显示"正在聆听..." // 2. 拿到最终文本 → 直接 submitQuery() // 如果用流式片段:需要更多状态 // 1. listening(识别中) // 2. partialText(中间文本,不断变化) // 3. finalText(最终文本) // 4. 需要判断 partialText 什么时候稳定最终文本设计让状态管理更简单,也更不容易出 bug。
二、鸿蒙侧的引擎生命周期管理
当前设计还有一个好处:识别完成后立即 shutdown 引擎。
onResult: (sessionId, result) => { if (result.isLast && this.pendingResult) { this.pendingResult.success(result.result); this.pendingResult = null; this.shutdownEngine(); // ← 立即释放鸿蒙 ASR 引擎 } }如果用流式片段,引擎需要在整个识别过程中保持存活,直到用户手动停止或 VAD 检测到结束。这会增加鸿蒙端的资源占用。
当前设计中,引擎只在识别期间存活:
用户按住语音按钮 → createEngine() → startListening() → 用户说话... → onResult(isLast: true) → shutdownEngine() ← 立即释放在鸿蒙设备上,语音识别引擎是比较重的资源。及时释放能减少内存占用和电量消耗。
三、流式片段在什么场景下才有价值
虽然当前场景不适合流式片段,但有些场景确实需要:
场景 | 为什么需要流式片段 |
|---|---|
实时字幕/会议记录 | 用户需要看到正在识别的内容 |
语音输入框实时预览 | 用户边说边看,确认识别是否正确 |
长段落口述 | 用户说很长一段话,需要实时反馈 |
多人对话转写 | 需要实时区分不同说话人 |
在这些场景中,用户需要看到"正在识别什么",所以流式片段是必要的。
但在 AI 助手场景中,用户不需要看到中间过程——他只需要知道"系统在听"和"识别完了"。中间的"想吃鸡" → "想吃鸡蛋" → "想吃鸡蛋做的"变化对用户没有价值。
四、如果要改成流式片段,需要改什么
假设以后需要支持流式识别(比如做实时字幕),需要改 3 层:
鸿蒙侧:在每个onResult回调中都回传片段
// 改造前:只在 isLast 时回传 onResult: (sessionId, result) => { if (result.isLast && this.pendingResult) { this.pendingResult.success(result.result); } } // 改造后:每次 onResult 都回传 onResult: (sessionId, result) => { // 通过 EventChannel 或多次 invokeMethod 回传片段 this.channel?.invokeMethod('onPartialResult', { text: result.result, isLast: result.isLast, }); if (result.isLast) { this.shutdownEngine(); } }Flutter 侧:从 MethodChannel 改为 EventChannel
// 改造前:MethodChannel(单次返回) static Future<String> startListening() async { return await _channel.invokeMethod<String>('startListening'); } // 改造后:EventChannel(流式返回) static Stream<SpeechFragment> listen() { return _eventChannel.receiveBroadcastStream().map((event) { return SpeechFragment( text: event['text'], isLast: event['isLast'], ); }); }协调器侧:需要处理流式片段
// 改造后:需要处理每个片段 SpeechRecognitionChannel.listen().listen((fragment) { if (fragment.isLast) { submitQuery(fragment.text); } else { // 更新 UI 显示中间文本 state = state.copyWith(partialText: fragment.text); } });这会显著增加复杂度。所以当前选择最终文本是一个务实的决定。
五、VAD(语音端点检测)如何配合最终文本设计
鸿蒙 ASR 引擎的 VAD 参数:
const extraParam: Record<string, Object> = { 'recognitionMode': 0, 'vadBegin': 2000, // 2秒静音开始检测结束 'vadEnd': 3000, // 3秒静音确认结束 'maxAudioDuration': 20000 // 最长20秒 };VAD 和最终文本设计的配合:
用户说话 → 引擎识别(中间结果,不回传 Flutter) → 用户停顿 2 秒 → VAD 开始检测 → 用户继续说话 → VAD 重置 → 用户停顿 3 秒 → VAD 确认结束 → 引擎返回 isLast: true → 回传最终文本给 FlutterVAD 自动处理了"用户什么时候说完"的判断,所以 Flutter 侧不需要手动调stopListening()。用户说完话后,鸿蒙引擎自动结束识别并返回最终结果。
六、手动停止 vs 自动停止的设计
当前支持两种停止方式:
1. VAD 自动停止— 用户说完话后 3 秒静音,引擎自动结束
2. 用户手动停止— 松开语音按钮时调用stopListening()
private handleStopListening(result: MethodResult): void { if (this.asrEngine) { this.asrEngine.finish(this.sessionId); // 手动结束识别 } result.success(null); }两种方式都会触发onResult(isLast: true),最终文本都会回传给 Flutter。
在 AI 助手场景中,VAD 自动停止是主要方式——用户说完话后自动识别完成,不需要手动操作。手动停止只是备选,用于用户提前松开按钮的情况。
七、最终文本设计对 AI 推荐质量的影响
最终文本设计间接提升了 AI 推荐质量,因为:
AI 拿到的是完整句子— 不是碎片,能正确理解意图
识别质量更高— 引擎在最终结果时会做全局优化,中间片段可能有错误
没有误触发— 不会因为一个碎片就触发 AI 调用
如果用流式片段,可能出现:
片段1: "想吃" → AI 不知道想吃什么 片段2: "想吃鸡" → AI 可能理解为"想吃鸡肉" 片段3: "想吃鸡蛋做的" → AI 重新理解为"鸡蛋料理"每次片段变化都可能触发不同的工具调用,导致混乱。最终文本避免了这个问题。
关键代码位置
文件 | 作用 |
|---|---|
| 鸿蒙 ASR 插件,isLast 判断 |
| Flutter 侧语音识别通道 |
| 协调器,接收最终文本并提交 AI |
| AI 页面,语音按钮交互 |
| 输入栏,语音按钮 UI |
最终文本 vs 流式片段对比
维度 | 最终文本(当前) | 流式片段 |
|---|---|---|
AI 理解质量 | ✅ 拿到完整语义 | ⚠️ 中间片段语义不完整 |
状态管理复杂度 | ✅ 简单(listening → final) | ⚠️ 复杂(listening → partial → final) |
鸿蒙引擎生命周期 | ✅ 识别完立即释放 | ⚠️ 需要保持存活更久 |
用户反馈 | ⚠️ 只有"正在聆听..." | ✅ 能看到实时识别文本 |
适用场景 | AI 助手、推荐、搜索 | 实时字幕、会议记录、口述 |
实现复杂度 | ✅ 低(MethodChannel) | ⚠️ 高(EventChannel) |
资源占用 | ✅ 低 | ⚠️ 高(引擎存活时间长) |
常见坑
中间片段直接提交 AI— 语义不完整,AI 可能误解意图
每个片段都更新 UI— 文本频繁变化,用户看着眼花
没有处理 isLast— 引擎不会自动结束,需要手动 finish
没有 shutdown 引擎— 鸿蒙 ASR 引擎一直占用内存
没有 VAD 配置— 引擎不知道用户什么时候说完,需要手动停止
没有处理 onComplete— 识别完成但没有结果时,Flutter 会一直等待
没有错误处理— 麦克风权限被拒绝、引擎创建失败时页面卡死
可复用模板
最终文本模式(推荐用于 AI 助手)
// 鸿蒙侧 onResult: (sessionId, result) => { if (result.isLast && this.pendingResult) { this.pendingResult.success(result.result); this.pendingResult = null; this.shutdownEngine(); } }// Flutter 侧 final text = await SpeechRecognitionChannel.startListening(); if (text.isNotEmpty) { await submitQuery(text); // 直接提交 AI }流式片段模式(用于实时字幕等场景)
// 鸿蒙侧:通过 EventChannel 回传 onResult: (sessionId, result) => { this.eventChannel?.send({ text: result.result, isLast: result.isLast, }); if (result.isLast) { this.shutdownEngine(); } }// Flutter 侧:消费流式片段 SpeechRecognitionChannel.listen().listen((fragment) { if (fragment.isLast) { submitQuery(fragment.text); } else { updatePartialText(fragment.text); // 更新 UI } });VAD 参数配置模板
const vadParams = { 'recognitionMode': 0, // 在线识别 'vadBegin': 2000, // 2秒静音开始检测 'vadEnd': 3000, // 3秒静音确认结束 'maxAudioDuration': 20000 // 最长20秒 };语音识别状态机模板
idle → listening(用户按住语音按钮) → [鸿蒙 ASR 识别中] → onResult(isLast: true) → 回传最终文本 → submitQuery(text) → parsing → searching → responding → idle idle → listening → [用户松开按钮] → stopListening() → onResult(isLast: true) → 回传最终文本 → submitQuery(text) idle → listening → [识别出错] → onError() → state = error → 用户点击重试本篇总结
在鸿蒙 + Flutter 下做语音识别回传,选择最终文本还是流式片段取决于场景:
AI 助手场景→ 最终文本更合适。AI 需要完整语义,中间片段没有价值,而且最终文本设计更简单、资源占用更低、识别质量更高
实时字幕/会议记录场景→ 流式片段更合适。用户需要看到正在识别的内容,中间反馈是必要的
食界探味当前选择最终文本是一个务实的决定。鸿蒙 ASR 引擎在result.isLast时才回传文本给 Flutter,中间片段只用于日志。这让整个链路更简单:用户说话 → 鸿蒙识别 → 等待最终结果 → 一次性提交 AI。
如果以后需要支持流式识别,需要从 MethodChannel 改为 EventChannel,协调器也需要处理每个片段。但对当前 AI 助手场景来说,最终文本已经足够好用。