Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别引擎创建

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

上一篇搞定了权限申请,今天来讲语音识别引擎的创建 ------speechRecognizer.createEngine。这是整个语音识别流程中最关键的一步,引擎创建成功了,后面的监听、识别、停止都是顺水推舟的事。

说实话,createEngine这个API看起来很简单------就两个参数嘛。但实际用起来,参数格式、异步处理、异常捕获、能力检测,每一个环节都有讲究。我在适配过程中,光是language参数的格式转换就折腾了好一会儿------Dart层传过来的是zh_CN(下划线),Core Speech Kit要求的是zh-CN(连字符),差一个符号引擎就创建失败。

今天把activate方法中引擎创建相关的所有细节都讲透。

💡 本文对应源码FlutterSpeechPlugin.etsactivate方法,第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在创建引擎时需要做一些耗时操作

  1. 检查系统AI服务是否可用
  2. 加载语音识别模型
  3. 建立与AI引擎服务的连接
  4. 初始化音频采集管道

如果这些操作是同步的,会阻塞主线程,导致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_CNen_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  // 固定在线模式
});

为什么选在线模式:

  1. 准确率更高:在线模式使用云端大模型,识别准确率明显优于离线
  2. 词汇量更大:云端模型的词汇覆盖面更广
  3. 大多数场景有网络:手机用户通常都有网络连接

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篇)会详细讲解。简单来说就是注册onStartonResultonCompleteonError四个回调。

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中语音识别引擎的创建过程:

  1. createEngine参数:language(BCP 47格式)和online(1=在线,0=离线)
  2. locale转换convertLocale将下划线格式转为连字符格式
  3. 能力检测canIUse('SystemCapability.AI.SpeechRecognizer')
  4. 异步创建:必须用await等待引擎创建完成
  5. 异常处理:try-catch捕获所有异常,通过result.error返回
  6. 创建后操作:setupListener + 通知Dart层 + 返回成功

下一篇我们深入语音识别监听器的实现 ------setupListener方法中四个回调的详细解析。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
星空22231 小时前
【HarmonyOS】HarmonyOS React Native实战:手势交互配置优化
react native·交互·harmonyos
松叶似针1 小时前
Flutter三方库适配OpenHarmony【secure_application】— onMethodCall 方法分发实现
flutter·harmonyos
密瓜智能1 小时前
2025 年 HAMi 年度回顾 | 从 GPU 调度器到云原生 AI 基础设施的中流砥柱
人工智能·云原生
键盘鼓手苏苏1 小时前
Flutter for OpenHarmony:dart_ping 网络诊断的瑞士军刀(支持 ICMP Ping) 深度解析与鸿蒙适配指南
开发语言·网络·flutter·华为·rust·harmonyos
咚咚王者2 小时前
人工智能之视觉领域 计算机视觉 第七章 图像形态学操作
人工智能·计算机视觉
weixin_468466852 小时前
PyTorch导出ONNX格式分割模型及在C#中调用预测
人工智能·pytorch·深度学习·c#·跨平台·onnx·语义分割
AI英德西牛仔2 小时前
AIword排版
人工智能
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别启动与参数配置
人工智能·flutter·语音识别·harmonyos
软件算法开发2 小时前
基于火烈鸟搜索算法的LSTM网络模型(FSA-LSTM)的一维时间序列预测matlab仿真
人工智能·rnn·matlab·lstm·一维时间序列预测·火烈鸟搜索算法·fsa-lstm