【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 模式完美匹配用户心智模型------按住录音,松开发送,移开丢弃。引擎不会因停顿自动结束,状态极简。

arkts 复制代码
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 语义:

isFinal result.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" 这种垃圾文本。

正确的拼接逻辑:

arkts 复制代码
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 + !holding finish() 后一定会来,时机正确

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

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

arkts 复制代码
// 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 重防护

arkts 复制代码
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 中显式设置:

arkts 复制代码
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

arkts 复制代码
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 回调内 this arrow 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) 可能有空回调提前触发
maxAudioDuration long 模式必须显式设置,默认只有 20 秒
recognitionMode 用 0(引擎自己录音),除非有特殊需求需要手动 writeAudio
listener 回调 const self = this 闭包捕获,不要用 this
引擎重启 必须 3 重防护:generation 过滤旧事件 + restarting 防并发 + restartCount 限次
ASR 准确度 离线模式硬限制,代码无法优化,日常对话场景够用
arkts 复制代码
// 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 不是"增量",而是"当前最佳猜测"。

相关推荐
谙弆悕博士5 小时前
信息系统项目管理师教程(第4版)笔记——第 2 章 信息技术发展
笔记·职场和发展·软考高级·软考·高项·项目经理
不言鹅喻6 小时前
HarmonyOS ArkTS 实战:实现一个待办清单与日程管理应用
华为·harmonyos
wp123_17 小时前
【硬件选型笔记】TONEVEE MVL4020-1R2M 与Coilcraft XAL4020-122MEC 大电流功率电感参数深度解析
笔记
aaaameliaaa7 小时前
指针之总结
c语言·笔记·算法
caimouse8 小时前
protoc-gen-c 支持 proto3 `optional` 关键字修改记录
c语言·学习
撩得Android一次心动8 小时前
Linux编程笔记3【个人用】
linux·笔记
憧憬成为java架构高手的小白8 小时前
黑马八股--中间件学习之MQ(RabbitMQ)
学习·中间件
不肥嘟嘟右卫门8 小时前
鸿蒙原生ArkTS布局方式之RelativeContainer+百分比多设备布局深度解析
华为·harmonyos
●VON8 小时前
鸿蒙 PC Markdown 编辑器原生右键弹层兼容
安全·华为·编辑器·harmonyos·鸿蒙
达子6668 小时前
第9章_HarmonyOs图解 用Java开发UI
java·ui·harmonyos