Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别监听器实现

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

引擎创建好了,接下来最重要的就是设置监听器 ------告诉引擎"识别到结果了通知我"。这就是setupListener方法要做的事。

监听器是整个语音识别流程中信息密度最高的部分。四个回调方法(onStart、onEvent、onResult、onError)加上一个onComplete,每个回调的触发时机、参数含义、与Dart层的映射关系,都需要搞清楚。

我在实现这个监听器的时候,最头疼的是onResult回调中isLast字段的处理 。Core Speech Kit会多次调用onResult,每次返回一个部分结果,最后一次的isLasttrue表示最终结果。这个逻辑和Android的onPartialResults+onResults两个回调的设计不同,需要在一个回调里同时处理部分结果和最终结果。

今天把监听器的每一行代码都掰开讲清楚。

💡 本文对应源码FlutterSpeechPlugin.etssetupListener方法,第141-178行。

一、setListener 回调接口设计

1.1 监听器接口结构

Core Speech Kit的识别监听器包含五个回调方法:

typescript 复制代码
interface SpeechRecognitionListener {
  onStart(sessionId: string, eventMessage: string): void;
  onEvent(sessionId: string, eventCode: number, eventMessage: string): void;
  onResult(sessionId: string, result: SpeechRecognitionResult): void;
  onComplete(sessionId: string, eventMessage: string): void;
  onError(sessionId: string, errorCode: number, errorMessage: string): void;
}
回调 触发时机 触发次数 重要程度
onStart 识别会话启动 1次 ⭐⭐⭐
onEvent 识别过程中的事件 0-N次 ⭐⭐
onResult 收到识别结果 1-N次 ⭐⭐⭐⭐⭐
onComplete 识别会话完成 1次 ⭐⭐⭐⭐
onError 发生错误 0-1次 ⭐⭐⭐⭐

1.2 与Android RecognitionListener的对比

Core Speech Kit Android RecognitionListener 说明
onStart onReadyForSpeech 准备就绪
onEvent onBeginningOfSpeech / onBufferReceived 过程事件
onResult (isLast=false) onPartialResults 部分结果
onResult (isLast=true) onResults 最终结果
onComplete onEndOfSpeech 识别完成
onError onError 错误

📌 关键区别 :Android把部分结果和最终结果分成了两个回调(onPartialResultsonResults),而Core Speech Kit合并成了一个onResult回调,通过isLast字段区分。这种设计更简洁,但处理逻辑需要在一个回调里做分支判断。

1.3 flutter_speech的setupListener完整代码

typescript 复制代码
private setupListener(): void {
  if (!this.asrEngine) return;

  const channel = this.channel;
  const plugin = this;

  this.asrEngine.setListener({
    onStart(sessionId: string, eventMessage: string): void {
      console.info(TAG, `onStart: sessionId=${sessionId}`);
      channel?.invokeMethod('speech.onRecognitionStarted', null);
    },
    onEvent(sessionId: string, eventCode: number, eventMessage: string): void {
      console.info(TAG, `onEvent: code=${eventCode}, message=${eventMessage}`);
    },
    onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult): void {
      console.info(TAG, `onResult: ${result.result}, isLast=${result.isLast}`);
      plugin.lastTranscription = result.result;
      channel?.invokeMethod('speech.onSpeech', result.result);
      if (result.isLast) {
        plugin.isListening = false;
        channel?.invokeMethod('speech.onRecognitionComplete', result.result);
      }
    },
    onComplete(sessionId: string, eventMessage: string): void {
      console.info(TAG, `onComplete: ${eventMessage}`);
      if (plugin.isListening) {
        plugin.isListening = false;
        channel?.invokeMethod('speech.onRecognitionComplete', plugin.lastTranscription);
      }
    },
    onError(sessionId: string, errorCode: number, errorMessage: string): void {
      console.error(TAG, `onError: code=${errorCode}, message=${errorMessage}`);
      plugin.isListening = false;
      channel?.invokeMethod('speech.onSpeechAvailability', false);
      channel?.invokeMethod('speech.onError', errorCode);
    }
  });
}

二、onStart 回调:识别会话启动通知

2.1 触发时机

onStart在调用startListening后、引擎开始采集音频时触发。它表示"识别会话已经成功启动,引擎正在等待语音输入"。

复制代码
startListening(params)
    │
    └── 引擎初始化音频采集
         │
         └── onStart(sessionId, eventMessage)  ← 在这里触发
              │
              └── 开始等待用户说话

2.2 实现代码

typescript 复制代码
onStart(sessionId: string, eventMessage: string): void {
  console.info(TAG, `onStart: sessionId=${sessionId}`);
  channel?.invokeMethod('speech.onRecognitionStarted', null);
},

做了两件事:

  1. 打印日志:记录sessionId,便于调试
  2. 通知Dart层 :调用speech.onRecognitionStarted事件

2.3 Dart层的响应

dart 复制代码
// Dart层收到onRecognitionStarted后
Future _platformCallHandler(MethodCall call) async {
  switch (call.method) {
    case "speech.onRecognitionStarted":
      recognitionStartedHandler();  // 调用用户注册的回调
      break;
  }
}

在示例App中,recognitionStartedHandler用来更新UI状态:

dart 复制代码
_speech.setRecognitionStartedHandler(() {
  setState(() => _isListening = true);
  // 可以在这里启动录音动画
});

2.4 参数说明

参数 类型 说明 flutter_speech是否使用
sessionId string 会话ID 仅日志记录
eventMessage string 事件描述 未使用

💡 sessionId的作用:如果你的App同时管理多个识别会话(flutter_speech不需要),可以用sessionId区分不同会话的回调。

三、onResult 回调:实时识别结果与 isLast 判断

3.1 这是最核心的回调

onResult是整个监听器中最重要的回调,所有识别结果都通过它传递。

3.2 SpeechRecognitionResult 结构

typescript 复制代码
interface SpeechRecognitionResult {
  result: string;    // 识别出的文本
  isLast: boolean;   // 是否是最终结果
}
字段 类型 说明
result string 当前识别到的文本内容
isLast boolean false=部分结果(还在识别中),true=最终结果(识别结束)

3.3 onResult的调用时序

一次完整的语音识别,onResult会被调用多次

复制代码
用户说:"今天天气怎么样"

onResult("今", isLast=false)           ← 第1次:刚开始识别
onResult("今天", isLast=false)         ← 第2次:识别更多
onResult("今天天气", isLast=false)     ← 第3次:继续
onResult("今天天气怎么样", isLast=false) ← 第4次:接近完成
onResult("今天天气怎么样", isLast=true)  ← 第5次:最终结果

每次调用都带有当前的完整识别文本 (不是增量),isLast=true的那次就是最终结果。

3.4 flutter_speech的处理逻辑

typescript 复制代码
onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult): void {
  console.info(TAG, `onResult: ${result.result}, isLast=${result.isLast}`);

  // 1. 保存最新的识别文本
  plugin.lastTranscription = result.result;

  // 2. 每次都通知Dart层(实时更新UI)
  channel?.invokeMethod('speech.onSpeech', result.result);

  // 3. 如果是最终结果,额外发送完成事件
  if (result.isLast) {
    plugin.isListening = false;
    channel?.invokeMethod('speech.onRecognitionComplete', result.result);
  }
},

逐行解析:

第1行plugin.lastTranscription = result.result

  • 保存最新的识别文本到插件成员变量
  • 为什么要保存?因为onComplete回调不带识别结果,需要用这个变量

第2行channel?.invokeMethod('speech.onSpeech', result.result)

  • 每次收到结果都通知Dart层
  • Dart层的recognitionResultHandler会被调用,更新UI显示实时文本

第3-5行if (result.isLast) { ... }

  • 只在最终结果时执行
  • isListening置为false,表示识别已结束
  • 发送speech.onRecognitionComplete事件,Dart层会调用recognitionCompleteHandler

3.5 为什么每次结果都通知Dart层

你可能会想:只在isLast=true时通知不就行了?为什么部分结果也要通知?

因为实时反馈对用户体验至关重要。用户说话时看到文字一个个蹦出来,会觉得系统在"认真听"。如果等到说完才一次性显示结果,用户会以为系统卡住了。

复制代码
❌ 只发送最终结果:
用户说话中... (UI无变化,用户以为卡了)
突然显示完整文本

✅ 实时发送部分结果:
"今"
"今天"
"今天天气"
"今天天气怎么样"  ← 用户能看到实时变化

3.6 lastTranscription 的作用

lastTranscription是一个"保险"变量。正常情况下,onResult(isLast=true)会带有最终文本。但在某些边界情况下,onComplete可能在onResult(isLast=true)之前触发,这时就需要用lastTranscription来获取最后一次的识别结果。

typescript 复制代码
// onComplete中的使用
onComplete(sessionId: string, eventMessage: string): void {
  if (plugin.isListening) {
    plugin.isListening = false;
    // 用lastTranscription作为最终结果
    channel?.invokeMethod('speech.onRecognitionComplete', plugin.lastTranscription);
  }
},

四、onComplete 回调:识别完成处理

4.1 触发时机

onComplete在整个识别会话结束时触发,表示引擎已经停止音频采集和处理。

复制代码
startListening → onStart → onResult(多次) → onComplete
                                              ↑ 在这里触发

4.2 实现代码

typescript 复制代码
onComplete(sessionId: string, eventMessage: string): void {
  console.info(TAG, `onComplete: ${eventMessage}`);
  if (plugin.isListening) {
    plugin.isListening = false;
    channel?.invokeMethod('speech.onRecognitionComplete', plugin.lastTranscription);
  }
},

4.3 为什么要检查isListening

typescript 复制代码
if (plugin.isListening) {
  // ...
}

这个检查是为了防止重复发送完成事件 。正常流程中,onResult(isLast=true)已经发送了speech.onRecognitionComplete并将isListening置为false。当onComplete触发时,isListening已经是false了,就不会重复发送。

但如果onComplete先于onResult(isLast=true)触发(异常情况),isListening还是true,这时就需要在onComplete中发送完成事件。

复制代码
正常流程:
onResult(isLast=true) → isListening=false → onComplete → isListening已经是false,跳过

异常流程:
onComplete → isListening还是true → 发送complete事件 → isListening=false

🎯 这是一种防御性编程 :两个地方都可能发送完成事件,但通过isListening标志保证只发送一次。

4.4 onComplete vs onResult(isLast=true) 的区别

维度 onResult(isLast=true) onComplete
含义 最终识别结果已产生 识别会话已结束
带结果 ✅ 有result.result ❌ 只有eventMessage
触发保证 不一定触发(如cancel) 通常都会触发
先后顺序 通常先触发 通常后触发

五、onError 回调:错误码解析与异常恢复

5.1 实现代码

typescript 复制代码
onError(sessionId: string, errorCode: number, errorMessage: string): void {
  console.error(TAG, `onError: code=${errorCode}, message=${errorMessage}`);
  plugin.isListening = false;
  channel?.invokeMethod('speech.onSpeechAvailability', false);
  channel?.invokeMethod('speech.onError', errorCode);
}

做了四件事:

  1. 打印错误日志:记录错误码和错误信息
  2. 重置监听状态isListening = false
  3. 通知不可用 :发送speech.onSpeechAvailability(false),告诉Dart层引擎不可用了
  4. 发送错误事件 :发送speech.onError(errorCode),Dart层可以做相应处理

5.2 错误码分类

错误码 含义 常见原因 恢复策略
0 无错误 - -
1 网络超时 网络不稳定 提示检查网络,重试
2 网络异常 无网络连接 提示连接网络
3 音频异常 麦克风被占用 释放麦克风,重试
4 引擎忙 上一次识别未结束 等待后重试
5 无语音输入 VAD超时 提示用户说话
6 识别失败 服务端错误 重试

5.3 为什么发送onSpeechAvailability(false)

typescript 复制代码
channel?.invokeMethod('speech.onSpeechAvailability', false);

这行代码告诉Dart层"语音识别当前不可用"。Dart层收到后会更新UI状态,比如禁用"开始识别"按钮。

但这里有个设计上的权衡 :发生错误不一定意味着引擎完全不可用。有些错误(如网络超时)是临时的,重试可能就好了。发送false会导致用户需要重新调用activate才能恢复。

🤔 我的思考 :如果要做得更精细,可以根据错误码判断是否需要发送false。比如网络超时只发送onError,不发送onSpeechAvailability(false),让用户可以直接重试listen。但flutter_speech的当前实现选择了更保守的策略------出错就标记不可用,让用户重新activate。

5.4 Dart层的错误处理

dart 复制代码
_speech.setErrorHandler(() {
  setState(() {
    _isListening = false;
  });
  // 可以在这里尝试重新激活
  _speech.activate('zh_CN').then((res) {
    setState(() => _speechRecognitionAvailable = res);
  });
});

示例App中,错误处理的策略是自动重新激活。这样用户不需要手动操作就能恢复。

六、onEvent 回调:过程事件

6.1 实现代码

typescript 复制代码
onEvent(sessionId: string, eventCode: number, eventMessage: string): void {
  console.info(TAG, `onEvent: code=${eventCode}, message=${eventMessage}`);
},

flutter_speech对onEvent只做了日志记录,没有向Dart层发送任何事件。

6.2 为什么不处理onEvent

onEvent传递的是识别过程中的中间事件,比如"检测到语音开始"、"检测到语音结束"等。这些事件对于基本的语音识别功能来说不是必需的。

flutter_speech的Dart层API也没有定义对应的回调接口,所以原生端不需要转发这些事件。

6.3 可能的扩展

如果将来要增加更丰富的功能(比如语音活动指示器),可以利用onEvent:

typescript 复制代码
// 未来可能的扩展
onEvent(sessionId: string, eventCode: number, eventMessage: string): void {
  console.info(TAG, `onEvent: code=${eventCode}, message=${eventMessage}`);
  // 例如:检测到语音开始
  if (eventCode === 1) {
    channel?.invokeMethod('speech.onVoiceStart', null);
  }
  // 例如:检测到语音结束
  if (eventCode === 2) {
    channel?.invokeMethod('speech.onVoiceEnd', null);
  }
},

七、this 指向问题与闭包技巧

7.1 问题背景

setupListener中有一个容易被忽略的细节------this指向问题

typescript 复制代码
private setupListener(): void {
  if (!this.asrEngine) return;

  const channel = this.channel;  // ← 为什么要这样做?
  const plugin = this;           // ← 为什么要这样做?

  this.asrEngine.setListener({
    onResult(sessionId, result) {
      plugin.lastTranscription = result.result;  // 用plugin而不是this
      channel?.invokeMethod('speech.onSpeech', result.result);  // 用channel而不是this.channel
    }
  });
}

7.2 原因解释

在JavaScript/TypeScript中,对象字面量中的方法的this指向调用者 ,而不是定义时的外部对象。如果在回调中直接用this,它指向的是监听器对象本身,而不是FlutterSpeechPlugin实例。

typescript 复制代码
// ❌ 错误:this指向监听器对象,不是插件实例
this.asrEngine.setListener({
  onResult(sessionId, result) {
    this.lastTranscription = result.result;  // this不是FlutterSpeechPlugin!
    this.channel?.invokeMethod(...);         // this.channel是undefined!
  }
});

// ✅ 正确:用闭包捕获外部变量
const channel = this.channel;
const plugin = this;
this.asrEngine.setListener({
  onResult(sessionId, result) {
    plugin.lastTranscription = result.result;  // plugin是FlutterSpeechPlugin实例
    channel?.invokeMethod(...);                // channel是MethodChannel实例
  }
});

7.3 另一种解决方案:箭头函数

用箭头函数也可以解决this指向问题,因为箭头函数不创建自己的this:

typescript 复制代码
// 也可以用箭头函数(flutter_speech没有采用这种方式)
this.asrEngine.setListener({
  onResult: (sessionId, result) => {
    this.lastTranscription = result.result;  // 箭头函数的this指向外部
    this.channel?.invokeMethod(...);
  }
});

flutter_speech选择了闭包变量 的方式而不是箭头函数。两种方式都可以,但闭包变量的方式更明确------一眼就能看出pluginchannel是什么。

💡 ArkTS的this行为:ArkTS基于TypeScript,this的行为和TypeScript一致。如果你不确定this指向什么,用闭包变量是最安全的选择。

八、回调与Dart层事件的映射关系

8.1 完整映射表

Core Speech Kit回调 触发条件 Dart层事件 Dart回调
onStart 识别启动 speech.onRecognitionStarted recognitionStartedHandler()
onEvent 过程事件 (不转发) -
onResult (isLast=false) 部分结果 speech.onSpeech recognitionResultHandler(text)
onResult (isLast=true) 最终结果 speech.onSpeech + speech.onRecognitionComplete recognitionResultHandler + recognitionCompleteHandler
onComplete 会话结束 speech.onRecognitionComplete (仅isListening时) recognitionCompleteHandler(text)
onError 错误 speech.onSpeechAvailability(false) + speech.onError availabilityHandler(false) + errorHandler()

8.2 一次完整识别的事件流

复制代码
用户说"你好世界":

Native端                              Dart端
────────                              ──────
onStart("10000", "")
  → invokeMethod('speech.onRecognitionStarted')  →  recognitionStartedHandler()

onResult("10000", {result:"你好", isLast:false})
  → invokeMethod('speech.onSpeech', "你好")       →  recognitionResultHandler("你好")

onResult("10000", {result:"你好世界", isLast:false})
  → invokeMethod('speech.onSpeech', "你好世界")   →  recognitionResultHandler("你好世界")

onResult("10000", {result:"你好世界", isLast:true})
  → invokeMethod('speech.onSpeech', "你好世界")   →  recognitionResultHandler("你好世界")
  → invokeMethod('speech.onRecognitionComplete', "你好世界")
                                                  →  recognitionCompleteHandler("你好世界")

onComplete("10000", "")
  → (isListening已经是false,跳过)

8.3 错误场景的事件流

复制代码
网络超时场景:

Native端                              Dart端
────────                              ──────
onStart("10000", "")
  → invokeMethod('speech.onRecognitionStarted')  →  recognitionStartedHandler()

onError("10000", 1, "network timeout")
  → invokeMethod('speech.onSpeechAvailability', false)  →  availabilityHandler(false)
  → invokeMethod('speech.onError', 1)                   →  errorHandler()

九、调试监听器

9.1 日志输出

每个回调都有日志输出,方便调试:

bash 复制代码
# 查看所有回调日志
hdc hilog | grep "FlutterSpeechPlugin" | grep -E "onStart|onEvent|onResult|onComplete|onError"

正常识别的日志输出:

复制代码
FlutterSpeechPlugin: onStart: sessionId=10000
FlutterSpeechPlugin: onResult: 你好, isLast=false
FlutterSpeechPlugin: onResult: 你好世界, isLast=false
FlutterSpeechPlugin: onResult: 你好世界, isLast=true
FlutterSpeechPlugin: onComplete:

9.2 常见问题

问题 症状 原因 解决
回调不触发 无日志输出 setupListener未调用或引擎未创建 检查activate流程
onResult只触发一次 只有最终结果 VAD参数太敏感 调整vadEnd参数
onError频繁触发 错误码5 无语音输入 检查麦克风或增大vadBegin
结果为空字符串 result.result为"" 说话声音太小 靠近麦克风
Dart层收不到事件 原生日志正常但Dart无响应 channel为null 检查onAttachedToEngine

总结

本文详细讲解了flutter_speech语音识别监听器的完整实现:

  1. 五个回调:onStart、onEvent、onResult、onComplete、onError各司其职
  2. onResult是核心 :通过isLast字段区分部分结果和最终结果
  3. 双重完成保障:onResult(isLast=true)和onComplete都可能发送完成事件,通过isListening标志防止重复
  4. 错误处理:onError中重置状态并通知Dart层不可用
  5. this指向:用闭包变量解决回调中的this指向问题
  6. 事件映射:每个原生回调都有对应的Dart层事件

下一篇我们讲语音识别的启动与参数配置 ------startListening方法中StartParams的详细解析。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
教男朋友学大模型2 小时前
LoRA 为什么必须把一个矩阵初始化为0
人工智能·算法·面试·求职招聘
小鸡吃米…2 小时前
TensorFlow—— 卷积神经网络(CNN)与循环神经网络(RNN)的区别
人工智能·tensorflow
智能交通技术2 小时前
iTSTech:从AGI到AMI——自动驾驶的新方向 2026
人工智能·机器学习·自动驾驶·agi
小lo想吃棒棒糖2 小时前
思路启发:基于预测编码的Transformer无反向传播训练:局部收敛性与全局最优性分析:
人工智能·深度学习·transformer
来两个炸鸡腿2 小时前
【Datawhale组队学习202602】Hello-Agents task04智能体经典范式构建
人工智能·学习·大模型·智能体
松叶似针2 小时前
Flutter三方库适配OpenHarmony【secure_application】— setWindowPrivacyMode 隐私模式实现
flutter·harmonyos
2501_926978332 小时前
重整化群理论:从基础到前沿应用的综述(公式版)---AGI理论系统基础2.2
人工智能·经验分享·深度学习·机器学习·agi
tIjJrDKv2 小时前
自动驾驶汽车轨迹规划:人工势场法与MPC联合仿真探索
harmonyos
哈__2 小时前
基础入门 Flutter for OpenHarmony:image_cropper 图片裁剪详解
flutter