news 2026/6/17 16:38:23

鸿蒙语音识别为什么要区分 startListening 和 stopListening

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙语音识别为什么要区分 startListening 和 stopListening

适合谁看

  • 正在设计鸿蒙 Flutter 语音识别接口的人

  • 觉得"只保留一个 start 就够了"的开发者

  • 想做"按住说话、松手停止"交互的人

  • 想理解 startListening 的 Future 和 stopListening 的触发关系的人

问题背景

语音识别天然带有状态。在鸿蒙 Core Speech Kit 里,一次识别的生命周期是这样的:

未开始 ──start──▶ 正在监听 ──finish/自动──▶ 引擎回调 ──▶ 已结束 │ │ │ ├── onResult(isLast) → 回传文本 │ ├── onComplete → 兜底回传 │ └── onError → 回传错误 │ └──stop──▶ finish(sessionId) ──▶ 触发上面的回调

如果只有一个start,会出现两个问题:

  • 页面无法主动结束— 用户说完话了,但引擎还在等(vadEnd: 3000的静音超时还没到),体验很迟钝

  • 异步回传搞混start返回的 Future 到底什么时候 resolve?是 start 的时候还是识别结束的时候?

只有把startListeningstopListening拆开,页面才能真正掌控交互节奏。

项目中的真实场景

食界探味的 AI 助手页面用"按住说话、松手停止"的交互模式。用户手指按下时开始识别,松手时结束识别——这天然需要两个独立的控制点。

涉及的代码:

  • app/lib/core/platform/speech_recognition_channel.dart— Flutter 侧两个方法

  • app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets— 鸿蒙侧两个 handler

  • app/lib/core/ai/ai_explore_coordinator.dart— 协调器串联两个动作

  • app/lib/features/ai_assistant/screens/ai_assistant_screen.dart— 页面手势交互

两个动作的时序关系

用户手指按下 用户手指松手 │ │ ▼ ▼ onPanDown onPanEnd │ │ ▼ ▼ startVoiceInput() stopVoiceInput() │ │ ▼ ▼ SpeechRecognitionChannel SpeechRecognitionChannel .startListening() .stopListening() │ │ ▼ ▼ (鸿蒙侧) (鸿蒙侧) handleStartListening handleStopListening 权限→引擎→监听→开始 asrEngine.finish(sessionId) │ │ │ ▼ │ finish 触发引擎回调 │ │ │ ▼ │ onResult(isLast=true) │ │ ▼ ▼ pendingResult ──────────▶ success("今晚想吃牛肉") (Future 一直悬挂着) │ ▼ Flutter Future<String> resolve │ ▼ coordinator 拿到文本 → submitQuery()

关键理解:startListening()的 Future 一直悬挂着,直到stopListening()触发的onResult回调通过pendingResult.success()才把它 resolve 掉。两个方法配合完成一次识别。

核心实现

一、交互控制权在页面手里

开始识别和停止识别由完全不同的用户手势触发:

// ai_assistant_screen.dart 里的语音按钮 GestureDetector( onPanDown: (_) => onVoiceStart(), // 手指按下 → 开始 onPanEnd: (_) => onVoiceEnd(), // 手指松手 → 停止 onPanCancel: () => onVoiceEnd(), // 手势被打断 → 停止 child: Container( child: const Text('按住说话'), ), )

停止不是开始的附属动作,而是独立的交互控制点。可能触发停止的场景包括:

  • 用户松手— 最主要的场景

  • 手势被打断onPanCancel,比如系统通知弹出来了

  • 页面切换— 用户在说话过程中导航走了

  • 业务超时— 录音超过maxAudioDuration: 20000(20 秒),引擎自动结束

如果只有一个start,这些场景都无法处理。

二、两个方法各自的职责

Flutter 侧(speech_recognition_channel.dart):

class SpeechRecognitionChannel { static const _channel = MethodChannel('com.foodvoyage.speech_recognition'); /// 开始识别,返回最终文本(注意:不是马上返回,要等 stop 之后) static Future<String> startListening({String language = 'zh-CN'}) async { final result = await _channel.invokeMethod<String>( 'startListening', {'language': language}, ); return result ?? ''; } /// 停止识别(返回 void,不返回识别文本) static Future<void> stopListening() async { await _channel.invokeMethod<void>('stopListening'); } }

鸿蒙侧(SpeechRecognitionPlugin.ets):

onMethodCall(call: MethodCall, result: MethodResult): void { switch (call.method) { case 'startListening': this.handleStartListening(call, result); // 权限→引擎→监听→开始 break; case 'stopListening': this.handleStopListening(result); // finish → 触发回调 break; default: result.notImplemented(); break; } }

两个方法分工明确:

方法

鸿蒙侧职责

返回值

何时 resolve

startListening

权限申请、引擎创建、监听器注册、开始录音

Future<String>

stop 触发onResult

stopListening

调用finish(sessionId)结束会话

Future<void>

finish 调用完成后立即

三、stopListening 不返回识别文本

这是一个容易搞混的点。stopListening的鸿蒙侧实现:

private handleStopListening(result: MethodResult): void { try { if (this.asrEngine) { this.asrEngine.finish(this.sessionId); // 通知引擎"用户说完了" } result.success(null); // ← 返回的是 null,不是识别文本 } catch (err) { const error = err as BusinessError; result.error('ASR_ERROR', `停止识别失败: ${error.message}`, null); } }

finish()做的事情是通知鸿蒙引擎"用户已经说完话了",引擎随后会通过onResult回调返回最终识别结果。但这个结果是通过pendingResult.success()回传给startListening那个 Future 的,而不是通过stopListening的返回值。

startListening() ──▶ pendingResult = result (Future 悬挂) │ stopListening() ──▶ finish(sessionId) (触发引擎结束) │ onResult(isLast) (引擎回调) │ pendingResult.success(text) (Future resolve)

stopListening返回success(null)只是告诉 Flutter "停止指令已执行",不是识别结果。

四、协调器里的 mounted 保护

因为startListening()是一个长时间悬挂的 Future(从按下到松手可能持续好几秒),协调器必须处理用户中途退出页面的情况:

Future<void> startVoiceInput() async { if (!mounted) return; state = state.copyWith(status: AiSessionStatus.listening); try { // 这个 await 会一直挂到 stopListening 触发 onResult final text = await SpeechRecognitionChannel.startListening(); if (!mounted) return; // ← 用户可能已经退出页面 if (text.isEmpty) { state = state.copyWith(status: AiSessionStatus.error, errorMessage: '未听清,请再说一次'); return; } await submitQuery(text); } catch (e) { if (!mounted) return; // ← 异常时也要检查 state = state.copyWith(status: AiSessionStatus.error, errorMessage: '语音识别出错,请手动输入'); } } Future<void> stopVoiceInput() async { try { await SpeechRecognitionChannel.stopListening(); } catch (_) {} // 停止失败静默吞掉,不影响主流程 }

注意stopVoiceInput里异常被静默吞掉了——因为停止只是一个收尾动作,它失败不应该影响用户体验。真正的结果走的是startVoiceInputstartListening()的 Future。

五、引擎生命周期的显式收口

startListeningstopListening的分离,也让鸿蒙引擎的生命周期管理更清晰:

  • startListening负责创建引擎(createEngine

  • stopListening负责结束会话(finish

  • onResult/onComplete/onError负责销毁引擎(shutdownEngine

createEngine ←── startListening │ startListening(引擎API) ←── 开始录音 │ finish(sessionId) ←── stopListening │ onResult(isLast) ←── 引擎回调 │ shutdownEngine ←── 回调里统一清理

每个阶段都有明确的入口和出口,不会出现"引擎创建了但没销毁"的泄漏。

六、为后续扩展留空间

即使当前项目只回传最终文本(isLast时的完整结果),把开始和结束分开也为后续扩展留出了空间:

  • 实时中间结果— 在onResult里不判断isLast,把中间片段通过 EventChannel 推给 Flutter

  • 自动停止策略— 在startListening里配好vadEnd: 3000,用户说完 3 秒后引擎自动finish,不需要手动 stop

  • 长按说话上限— 通过maxAudioDuration: 20000限制最长 20 秒,超过自动结束

  • 多段识别— start/stop 循环调用,实现"说一段、提交一段"的分段输入

关键代码位置

  • app/lib/core/platform/speech_recognition_channel.dart— Flutter 侧startListening/stopListening

  • app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets— 鸿蒙侧handleStartListening/handleStopListening

  • app/lib/core/ai/ai_explore_coordinator.dart— 协调器startVoiceInput/stopVoiceInput

  • app/lib/features/ai_assistant/screens/ai_assistant_screen.dart— 页面手势onPanDown/onPanEnd

鸿蒙侧实现

鸿蒙侧拆分后,每个 handler 的职责非常单一:

// startListening:四步串联 private async handleStartListening(call, result): Promise<void> { this.pendingResult = result; await this.requestMicrophonePermission(); // 1. 权限 await this.createEngine(); // 2. 引擎 this.setupListener(); // 3. 监听器 this.startListening(); // 4. 开始录音 } // stopListening:一步触发 private handleStopListening(result): void { this.asrEngine.finish(this.sessionId); // 触发引擎结束 → onResult result.success(null); }

start是重操作(权限+引擎+监听+录音),stop是轻操作(一行finish)。这种"重启动、轻停止"的设计在鸿蒙设备上很合理——启动时做好所有准备工作,停止时只发一个信号。

引擎的销毁不在stopListening里做,而是在onResult/onComplete/onError回调里统一执行shutdownEngine()。这是因为finish()是异步的——调完之后引擎还要处理最后的音频帧,等onResult回调来了才真正结束。

Flutter 侧实现

Flutter 侧受益更大。因为有了两个独立方法,页面可以非常自然地实现"按住说话":

// 页面只需关注手势 GestureDetector( onPanDown: (_) => coordinator.startVoiceInput(), // 按下开始 onPanEnd: (_) => coordinator.stopVoiceInput(), // 松手停止 onPanCancel: () => coordinator.stopVoiceInput(), // 打断也停止 ) // 协调器只需关注结果 Future<void> startVoiceInput() async { state = state.copyWith(status: AiSessionStatus.listening); // UI 显示"正在听" final text = await SpeechRecognitionChannel.startListening(); // 悬挂等结果 await submitQuery(text); // 拿到文本自动提交给 AI }

状态同步也很自然——startVoiceInput把状态切到listening,页面通过 Riverpod 的ref.watch自动更新 UI(麦克风按钮变色、显示"正在聆听...")。结果回来后状态自动流转到parsingrespondingidle,全程不需要页面手动管理。

如果把"什么时候停"强塞给鸿蒙原生侧(比如只用vadEnd自动停止),页面就失去了控制权——用户说完话后还要等 3 秒静音超时才能拿到结果,体验很迟钝。

常见坑

  • 只有start没有stop— 页面无法主动结束输入,用户说完话还得等 VAD 超时(vadEnd: 3000

  • 页面退出时忘记调用stop— 鸿蒙引擎还在录音,pendingResult还悬挂着,造成状态残留和资源泄漏

  • stop接口存在,但页面不维护"是否正在识别"的状态— 用户连点两次 start,导致引擎冲突

  • 把"自动完成"和"用户主动停止"混为一种交互— 自动完成走onComplete兜底,主动停止走onResult(isLast),两个回调的处理逻辑不一样

  • 期望stopListening直接返回识别文本— 搞混了两个 MethodChannel 调用的返回值,文本走的是startListeningpendingResult

  • stopVoiceInput里抛异常影响主流程— 停止只是一个收尾动作,异常应该静默吞掉

  • startListening的 Future resolve 后不做mounted检查— 用户在识别期间退出页面,coordinator 已 dispose,更新 state 会报StateError

可复用模板

Flutter 侧 Channel 模板

class SpeechRecognitionChannel { static const _channel = MethodChannel('com.yourapp.speech_recognition'); /// 开始识别,Future 悬挂到 stop 触发的 onResult 后才 resolve static Future<String> startListening({String language = 'zh-CN'}) async { final result = await _channel.invokeMethod<String>( 'startListening', {'language': language}, ); return result ?? ''; } /// 停止识别,返回 void(不返回识别文本) static Future<void> stopListening() async { await _channel.invokeMethod<void>('stopListening'); } }

Flutter 侧"按住说话"交互模板

GestureDetector( onPanDown: (_) => coordinator.startVoiceInput(), onPanEnd: (_) => coordinator.stopVoiceInput(), onPanCancel: () => coordinator.stopVoiceInput(), child: YourButtonWidget(), ) // 协调器 Future<void> startVoiceInput() async { if (!mounted) return; state = state.copyWith(status: AiSessionStatus.listening); try { final text = await SpeechRecognitionChannel.startListening(); if (!mounted) return; if (text.isEmpty) { state = state.copyWith(status: AiSessionStatus.error, errorMessage: '未听清'); return; } await submitQuery(text); // 拿到文本后做业务决策 } catch (e) { if (!mounted) return; state = state.copyWith(status: AiSessionStatus.error, errorMessage: '识别出错'); } } Future<void> stopVoiceInput() async { try { await SpeechRecognitionChannel.stopListening(); } catch (_) {} // 静默吞掉 }

鸿蒙侧 handler 模板

// start:重操作(权限→引擎→监听→录音) private async handleStartListening(call, result): Promise<void> { this.pendingResult = result; // 权限、引擎、监听、开始... } // stop:轻操作(finish 触发回调) private handleStopListening(result: MethodResult): void { try { if (this.asrEngine) { this.asrEngine.finish(this.sessionId); // 触发 onResult } result.success(null); // 不返回文本,文本走 pendingResult } catch (err) { result.error('ASR_ERROR', '停止失败', null); } }

本篇总结

  • startListeningstopListening分开,核心价值是交互控制权回到页面手里——用户按下开始、松手停止,而不是等引擎自己判断

  • 两个方法之间有一条隐含的异步链:start的 Future 一直悬挂,stop触发finishfinish触发onResultonResult通过pendingResult.success()把结果还给start的 Future

  • stopListening返回 void 不返回文本——文本走的是startListeningpendingResult,搞混了就拿不到结果

  • 这种拆分既利于鸿蒙引擎的生命周期管理(重启动、轻停止),也利于 Flutter 页面的状态同步(按下切listening、结果回来切parsing

  • 真实的鸿蒙 Flutter 项目里,语音输入一定是状态能力,不是单次函数调用

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/13 17:19:51

NSK W5007SA滚珠丝杠重载参数手册

为您详细整理 W5007SA-1Z-C5Z10 滚珠丝杠的参数规格、技术特点及产品应用。 该型号属于 NSK 轴端完成品 SA 型单螺母重载系列。作为跨入 50mm 究极超大轴径门槛的中长距行程单螺母版本&#xff0c;它的公称工作行程为 650 mm&#xff0c;总全长正式突破了 1 米大关&#xff08;…

作者头像 李华
网站建设 2026/6/16 7:03:25

Typora自动编号插件:彻底解决文档编号难题的完整指南

Typora自动编号插件&#xff1a;彻底解决文档编号难题的完整指南 【免费下载链接】typora_plugin Typora Plugin. Feature Enhancement Tool | Typora 插件&#xff0c;功能增强工具 项目地址: https://gitcode.com/gh_mirrors/ty/typora_plugin Typora插件系统中的自动…

作者头像 李华
网站建设 2026/6/13 17:13:56

抖音批量下载技术揭秘:从零构建高效无水印内容采集系统

抖音批量下载技术揭秘&#xff1a;从零构建高效无水印内容采集系统 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback supp…

作者头像 李华
网站建设 2026/6/16 7:05:34

flutter-webrtc-server常见问题解答:从开发到部署的避坑指南

flutter-webrtc-server常见问题解答&#xff1a;从开发到部署的避坑指南 【免费下载链接】flutter-webrtc-server A simple WebRTC signaling server for flutter-webrtc. 项目地址: https://gitcode.com/gh_mirrors/fl/flutter-webrtc-server 如果你正在使用Flutter开发…

作者头像 李华
网站建设 2026/6/16 1:19:41

工业大模型应用指南:小白程序员必备,收藏学习助你起飞!

本文全面介绍了工业大模型在各个领域的应用&#xff0c;包括设计研发、生产制造、质量管控、物流配送、营销与售后等。通过工业大模型&#xff0c;企业能够实现创意与效率的双飞跃&#xff0c;柔性生产&#xff0c;智能化仿真&#xff0c;材料选择与优化&#xff0c;代码生成&a…

作者头像 李华