适合谁看

  • 正在做鸿蒙语音识别 + Flutter 对接的人

  • 纠结该返回中间结果还是最终结果的人

  • 想理解语音识别回传策略对 AI 体验影响的人

问题背景

鸿蒙 CoreSpeechKit 的 SpeechRecognitionEngine 在识别过程中会产生多个回调:

回调

触发时机

数据

onStart

引擎开始识别

sessionId, eventMessage

onEvent

识别过程中的事件

eventCode, eventMessage

onResult

识别到文本片段

result.result, result.isLast

onComplete

识别完成

eventMessage

onError

出错

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 推荐质量,因为:

  1. AI 拿到的是完整句子 — 不是碎片,能正确理解意图

  2. 识别质量更高 — 引擎在最终结果时会做全局优化,中间片段可能有错误

  3. 没有误触发 — 不会因为一个碎片就触发 AI 调用

如果用流式片段,可能出现:

片段1: "想吃" → AI 不知道想吃什么
片段2: "想吃鸡" → AI 可能理解为"想吃鸡肉"
片段3: "想吃鸡蛋做的" → AI 重新理解为"鸡蛋料理"

每次片段变化都可能触发不同的工具调用,导致混乱。最终文本避免了这个问题。

关键代码位置

文件

作用

app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets

鸿蒙 ASR 插件,isLast 判断

app/lib/core/platform/speech_recognition_channel.dart

Flutter 侧语音识别通道

app/lib/core/ai/ai_explore_coordinator.dart

协调器,接收最终文本并提交 AI

app/lib/features/ai_assistant/screens/ai_assistant_screen.dart

AI 页面,语音按钮交互

app/lib/features/ai_assistant/widgets/ai_input_bar.dart

输入栏,语音按钮 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 助手场景来说,最终文本已经足够好用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐