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

适合谁看
-
同时在接鸿蒙语音识别和 TTS 的开发者
-
觉得"都是语音能力,调法应该差不多"的人
-
想更快定位语音链路问题的人
问题背景
鸿蒙语音识别和 TTS 虽然都来自 CoreSpeechKit,但它们的能力模型不同:
|
维度 |
语音识别(ASR) |
TTS |
|---|---|---|
|
方向 |
输入型(用户说话 → 文本) |
输出型(文本 → 语音播放) |
|
触发方式 |
用户主动触发 |
应用主动触发 |
|
关键回调 |
onResult(文本) |
onComplete(播报完成) |
|
最容易卡在哪 |
开始了但文本没带回来 |
能播但状态回不来 |
所以调试重点不可能完全一样。
项目中的真实场景
食界探味当前的语音能力实现:
|
文件 |
能力 |
调试关键 |
|---|---|---|
|
|
鸿蒙 ASR |
权限、引擎、回调、文本收口 |
|
|
鸿蒙 TTS |
参数、引擎复用、播报结束、stop |
|
|
Flutter ASR 通道 |
返回值、空结果处理 |
|
|
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);
}
});
});
}
调试检查:
|
检查项 |
预期 |
异常表现 |
|---|---|---|
|
引擎创建成功 |
|
|
|
引擎创建超时 |
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:onComplete、onStop、onError 是否都能收口
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 通道名一致 |
|
|
不一致则 MissingPluginException |
|
原生插件正确注册 |
EntryAbility 添加插件 |
EntryAbility 添加插件 |
没注册则找不到插件 |
|
页面退出时清理 |
无(引擎自动销毁) |
|
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)
关键代码位置
|
文件 |
调试关键 |
|---|---|
|
|
ASR 鸿蒙侧 |
|
|
TTS 鸿蒙侧 |
|
|
ASR Flutter 侧 |
|
|
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)
把两条链分开看,会比混着排查高效很多。一旦抓住各自最常出问题的点,定位效率会明显提升。
更多推荐


所有评论(0)