【HarmonyOS学习笔记】2026-07-22 | speechRecognizer 踩坑实录2

date: 2026-07-22
tags: [HarmonyOS, speechRecognizer, 语音识别, 异步回调, 状态管理, ArkTS]
type: 踩坑实录

1️⃣ 现象(发生了什么?)

使用 speechRecognizer 做语音输入,踩了一串连环坑:

  1. 用户说话停顿后消息"等一会才发出来"或者直接丢失
  2. 发出的文本是乱码拼接,比如用户说 “hello”,发出来的是 “helhellohello helhello hello”
  3. 松手后一条消息被分裂成两条
  4. 首次使用就报错1002200010: Write audio failed because the start listening is failed
  5. 报错后引擎疯狂重启,session 计数器 1→2→4→8→16 指数爆炸
  6. 说话大约 20 秒后录音被强制中断
  7. 识别准确度不高,“停杯投箸"被识别成"情杯投注”
  8. 代码里手动创建 AudioCapturer + 切片喂引擎,复杂且容易出 bug

2️⃣ 解决办法(我是怎么做的?)

一、short 模式 → long 模式

speechRecognizer 有两种识别模式:

维度short 模式long 模式
停顿后引擎自动结束 → onComplete → 需重启只结束子句 → onResult(isFinal=true) → 引擎继续
结束方式自动,控制不了必须手动 finish()
onComplete 触发停顿就触发只有 finish() 后触发
maxAudioDuration最长 60s最长 8 小时

short 模式的核心问题:用户说话停顿,引擎就自动触发 onComplete 死掉了。用户还按着按钮,但引擎已不识别了。代码需要复杂的双标志来处理"引擎死了但用户还按着"的异常状态。

long 模式完美匹配用户心智模型——按住录音,松开发送,移开丢弃。引擎不会因停顿自动结束,状态极简。

const extraParams: Record<string, 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 isFinal=false text=hel ← 引擎目前认为整句话是 "hel" onResult isFinal=false text=hello ← 更新猜测为 "hello"(不是 "hel"+"lo"!) onResult isFinal=false text=hello hel ← 继续更新 onResult isFinal=false text=hello hello ← 继续更新 onResult isFinal=true text=Hello,hello。← 子句最终确认

我最初把 isFinal=false 的 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(isFinal=true) + !holding空的 isFinal=true 会提前触发
onComplete + !holdingfinish() 后一定会来,时机正确

onResult(isFinal=true) 的空文本陷阱finish()后引擎可能先回调一个空的isFinal=true(确认前一个子句结束),然后才回调最终的确认文本。如果在 isFinal=true 时立即发送,空回调会触发提前发送,导致后续文本只能单独发成另一条消息。

正确做法:onResult(isFinal=true) 只累积,不发送。由 onComplete 负责发送。

// onResult 中 if (result.isFinal) { if (text !== '') { self._accumulatedText = self._accumulatedText + text } self._currentPartial = '' // 不发送!由 onComplete 负责 } // onComplete 中 if (!self._holding) { self.sendAccumulated() // 这里发,时机正确 }

四、startListening 失败 + restartEngine 指数爆炸

Bug 1:StartParams.extraParams 格式问题

short 模式时 StartParams 只传 sessionId + audioInfo,没有 extraParams,工作正常。改 long 模式后加了 StartParams.extraParams(maxAudioDuration/enablePartialResult),引擎的 startListening 内部失败,报错1002200010

修复:先去掉 StartParams.extraParams,只改 CreateEngineParams 的recognizerMode: 'long'。后续加回 extraParams 时用recognitionMode: 0+maxAudioDuration的组合,不加recognizerOption

Bug 2:restartEngine 无并发防护

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使用默认值20000ms(20秒)。用户说话超过 20 秒,引擎报错1002200003: Exceeded the maximum audio length supported,录音被强制中断。

long 模式支持的 maxAudioDuration 范围是 20000 ~ 28800000ms(20秒 ~ 8小时)。

修复:在 StartParams.extraParams 中显式设置:

const startExtraParams: Record<string, Object> = { 'recognitionMode': 0, 'maxAudioDuration': 28800000 }

long 模式必须显式设置 maxAudioDuration,否则默认只有 20 秒。

六、recognitionMode 参数 — 不需要手动喂音频

speechRecognizer 的 StartParams.extraParams 中有recognitionMode参数:

recognitionMode含义需要手动 writeAudio?
0实时录音识别(引擎自己录音)不需要
1(默认)音频转文字(外部写入音频流)需要writeAudio

我之前的代码没有传recognitionMode,默认值是 1,引擎不自己录音,等待外部写入音频。所以不得不创建 AudioCapturer + processAudioData 手动喂引擎。

改用 recognitionMode=0 后:删掉import { audio }audioCaptureraudioBufferprocessAudioData()releaseCapturer(),代码从 410 行降到 290 行。

七、listener 回调中 self vs this

this.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),不做语义理解。这是两个不同的能力:

能力做什么Kit
speechRecognizer语音 → 文字CoreSpeechKit
textProcessing文字 → 实体/意图NaturalLanguageKit

"情杯投注"→"停杯投箸"这种纠错需要世界知识,不是传统 NLP 能解决的。离线 ASR 准确度天然不如云端,因为端侧模型小、计算资源有限。这是硬限制,代码层面无法优化。

4️⃣ 验证

onResult 回调替换行为验证

onResult isFinal=false text=hel onResult isFinal=false text=hello onResult isFinal=false text=hello hel onResult isFinal=false text=hello hello onResult isFinal=true text=Hello,hello。

结论:isFinal=false 的 result 是当前子句的最新完整猜测,不是增量文本。

空 isFinal=true 导致消息分裂验证

gen=12: isFinal=false "心茫然" → _currentPartial = "心茫然" stop() → _holding=false, finish() isFinal=true "" (空!) → 空文本不追加,但触发了提前发送 isFinal=true "心茫然。" → 追加到空的 _accumulatedText,单独发送 onComplete → 已空,无文本可发

结论:finish() 后引擎可能先回调空的 isFinal=true,再回调最终确认文本。onResult(isFinal=true) 不能作为发送时机。

restartEngine 指数爆炸验证

session_1 → onError → restart → session_2 → onError(旧listener) + onComplete(旧listener) → 并发 restart → session_4 → session_8 → ...

结论:旧 listener 事件 + 无并发防护 = 指数爆炸。必须用 generation 过滤 + restarting 防并发。

5️⃣ 最终结论(我的观察)

维度建议
模式选择优先用 long 模式,short 模式不适合"按住说话"场景
onResult 处理isFinal=false 是替换(更新 partial),isFinal=true 是追加(累积到总文本)
发送时机onComplete 最可靠,onResult(isFinal=true) 可能有空回调提前触发
maxAudioDurationlong 模式必须显式设置,默认只有 20 秒
recognitionMode用 0(引擎自己录音),除非有特殊需求需要手动 writeAudio
listener 回调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 模式 + recognitionMode=0 + onComplete 发送是最简可靠的组合。


学习小结:speechRecognizer 从 short 改 long 模式后,状态变量从 5 个降到 3 个,代码从 410 行降到 290 行。核心认知是 onResult 的 result 语义(替换 vs 追加)、发送时机(onComplete 最可靠)、引擎重启的并发防护。每个坑都来自对 API 语义的误解——short 不是"短录音",而是"引擎自动管理生命周期";result 不是"增量",而是"当前最佳猜测"。