鸿蒙语音识别和 TTS 接口的调试重点分别是什么

适合谁看

  • 同时在接鸿蒙语音识别和 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)

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