鸿蒙语音识别结果回传 Flutter 时应该返回最终文本还是流式片段
适合谁看
-
正在做鸿蒙语音识别 + 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 → 回传最终文本给 Flutter
VAD 自动处理了"用户什么时候说完"的判断,所以 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 助手场景来说,最终文本已经足够好用。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)