【HarmonyOS学习笔记】2026-07-22 | speechRecognizer 踩坑实录2
date: 2026-07-22tags: [HarmonyOS, speechRecognizer, 语音识别, 异步回调, 状态管理, ArkTS]type: 踩坑实录1️⃣ 现象发生了什么使用 speechRecognizer 做语音输入踩了一串连环坑用户说话停顿后消息等一会才发出来或者直接丢失发出的文本是乱码拼接比如用户说 “hello”发出来的是 “helhellohello helhello hello”松手后一条消息被分裂成两条首次使用就报错1002200010: Write audio failed because the start listening is failed报错后引擎疯狂重启session 计数器 1→2→4→8→16 指数爆炸说话大约 20 秒后录音被强制中断识别准确度不高“停杯投箸被识别成情杯投注”代码里手动创建 AudioCapturer 切片喂引擎复杂且容易出 bug2️⃣ 解决办法我是怎么做的一、short 模式 → long 模式speechRecognizer 有两种识别模式维度short 模式long 模式停顿后引擎自动结束 → onComplete → 需重启只结束子句 → onResult(isFinaltrue) → 引擎继续结束方式自动控制不了必须手动 finish()onComplete 触发停顿就触发只有 finish() 后触发maxAudioDuration最长 60s最长 8 小时short 模式的核心问题用户说话停顿引擎就自动触发 onComplete 死掉了。用户还按着按钮但引擎已不识别了。代码需要复杂的双标志来处理引擎死了但用户还按着的异常状态。long 模式完美匹配用户心智模型——按住录音松开发送移开丢弃。引擎不会因停顿自动结束状态极简。const extraParams: Recordstring, Object { locate: CN, recognizerMode: long } const params: speechRecognizer.CreateEngineParams { language: zh-CN, online: 1, extraParams: extraParams }二、onResult 回调的 result 是替换不是追加long 模式 enablePartialResult蹦字模式下onResult 回调的result.result语义isFinalresult.result 含义应该怎么处理false当前子句的最新猜测替换上次 partial存为_currentPartial不追加true当前子句的最终确认追加到总文本_accumulatedText result.result实测日志展示的替换行为onResult isFinalfalse texthel ← 引擎目前认为整句话是 hel onResult isFinalfalse texthello ← 更新猜测为 hello不是 hello onResult isFinalfalse texthello hel ← 继续更新 onResult isFinalfalse texthello hello ← 继续更新 onResult isFinaltrue textHellohello。← 子句最终确认我最初把 isFinalfalse 的 result 当增量追加了导致发出 “helhellohello helhello hello” 这种垃圾文本。正确的拼接逻辑onResult(sessionId, result) { const text result.result.trim() if (result.isFinal) { if (text ! ) { self._accumulatedText self._accumulatedText text } self._currentPartial } else { self._currentPartial text } }三、发送时机onComplete 最可靠我试过三种发送时机发送时机可靠问题stop() 立即发❌finish() 是异步的最终结果还没到onResult(isFinaltrue) !holding❌空的 isFinaltrue 会提前触发onComplete !holding✅finish() 后一定会来时机正确onResult(isFinaltrue) 的空文本陷阱finish()后引擎可能先回调一个空的isFinaltrue确认前一个子句结束然后才回调最终的确认文本。如果在 isFinaltrue 时立即发送空回调会触发提前发送导致后续文本只能单独发成另一条消息。正确做法onResult(isFinaltrue) 只累积不发送。由 onComplete 负责发送。// onResult 中 if (result.isFinal) { if (text ! ) { self._accumulatedText self._accumulatedText text } self._currentPartial // 不发送由 onComplete 负责 } // onComplete 中 if (!self._holding) { self.sendAccumulated() // 这里发时机正确 }四、startListening 失败 restartEngine 指数爆炸Bug 1StartParams.extraParams 格式问题short 模式时 StartParams 只传 sessionId audioInfo没有 extraParams工作正常。改 long 模式后加了 StartParams.extraParamsmaxAudioDuration/enablePartialResult引擎的 startListening 内部失败报错1002200010。修复先去掉 StartParams.extraParams只改 CreateEngineParams 的recognizerMode: long。后续加回 extraParams 时用recognitionMode: 0maxAudioDuration的组合不加recognizerOption。Bug 2restartEngine 无并发防护restartEngine() 是 async 的await createEngine()期间旧引擎的 listener 还在触发 onError/onComplete每个回调都触发一次 restartEngine导致并发创建多个引擎session 计数指数增长。日志22:00:26.608 onStart session_1 22:00:26.705 onError 1002200010 start listening is failed 22:00:26.806 onError while holding, restarting engine 22:00:27.037 engine restarted session_2 ← 第1次重启 22:00:27.045 restart onError 1002200010 ← 又失败但旧listener还在触发 22:00:27.200 restart onStart session_2 22:00:27.200 restart onComplete ← 立即onComplete 22:00:27.398 engine restarted session_4 ← 并发重启跳过了3 22:00:27.702 engine restarted session_8 ← 指数爆炸修复3 重防护private _restarting: boolean false // 防并发 private _restartCount: number 0 // 限次数最多3次 private _sessionGeneration: number 0 // 过滤旧代listener事件 // listener 回调中检查 generation onComplete(sessionId, eventMessage) { if (gen ! self._sessionGeneration) return // 旧代事件忽略 }五、maxAudioDuration 默认只有 20 秒去掉 StartParams.extraParams 后maxAudioDuration使用默认值20000ms20秒。用户说话超过 20 秒引擎报错1002200003: Exceeded the maximum audio length supported录音被强制中断。long 模式支持的 maxAudioDuration 范围是 20000 ~ 28800000ms20秒 ~ 8小时。修复在 StartParams.extraParams 中显式设置const startExtraParams: Recordstring, Object { recognitionMode: 0, maxAudioDuration: 28800000 }long 模式必须显式设置 maxAudioDuration否则默认只有 20 秒。六、recognitionMode 参数 — 不需要手动喂音频speechRecognizer 的 StartParams.extraParams 中有recognitionMode参数recognitionMode含义需要手动 writeAudio0实时录音识别引擎自己录音不需要1默认音频转文字外部写入音频流需要writeAudio我之前的代码没有传recognitionMode默认值是 1引擎不自己录音等待外部写入音频。所以不得不创建 AudioCapturer processAudioData 手动喂引擎。改用 recognitionMode0 后删掉import { audio }、audioCapturer、audioBuffer、processAudioData()、releaseCapturer()代码从 410 行降到 290 行。七、listener 回调中 self vs thisthis.engine.setListener({ onComplete(sessionId: string, eventMessage: string): void { // 这里 this ≠ 当前实例 // this 指向调用这个函数的对象SDK 内部某个对象 // 所以 this._accumulatedText 会是 undefined self._accumulatedText // ✅ 通过闭包捕获指向正确的实例 } })listener 回调是 SDK 调用的不是我的实例调用的所以this不指向我的实例。const self this在函数外面把实例存下来listener 里通过闭包访问self一定能拿到正确的实例。场景用 this 还是 self原因实例方法体内this方法由实例调用this 指向实例SDK listener 回调内self闭包回调由 SDK 调用this 不指向实例Arrow function 回调内thisarrow function 不绑定自己的 this继承外层同理普通服务类export class的回调属性不需要 Event/Param 装饰器——这些装饰器只能用在ComponentV2 struct里面是 ArkUI 框架的组件通信机制。普通类用普通属性onResult?就行。3️⃣ 为什么能解决刨根问底为什么 short 模式问题这么多short 模式的设计目标是说一句话识别一句引擎检测到停顿就自动结束。但用户按住按钮说话时停顿是很正常的——喘口气、想一下措辞。引擎死了但用户还按着代码就得处理这个异常状态复杂度飙升。long 模式的设计目标是持续录音持续识别停顿只结束子句不结束引擎完美匹配按住说话的交互模式。为什么 onResult 的 result 是替换而不是追加这是 ASR自动语音识别的通用设计。引擎在识别过程中不断更新对当前子句的猜测——说了一个音节 “hel”引擎猜整句话是 “hel”再说了 “lo”引擎更新猜测为 “hello”。这不是追加是修正。如果按追加处理就会出现 “hel” “hello” “hello hel” “helhellohello helhello hello” 这种结果。为什么 ASR 准确度有限speechRecognizer 只做语音转文字ASR不做语义理解。这是两个不同的能力能力做什么KitspeechRecognizer语音 → 文字CoreSpeechKittextProcessing文字 → 实体/意图NaturalLanguageKit情杯投注→停杯投箸这种纠错需要世界知识不是传统 NLP 能解决的。离线 ASR 准确度天然不如云端因为端侧模型小、计算资源有限。这是硬限制代码层面无法优化。4️⃣ 验证onResult 回调替换行为验证onResult isFinalfalse texthel onResult isFinalfalse texthello onResult isFinalfalse texthello hel onResult isFinalfalse texthello hello onResult isFinaltrue textHellohello。结论isFinalfalse 的 result 是当前子句的最新完整猜测不是增量文本。空 isFinaltrue 导致消息分裂验证gen12: isFinalfalse 心茫然 → _currentPartial 心茫然 stop() → _holdingfalse, finish() isFinaltrue (空) → 空文本不追加但触发了提前发送 isFinaltrue 心茫然。 → 追加到空的 _accumulatedText单独发送 onComplete → 已空无文本可发结论finish() 后引擎可能先回调空的 isFinaltrue再回调最终确认文本。onResult(isFinaltrue) 不能作为发送时机。restartEngine 指数爆炸验证session_1 → onError → restart → session_2 → onError(旧listener) onComplete(旧listener) → 并发 restart → session_4 → session_8 → ...结论旧 listener 事件 无并发防护 指数爆炸。必须用 generation 过滤 restarting 防并发。5️⃣ 最终结论我的观察维度建议模式选择优先用 long 模式short 模式不适合按住说话场景onResult 处理isFinalfalse 是替换更新 partialisFinaltrue 是追加累积到总文本发送时机onComplete 最可靠onResult(isFinaltrue) 可能有空回调提前触发maxAudioDurationlong 模式必须显式设置默认只有 20 秒recognitionMode用 0引擎自己录音除非有特殊需求需要手动 writeAudiolistener 回调用const self this闭包捕获不要用 this引擎重启必须 3 重防护generation 过滤旧事件 restarting 防并发 restartCount 限次ASR 准确度离线模式硬限制代码无法优化日常对话场景够用// speechRecognizer long 模式推荐配置 const createParams: speechRecognizer.CreateEngineParams { language: zh-CN, online: 1, extraParams: { locate: CN, recognizerMode: long } } const startParams: speechRecognizer.StartParams { sessionId: sessionId, audioInfo: audioInfo, extraParams: { recognitionMode: 0, maxAudioDuration: 28800000 } }核心教训speechRecognizer 的 short/long 模式不只是时长的区别而是完全不同的回调语义和生命周期。选错模式会导致一系列连环问题。long 模式 recognitionMode0 onComplete 发送是最简可靠的组合。学习小结speechRecognizer 从 short 改 long 模式后状态变量从 5 个降到 3 个代码从 410 行降到 290 行。核心认知是 onResult 的 result 语义替换 vs 追加、发送时机onComplete 最可靠、引擎重启的并发防护。每个坑都来自对 API 语义的误解——short 不是短录音而是引擎自动管理生命周期result 不是增量而是当前最佳猜测。