前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
上一篇搞定了权限申请,今天来讲语音识别引擎的创建 ------speechRecognizer.createEngine。这是整个语音识别流程中最关键的一步,引擎创建成功了,后面的监听、识别、停止都是顺水推舟的事。
说实话,createEngine这个API看起来很简单------就两个参数嘛。但实际用起来,参数格式、异步处理、异常捕获、能力检测,每一个环节都有讲究。我在适配过程中,光是language参数的格式转换就折腾了好一会儿------Dart层传过来的是zh_CN(下划线),Core Speech Kit要求的是zh-CN(连字符),差一个符号引擎就创建失败。
今天把activate方法中引擎创建相关的所有细节都讲透。
💡 本文对应源码 :
FlutterSpeechPlugin.ets的activate方法,第80-131行。
一、speechRecognizer.createEngine 参数详解
1.1 API签名
typescript
speechRecognizer.createEngine(params: CreateEngineParams): Promise<SpeechRecognitionEngine>
这是一个异步方法 ,返回Promise。必须用await等待引擎创建完成。

1.2 CreateEngineParams 参数
typescript
interface CreateEngineParams {
language: string; // 识别语言,BCP 47格式
online: number; // 识别模式:1=在线,0=离线
}
flutter_speech中的调用:
typescript
this.asrEngine = await speechRecognizer.createEngine({
language: language, // "zh-CN"
online: 1 // 在线识别
});
| 参数 | 类型 | 必填 | 说明 | flutter_speech的值 |
|---|---|---|---|---|
| language | string | ✅ | BCP 47语言代码 | 由Dart层传入,经convertLocale转换 |
| online | number | ✅ | 1=在线,0=离线 | 固定为1 |
1.3 为什么是异步的
Android的SpeechRecognizer.createSpeechRecognizer()是同步的,为什么OpenHarmony的createEngine是异步的?
原因是Core Speech Kit在创建引擎时需要做一些耗时操作:
- 检查系统AI服务是否可用
- 加载语音识别模型
- 建立与AI引擎服务的连接
- 初始化音频采集管道
如果这些操作是同步的,会阻塞主线程,导致UI卡顿。所以设计成异步是合理的。
typescript
// ✅ 正确:用await等待
this.asrEngine = await speechRecognizer.createEngine({...});
console.info(TAG, 'engine created'); // 引擎已就绪
// ❌ 错误:不等待直接使用
speechRecognizer.createEngine({...}); // 返回Promise,引擎还没创建好
this.asrEngine.startListening(...); // asrEngine是undefined,崩溃!
📌 这也是为什么
activate方法是async的 :因为内部需要await两个异步操作------权限申请和引擎创建。
二、language 参数格式转换:locale 到 BCP47
2.1 问题背景
Dart层传过来的locale格式是下划线分隔 的(如zh_CN),这是Dart/Flutter的标准格式。但Core Speech Kit要求的是BCP 47格式 (连字符分隔,如zh-CN)。
Dart层: "zh_CN" "en_US" "fr_FR"
Core Kit: "zh-CN" "en-US" "fr-FR"
2.2 convertLocale 实现
flutter_speech的转换逻辑非常简洁:
typescript
private convertLocale(locale: string): string {
return locale.replace('_', '-');
}
就一行代码,把下划线替换成连字符。
2.3 为什么不用更复杂的转换
你可能会想:是不是应该做更完整的BCP 47解析?比如处理zh-Hans-CN这种三段式格式?
实际上不需要。flutter_speech的Dart层只传两段式的locale(如zh_CN、en_US),而且Core Speech Kit目前只支持中文,所以简单的replace就够了。
但如果你在做一个更通用的插件,可能需要更健壮的转换:
typescript
// 更健壮的转换(flutter_speech不需要这么复杂)
private convertLocale(locale: string): string {
// 处理各种可能的格式
// "zh_CN" → "zh-CN"
// "zh" → "zh"
// "zh_Hans_CN" → "zh-Hans-CN"
return locale.replace(/_/g, '-'); // 全局替换所有下划线
}
💡 注意 :flutter_speech用的是
replace('_', '-')(不带g标志),只替换第一个下划线。对于两段式locale来说足够了。如果有三段式locale,需要用正则的全局替换replace(/_/g, '-')。
2.4 locale转换的调用位置
typescript
// activate方法中的调用链
const language = this.convertLocale(locale); // "zh_CN" → "zh-CN"
if (!this.isSupportedLocale(language)) {
// 语言不支持,返回错误
result.error('ERROR_LANGUAGE_NOT_SUPPORTED', ...);
return;
}
// 用转换后的language创建引擎
this.asrEngine = await speechRecognizer.createEngine({
language: language, // "zh-CN"
online: 1
});
三、online 参数:在线识别模式配置
3.1 参数含义
| 值 | 模式 | 网络要求 | 准确率 | 延迟 |
|---|---|---|---|---|
| 1 | 在线识别 | 需要网络 | 高 | 取决于网络 |
| 0 | 离线识别 | 不需要 | 中等 | 较低 |
3.2 flutter_speech的选择
flutter_speech硬编码了online: 1(在线模式):
typescript
this.asrEngine = await speechRecognizer.createEngine({
language: language,
online: 1 // 固定在线模式
});
为什么选在线模式:
- 准确率更高:在线模式使用云端大模型,识别准确率明显优于离线
- 词汇量更大:云端模型的词汇覆盖面更广
- 大多数场景有网络:手机用户通常都有网络连接
3.3 未来改进方向
如果要支持用户选择在线/离线模式,可以通过Dart层传参:
dart
// Dart层(未来可能的改进)
Future activate(String locale, {bool online = true}) =>
_channel.invokeMethod("speech.activate", {
'locale': locale,
'online': online,
});
typescript
// 原生端接收参数
case "speech.activate":
const args = call.args as Record<string, Object>;
const locale = String(args['locale']);
const online = args['online'] as boolean;
this.activate(locale, online ? 1 : 0, result);
break;
📌 当前flutter_speech的Dart层只传一个locale字符串 ,所以原生端用
String(call.args)直接获取。如果要传多个参数,需要改成Map格式。
四、SystemCapability.AI.SpeechRecognizer 能力检测
4.1 为什么需要能力检测
不是所有OpenHarmony设备都支持语音识别。在创建引擎之前,应该先检测设备是否具备这个能力:
typescript
if (!canIUse('SystemCapability.AI.SpeechRecognizer')) {
result.error('ERROR_NO_SPEECH_RECOGNITION_AVAILABLE',
'Device does not support speech recognition', null);
return;
}
4.2 canIUse API
canIUse是OpenHarmony的全局函数,用于检测系统能力:
typescript
function canIUse(syscap: string): boolean
| 参数 | 说明 | 示例 |
|---|---|---|
| syscap | 系统能力标识 | "SystemCapability.AI.SpeechRecognizer" |
返回true表示设备支持该能力,false表示不支持。
4.3 哪些设备可能不支持
| 设备类型 | 是否支持语音识别 | 原因 |
|---|---|---|
| 手机(旗舰) | ✅ 通常支持 | 有AI芯片和麦克风 |
| 手机(入门) | ⚠️ 可能不支持 | 硬件能力不足 |
| 平板 | ✅ 通常支持 | 和手机类似 |
| 智慧屏 | ⚠️ 取决于型号 | 部分型号无麦克风 |
| 穿戴设备 | ❌ 通常不支持 | 算力和存储不足 |
| 开发板 | ❌ 通常不支持 | 无AI服务 |
4.4 能力检测的位置
flutter_speech把能力检测放在权限申请之后、引擎创建之前:
typescript
// 1. 权限申请
// ...
// 2. 能力检测 ← 在这里
if (!canIUse('SystemCapability.AI.SpeechRecognizer')) {
result.error('ERROR_NO_SPEECH_RECOGNITION_AVAILABLE', ...);
return;
}
// 3. 语言校验
// ...
// 4. 引擎创建
// ...
为什么不把能力检测放在最前面?因为即使设备支持语音识别,没有权限也用不了。先检查权限可以更早地给用户反馈。
💡 不过这个顺序见仁见智。有人觉得应该先检测能力再申请权限------如果设备不支持,就没必要弹权限弹窗了。两种方式都有道理,flutter_speech选择了先权限后能力的顺序。
五、引擎创建失败的异常处理与降级方案
5.1 可能的失败原因
createEngine可能因为多种原因失败,抛出异常:
| 失败原因 | 错误表现 | 发生概率 |
|---|---|---|
| 语言不支持 | 异常:language not supported | 高(非中文时) |
| 网络不可用 | 异常:network error | 中(在线模式) |
| AI服务未启动 | 异常:service not available | 低 |
| 系统资源不足 | 异常:resource exhausted | 极低 |
| 引擎已存在 | 异常:engine already exists | 低 |
5.2 flutter_speech的异常处理
typescript
try {
this.asrEngine = await speechRecognizer.createEngine({
language: language,
online: 1
});
console.info(TAG, `engine created successfully`);
this.setupListener();
this.channel?.invokeMethod('speech.onSpeechAvailability', true);
result.success(true);
} catch (e) {
console.error(TAG, `activate error: ${JSON.stringify(e)}`);
result.error('SPEECH_ACTIVATION_ERROR',
`Failed to activate speech recognition: ${JSON.stringify(e)}`, null);
}
整个activate方法被try-catch包裹,任何异常都会被捕获并通过result.error返回给Dart层。
5.3 错误信息的序列化
注意异常对象e的序列化方式:
typescript
// 用JSON.stringify序列化错误对象
console.error(TAG, `activate error: ${JSON.stringify(e)}`);
为什么用JSON.stringify而不是e.message?因为OpenHarmony的异常对象结构可能和标准的Error不同,JSON.stringify可以输出完整的错误信息,包括错误码和详细描述。
typescript
// 典型的错误对象结构
{
"code": 1002003,
"message": "Language not supported"
}
5.4 降级方案
如果在线模式创建失败,可以尝试降级到离线模式(flutter_speech当前未实现,但这是一个好的改进方向):
typescript
// 降级方案示例
private async createEngineWithFallback(language: string): Promise<boolean> {
// 先尝试在线
try {
this.asrEngine = await speechRecognizer.createEngine({
language: language,
online: 1
});
console.info(TAG, 'online engine created');
return true;
} catch (onlineErr) {
console.warn(TAG, `online failed: ${JSON.stringify(onlineErr)}`);
}
// 降级到离线
try {
this.asrEngine = await speechRecognizer.createEngine({
language: language,
online: 0
});
console.info(TAG, 'offline engine created (fallback)');
return true;
} catch (offlineErr) {
console.error(TAG, `offline also failed: ${JSON.stringify(offlineErr)}`);
return false;
}
}
5.5 重复创建的处理
如果用户多次调用activate,需要先销毁旧引擎再创建新的:
typescript
// 当前flutter_speech没有显式处理这种情况
// 建议改进:
private async activate(locale: string, result: MethodResult): Promise<void> {
// 如果已有引擎,先销毁
if (this.asrEngine) {
console.info(TAG, 'destroying existing engine before creating new one');
this.destroyEngine();
}
// 创建新引擎...
}
🤦 实际踩坑:我测试时连续调用了两次activate,第二次创建引擎时偶尔会失败。后来加了先销毁旧引擎的逻辑就好了。虽然Core Speech Kit理论上应该能处理这种情况,但保险起见还是自己管理好引擎的生命周期。
六、activate 方法的完整流程
6.1 流程图
activate(locale, result)
│
├── 1. 检查abilityContext
│ └── null → error('SPEECH_CONTEXT_ERROR') → return
│
├── 2. 申请麦克风权限
│ └── denied → error('SPEECH_PERMISSION_DENIED') → return
│
├── 3. 检测设备能力
│ └── 不支持 → error('ERROR_NO_SPEECH_RECOGNITION_AVAILABLE') → return
│
├── 4. 转换locale格式
│ └── convertLocale("zh_CN") → "zh-CN"
│
├── 5. 校验语言支持
│ └── 不支持 → error('ERROR_LANGUAGE_NOT_SUPPORTED') → return
│
├── 6. 创建引擎 (await)
│ └── 失败 → catch → error('SPEECH_ACTIVATION_ERROR') → return
│
├── 7. 设置监听器
│ └── setupListener()
│
├── 8. 通知Dart层
│ └── channel.invokeMethod('speech.onSpeechAvailability', true)
│
└── 9. 返回成功
└── result.success(true)
6.2 完整源码(带注释)
typescript
private async activate(locale: string, result: MethodResult): Promise<void> {
try {
console.info(TAG, `activate called with locale: ${locale}`);
// ========== 第1步:权限申请 ==========
if (this.abilityContext) {
console.info(TAG, `requesting microphone permission...`);
const atManager = abilityAccessCtrl.createAtManager();
const grantResult = await atManager.requestPermissionsFromUser(
this.abilityContext, ['ohos.permission.MICROPHONE']
);
const allGranted = grantResult.authResults.every(
(status: number) => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
);
console.info(TAG, `permission granted: ${allGranted}`);
if (!allGranted) {
result.error('SPEECH_PERMISSION_DENIED', 'Microphone permission denied', null);
return;
}
} else {
console.error(TAG, `abilityContext is null`);
result.error('SPEECH_CONTEXT_ERROR', 'UIAbilityContext not available', null);
return;
}
// ========== 第2步:能力检测 ==========
if (!canIUse('SystemCapability.AI.SpeechRecognizer')) {
result.error('ERROR_NO_SPEECH_RECOGNITION_AVAILABLE',
'Device does not support speech recognition', null);
return;
}
// ========== 第3步:语言校验 ==========
const language = this.convertLocale(locale);
if (!this.isSupportedLocale(language)) {
result.error('ERROR_LANGUAGE_NOT_SUPPORTED',
`Language "${locale}" is not supported on HarmonyOS.`, null);
return;
}
// ========== 第4步:创建引擎 ==========
console.info(TAG, `creating engine with language: ${language}`);
this.asrEngine = await speechRecognizer.createEngine({
language: language,
online: 1
});
console.info(TAG, `engine created successfully`);
// ========== 第5步:设置监听器 ==========
this.setupListener();
// ========== 第6步:通知Dart层 ==========
this.channel?.invokeMethod('speech.onSpeechAvailability', true);
result.success(true);
} catch (e) {
console.error(TAG, `activate error: ${JSON.stringify(e)}`);
result.error('SPEECH_ACTIVATION_ERROR',
`Failed to activate speech recognition: ${JSON.stringify(e)}`, null);
}
}
6.3 各步骤的耗时分析
| 步骤 | 预估耗时 | 是否异步 | 说明 |
|---|---|---|---|
| 权限申请 | 0-5秒 | ✅ await | 取决于用户操作速度 |
| 能力检测 | <1ms | ❌ 同步 | 系统调用,极快 |
| 语言校验 | <1ms | ❌ 同步 | 字符串比较 |
| 引擎创建 | 500ms-3秒 | ✅ await | 取决于网络和系统状态 |
| 设置监听器 | <1ms | ❌ 同步 | 注册回调 |
| 通知Dart | <1ms | ❌ 异步发送 | 不等待结果 |
📌 整个activate方法的总耗时:最快约500ms(权限已授予+引擎快速创建),最慢可能超过5秒(首次权限弹窗+网络慢)。Dart层应该在调用activate时显示loading状态。
七、引擎创建成功后的操作
7.1 setupListener
引擎创建成功后,立即设置监听器:
typescript
this.setupListener();
这一步在下一篇(第13篇)会详细讲解。简单来说就是注册onStart、onResult、onComplete、onError四个回调。
7.2 通知Dart层
typescript
this.channel?.invokeMethod('speech.onSpeechAvailability', true);
这行代码通过MethodChannel向Dart层发送一个事件,告诉Dart层"语音识别引擎已就绪"。Dart层收到后会调用availabilityHandler(true)回调。
7.3 返回结果
typescript
result.success(true);
最后通过result.success(true)告诉Dart层activate方法执行成功。Dart层的Future会以true完成。
💡 注意区分两种通知 :
result.success(true)是对activate方法调用的直接响应(同步语义),channel.invokeMethod('speech.onSpeechAvailability', true)是一个异步事件通知。两者都需要,因为Dart层可能分别监听方法返回值和事件回调。
八、与Android引擎创建的对比
8.1 代码对比
Android:
java
private void activate(String locale, MethodChannel.Result result) {
// 同步创建
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(activity);
speechRecognizer.setRecognitionListener(recognitionListener);
// 检查是否可用
if (SpeechRecognizer.isRecognitionAvailable(activity)) {
channel.invokeMethod("speech.onSpeechAvailability", true);
result.success(true);
} else {
result.success(false);
}
}
OpenHarmony:
typescript
private async activate(locale: string, result: MethodResult): Promise<void> {
// 异步创建
this.asrEngine = await speechRecognizer.createEngine({
language: language,
online: 1
});
this.setupListener();
this.channel?.invokeMethod('speech.onSpeechAvailability', true);
result.success(true);
}
8.2 关键差异
| 差异点 | Android | OpenHarmony |
|---|---|---|
| 创建方式 | 同步 | 异步(await) |
| 语言参数 | 不在创建时指定 | 创建时指定 |
| 在线/离线 | 不在创建时指定 | 创建时指定 |
| 能力检测 | isRecognitionAvailable() | canIUse() |
| 权限申请 | 分离在另一个方法 | 集成在activate中 |
🎯 OpenHarmony的设计更"前置":语言和模式在创建引擎时就确定了,而Android是在startListening时通过Intent指定。这意味着OpenHarmony如果要切换语言,需要重新创建引擎。
总结
本文详细讲解了flutter_speech中语音识别引擎的创建过程:
- createEngine参数:language(BCP 47格式)和online(1=在线,0=离线)
- locale转换 :
convertLocale将下划线格式转为连字符格式 - 能力检测 :
canIUse('SystemCapability.AI.SpeechRecognizer') - 异步创建:必须用await等待引擎创建完成
- 异常处理:try-catch捕获所有异常,通过result.error返回
- 创建后操作:setupListener + 通知Dart层 + 返回成功
下一篇我们深入语音识别监听器的实现 ------setupListener方法中四个回调的详细解析。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源: