适合谁看

  • 同时在接鸿蒙语音识别和 TTS 的开发者

  • 觉得"都是语音能力,调法应该差不多"的人

  • 想更快定位语音链路问题的人

问题背景

鸿蒙语音识别和 TTS 虽然都来自 CoreSpeechKit,但它们的能力模型不同:

维度

语音识别(ASR)

TTS

方向

输入型(用户说话 → 文本)

输出型(文本 → 语音播放)

触发方式

用户主动触发

应用主动触发

关键回调

onResult(文本)

onComplete(播报完成)

最容易卡在哪

开始了但文本没带回来

能播但状态回不来

所以调试重点不可能完全一样。

项目中的真实场景

食界探味当前的语音能力实现:

文件

能力

调试关键

SpeechRecognitionPlugin.ets

鸿蒙 ASR

权限、引擎、回调、文本收口

TextToSpeechPlugin.ets

鸿蒙 TTS

参数、引擎复用、播报结束、stop

speech_recognition_channel.dart

Flutter ASR 通道

返回值、空结果处理

text_to_speech_channel.dart

Flutter TTS 通道

阻塞返回、stop 调用

核心实现

一、语音识别调试重点——5 个关键点

重点 1:麦克风权限是否拿到

// SpeechRecognitionPlugin.ets

private async requestMicrophonePermission(): Promise<boolean> {
  try {
    const atManager = abilityAccessCtrl.createAtManager();
    const permissions: Permissions[] = ['ohos.permission.MICROPHONE'];
    const context = getContext(this);
    const grantResult = await atManager.requestPermissionsFromUser(context, permissions);
    return grantResult.authResults.every(
      status => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    );
  } catch (err) {
    console.error(TAG, `requestPermission failed: ${JSON.stringify(err)}`);
    return false;
  }
}

调试检查:

检查项

预期

异常表现

权限已授权

返回 true

返回 false,识别不启动

权限弹窗被拒绝

返回 false

识别不启动,Flutter 收到错误

权限 API 调用失败

返回 false

识别不启动

重点 2:引擎是否创建成功

private createEngine(): Promise<void> {
  return new Promise((resolve, reject) => {
    const extraParam: Record<string, Object> = { 'locate': 'CN', 'recognizerMode': 'short' };
    const initParams: speechRecognizer.CreateEngineParams = {
      language: 'zh-CN',
      online: 1,
      extraParams: extraParam
    };

    speechRecognizer.createEngine(initParams, (err, engine) => {
      if (!err) {
        console.info(TAG, 'Engine created successfully');
        this.asrEngine = engine;
        resolve();
      } else {
        console.error(TAG, `Failed to create engine: ${err.message}`);
        reject(err);
      }
    });
  });
}

调试检查:

检查项

预期

异常表现

引擎创建成功

Engine created successfully

Failed to create engine

引擎创建超时

Promise resolve

Promise reject

引擎创建后为 null

不为 null

后续调用崩溃

重点 3:监听器是否真的触发

private setupListener(): void {
  if (!this.asrEngine) return;

  const listener: speechRecognizer.RecognitionListener = {
    onStart: (sessionId, eventMessage) => {
      console.info(TAG, `onStart sessionId: ${sessionId}`);
    },
    onResult: (sessionId, result) => {
      console.info(TAG, `onResult: ${JSON.stringify(result)}`);
      if (result.isLast && this.pendingResult) {
        this.pendingResult.success(result.result);
        this.pendingResult = null;
        this.shutdownEngine();
      }
    },
    onComplete: (sessionId, eventMessage) => {
      console.info(TAG, `onComplete sessionId: ${sessionId}`);
      if (this.pendingResult) {
        this.pendingResult.success('');
        this.pendingResult = null;
      }
      this.shutdownEngine();
    },
    onError: (sessionId, errorCode, errorMessage) => {
      console.error(TAG, `onError code: ${errorCode}, msg: ${errorMessage}`);
      if (this.pendingResult) {
        this.pendingResult.error('ASR_ERROR', errorMessage, null);
        this.pendingResult = null;
      }
      this.shutdownEngine();
    }
  };

  this.asrEngine.setListener(listener);
}

调试检查:

检查项

预期

异常表现

onStart 触发

有日志

引擎没启动

onResult 触发

有日志

没收到语音

onResult(isLast: true)

触发一次

多次触发或不触发

onError 触发

有错误码

无日志

重点 4:result.isLast 是否正确收口

onResult: (sessionId, result) => {
  if (result.isLast && this.pendingResult) {
    this.pendingResult.success(result.result);
    this.pendingResult = null;
    this.shutdownEngine();
  }
}

这是语音识别最关键的收口点。isLast = true 时才回传文本给 Flutter。

调试检查:

检查项

预期

异常表现

isLast: true 时回传

pendingResult.success()

文本没到 Flutter

isLast: true 后 shutdown

引擎销毁

引擎一直占用

isLast: false 时不回传

只记日志

中间结果泄露到 Flutter

重点 5:空结果和错误结果是否区分开

// 协调器

final text = await SpeechRecognitionChannel.startListening();
if (text.isEmpty) {
  // 空结果 → 友好提示
  state = state.copyWith(
    status: AiSessionStatus.error,
    errorMessage: '未听清,请再说一次',
  );
  return;
}

调试检查:

检查项

预期

异常表现

空字符串

提示"未听清"

弹技术性错误

正常文本

提交 AI

文本丢失

错误异常

提示"请手动输入"

页面卡死

二、TTS 调试重点——4 个关键点

重点 1:文本参数是否为空

// TextToSpeechPlugin.ets

private async handleSpeak(call: MethodCall, result: MethodResult): Promise<void> {
  const text = call.argument('text') as string;

  if (!text || text.length === 0) {
    result.error('INVALID_ARGUMENT', '播报文本不能为空', null);
    return;
  }

  this.pendingResult = result;
  // ...
}

调试检查:

检查项

预期

异常表现

文本非空

正常播报

返回 INVALID_ARGUMENT

文本为空

返回错误

引擎尝试播报空内容

文本只有空格

返回错误

引擎尝试播报空白

重点 2:引擎是否重复创建

private createEngine(): Promise<void> {
  return new Promise((resolve, reject) => {
    if (this.ttsEngine) {
      resolve();  // 已创建则直接复用
      return;
    }
    // 创建引擎...
  });
}

调试检查:

检查项

预期

异常表现

首次调用

创建引擎

无日志

重复调用

复用引擎

重复创建,资源浪费

引擎被 shutdown 后调用

重新创建

引擎为 null,崩溃

重点 3:onCompleteonStoponError 是否都能收口

const speakListener: textToSpeech.SpeakListener = {
  onStart: (requestId, response) => {
    console.info(TAG, `onStart requestId: ${requestId}`);
  },
  onComplete: (requestId, response) => {
    console.info(TAG, `onComplete requestId: ${requestId}`);
    if (this.pendingResult) {
      this.pendingResult.success(null);
      this.pendingResult = null;
    }
  },
  onStop: (requestId, response) => {
    console.info(TAG, `onStop requestId: ${requestId}`);
    if (this.pendingResult) {
      this.pendingResult.success(null);
      this.pendingResult = null;
    }
  },
  onError: (requestId, errorCode, errorMessage) => {
    console.error(TAG, `onError code: ${errorCode}, msg: ${errorMessage}`);
    if (this.pendingResult) {
      this.pendingResult.error('TTS_ERROR', errorMessage, null);
      this.pendingResult = null;
    }
  }
};

调试检查:

检查项

预期

异常表现

onComplete 触发

pendingResult 回收

Flutter await 挂起

onStop 触发

pendingResult 回收

Flutter await 挂起

onError 触发

pendingResult 回收

Flutter await 挂起

三个回调都不触发

pendingResult 永远挂起

Flutter 页面卡死

重点 4:stop() 是否真能停止播报

private handleStop(result: MethodResult): void {
  try {
    if (this.ttsEngine) {
      this.ttsEngine.stop();
    }
    result.success(null);
  } catch (err) {
    result.error('TTS_ERROR', `停止播报失败: ${err.message}`, null);
  }
}

调试检查:

检查项

预期

异常表现

播报中调用 stop

播报停止

声音继续播放

没播报时调用 stop

安全返回

报错

stop 后 pendingResult

回收

Flutter await 挂起

三、两者共同的调试点

检查项

ASR

TTS

说明

Flutter 通道名一致

com.foodvoyage.speech_recognition

com.foodvoyage.text_to_speech

不一致则 MissingPluginException

原生插件正确注册

EntryAbility 添加插件

EntryAbility 添加插件

没注册则找不到插件

页面退出时清理

无(引擎自动销毁)

TextToSpeechChannel.stop()

TTS 必须手动停止

pendingResult 回收

isLast/error 时回收

onComplete/onStop/error 时回收

不回收则 Flutter 挂起

四、调试时的日志对照

ASR 链路日志:

ArkTS: requestPermission → result
ArkTS: Engine created successfully / Failed
ArkTS: startListening
ArkTS: onStart sessionId: xxx
ArkTS: onResult: {result: "想吃鸡蛋", isLast: true}
ArkTS: onComplete sessionId: xxx
ArkTS: Engine shutdown
Flutter: 收到文本 "想吃鸡蛋"
Flutter: [AI助手] 工具调用: search_dishes(...)

TTS 链路日志:

Flutter: TextToSpeechChannel.speak(text)
ArkTS: speak called
ArkTS: onStart requestId: xxx
ArkTS: onData requestId: xxx, sequence: 1
ArkTS: onComplete requestId: xxx
Flutter: await 返回

TTS stop 链路日志:

Flutter: TextToSpeechChannel.stop()
ArkTS: stop 被调用
ArkTS: onStop requestId: xxx
Flutter: await 返回
Flutter: setState(_isSpeaking = false)

关键代码位置

文件

调试关键

app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets

ASR 鸿蒙侧

app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets

TTS 鸿蒙侧

app/lib/core/platform/speech_recognition_channel.dart

ASR Flutter 侧

app/lib/core/platform/text_to_speech_channel.dart

TTS Flutter 侧

ASR vs TTS 调试重点对比

维度

语音识别(ASR)

TTS

最容易卡在哪

开始了但文本没带回来

能播但状态回不来

关键权限

麦克风权限

引擎生命周期

每次识别后 shutdown

可复用,不主动 shutdown

关键回调

onResult(isLast: true)

onComplete / onStop

空结果处理

提示"未听清"

静默跳过

stop 处理

stopListening()

stop() + pendingResult 回收

页面退出清理

无(引擎自动销毁)

必须 stop()

常见坑

  • 用同一套思路调试识别和播报 — 它们的回调模型完全不同

  • 语音识别不看权限和最终结果收口 — 最容易卡在 isLast 没触发

  • TTS 不看 stop 路径 — 用户手动停止时状态回不来

  • 只看原生日志,不看 Flutter 最终状态 — 需要看两端日志对照

  • TTS pendingResult 没有在 onStop 时回收 — Flutter await 永远挂起

  • ASR 引擎没有 shutdown — 鸿蒙端内存泄漏

  • TTS 引擎重复创建 — 应该复用,不要每次播报都创建

可复用模板

ASR 调试检查清单

语音识别调试:
  □ 麦克风权限是否拿到?
  □ 引擎是否创建成功?
  □ onStart 是否触发?
  □ onResult 是否触发?
  □ isLast: true 是否触发?
  □ 最终文本是否回传给 Flutter?
  □ 引擎是否 shutdown?
  □ 空结果是否有友好提示?

TTS 调试检查清单

TTS 调试:
  □ 文本参数是否非空?
  □ 引擎是否创建成功?
  □ 引擎是否重复创建?
  □ onStart 是否触发?
  □ onComplete 是否触发?
  □ onStop 是否触发(手动停止时)?
  □ onError 是否触发(出错时)?
  □ pendingResult 是否在所有路径都回收?
  □ stop() 是否真能停止播报?
  □ 页面退出时是否调用 stop()?

共同调试检查清单

语音能力共同检查:
  □ Flutter 通道名是否和鸿蒙插件一致?
  □ 鸿蒙插件是否在 EntryAbility 中注册?
  □ 页面退出时是否做清理?
  □ pendingResult 是否在所有路径都回收?

本篇总结

鸿蒙语音识别和 TTS 虽然都属 CoreSpeechKit 能力,但调试重点完全不同:

  • 语音识别 — 权限 → 引擎 → 回调 → 文本收口(isLast)

  • TTS — 参数 → 引擎复用 → 播报结束/停止 → 状态收口(pendingResult)

把两条链分开看,会比混着排查高效很多。一旦抓住各自最常出问题的点,定位效率会明显提升。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐