Administrator
发布于 2026-09-01 / 38 阅读
0
0

鸿蒙开发系列(第17篇):HiAI Foundation Kit端侧AI集成——模型推理与NLP能力调用

鸿蒙开发系列 · 专家篇

前言

前几篇我们构建了一个功能完整的语音备忘录应用:录音、播放、定位、搜索、账号登录、会员购买。但当我们审视这个产品的竞争力时,会发现一个尴尬的事实——市面上同类应用成百上千,用户为什么要选你?

答案是AI。如果用户的录音能自动转写成文字,如果应用能智能提取待办事项,如果能根据录音内容自动分类标签——这些能力就能形成差异化壁垒。过去,实现这些需要调用云端大模型API,意味着网络依赖、延迟、成本和隐私风险。但现在,端侧AI正在改变这个等式。

HarmonyOS提供的HiAI Foundation Kit让开发者可以在设备本地运行AI模型,无需联网即可完成图像识别、语音识别、文本理解等任务。本文将带你理解端侧AI的技术原理,并实战集成NLP和语音能力。


一、端侧AI的技术现状与价值

1.1 端侧AI vs 云端AI

维度

端侧AI

云端AI

延迟

毫秒级

百毫秒到秒级

网络依赖

无

必须联网

隐私

数据不出设备

数据上传服务器

成本

一次性模型部署

按调用量计费

模型大小

受设备存储限制(通常<1GB)

无限制

能力范围

特定任务优化

通用能力强

更新方式

随应用更新

服务端热更新

端侧AI不是云端AI的替代,而是互补。敏感数据本地处理,复杂任务云端协同,这才是合理的架构。

1.2 HiAI Foundation Kit 能力矩阵

HiAI Foundation Kit提供的能力分为几个大类:

能力域

具体功能

典型场景

计算机视觉

图像分类、物体检测、人脸检测、OCR

相册分类、扫码、证件识别

自然语言处理

文本分类、实体识别、文本生成

智能客服、内容审核、摘要

语音

语音识别(ASR)、语音合成(TTS)

语音输入、朗读、语音助手

预测

时序预测、用户行为预测

电量预测、推荐排序

对于我们的语音备忘录应用,最有价值的是语音转文字(ASR)和文本理解(NLP)。


二、语音转文字:ASR 实战

2.1 ASR 架构与流程

┌──────────────┐    音频流    ┌──────────────┐    特征提取    ┌──────────────┐
│   麦克风输入   │ ─────────→ │   音频预处理   │ ───────────→ │   编码器模型   │
│              │            │  降噪/VAD/分帧  │              │  (端侧运行)    │
└──────────────┘            └──────────────┘              └───────┬──────┘
                                                                  │
                                                                  ▼
┌──────────────┐    文本结果    ┌──────────────┐    解码    ┌──────────────┐
│   应用展示    │ ←─────────── │   后处理     │ ←──────── │   解码器模型   │
│              │   标点/格式化  │  标点/人名识别 │           │  (端侧运行)    │
└──────────────┘              └──────────────┘           └──────────────┘

2.2 语音识别接入

HarmonyOS的ASR能力通过 @kit.ASRKit 提供:

// entry/src/main/ets/utils/AsrEngine.ets
import { speechRecognizer } from '@kit.ASRKit';
import { BusinessError } from '@kit.BasicServicesKit';

export enum AsrState {
  IDLE = 'idle',
  INITIALIZING = 'initializing',
  LISTENING = 'listening',
  RECOGNIZING = 'recognizing',
  COMPLETED = 'completed',
  ERROR = 'error'
}

export class AsrEngine {
  private engine: speechRecognizer.SpeechRecognitionEngine | null = null;
  @State state: AsrState = AsrState.IDLE;
  private callbacks: {
    onResult?: (text: string, isFinal: boolean) => void;
    onError?: (code: number, msg: string) => void;
    onStateChange?: (state: AsrState) => void;
  } = {};

  setCallbacks(callbacks: typeof this.callbacks) {
    this.callbacks = callbacks;
  }

  private setState(state: AsrState) {
    this.state = state;
    this.callbacks.onStateChange?.(state);
  }

  // 初始化引擎
  async initialize(): Promise<void> {
    try {
      this.setState(AsrState.INITIALIZING);

      const initParams: speechRecognizer.EngineCreateParam = {
        context: getContext(),
        locale: 'zh-CN',
        // 是否开启离线识别
        enableOffline: true
      };

      this.engine = await speechRecognizer.createSpeechRecognizer(initParams);
      this.setupEngineCallbacks();
      this.setState(AsrState.IDLE);
    } catch (err) {
      const error = err as BusinessError;
      console.error('ASR引擎初始化失败:', error);
      this.setState(AsrState.ERROR);
      throw err;
    }
  }

  private setupEngineCallbacks() {
    if (!this.engine) return;

    // 识别结果回调(可能多次触发,isFinal=false是中间结果)
    this.engine.on('result', (result: speechRecognizer.SpeechRecognitionResult) => {
      const text = result.result;
      const isFinal = result.isLast;

      console.info(`识别结果: ${text}, 是否最终: ${isFinal}`);
      this.callbacks.onResult?.(text, isFinal);

      if (isFinal) {
        this.setState(AsrState.COMPLETED);
      } else {
        this.setState(AsrState.RECOGNIZING);
      }
    });

    // 错误回调
    this.engine.on('error', (error: BusinessError) => {
      console.error('ASR错误:', error.code, error.message);
      this.callbacks.onError?.(error.code, error.message);
      this.setState(AsrState.ERROR);
    });

    // 开始监听回调
    this.engine.on('start', () => {
      console.info('开始监听');
      this.setState(AsrState.LISTENING);
    });

    // 完成回调
    this.engine.on('complete', () => {
      console.info('识别完成');
      this.setState(AsrState.COMPLETED);
    });
  }

  // 开始识别(实时流式)
  async startListening(): Promise<void> {
    if (!this.engine) throw new Error('引擎未初始化');

    const params: speechRecognizer.StartParams = {
      // 识别场景
      scene: speechRecognizer.SceneType.SCENE_CHAT,
      // 是否开启标点
      enablePunctuation: true,
      // 是否开启数字智能转换
      enableNumberConvert: true
    };

    await this.engine.startListening(params);
  }

  // 停止识别
  async stopListening(): Promise<void> {
    if (!this.engine) return;
    await this.engine.stopListening();
  }

  // 取消识别
  async cancel(): Promise<void> {
    if (!this.engine) return;
    await this.engine.cancel();
    this.setState(AsrState.IDLE);
  }

  // 释放引擎
  async release(): Promise<void> {
    if (this.engine) {
      await this.engine.destroy();
      this.engine = null;
    }
    this.setState(AsrState.IDLE);
  }
}

2.3 录音文件转文字(非实时)

对于已有的录音文件,可以使用文件识别模式:

// 文件转文字
async transcribeAudioFile(filePath: string): Promise<string> {
  if (!this.engine) throw new Error('引擎未初始化');

  return new Promise((resolve, reject) => {
    let finalText = '';

    // 临时设置回调收集结果
    const originalCallback = this.callbacks.onResult;
    this.callbacks.onResult = (text, isFinal) => {
      if (isFinal) {
        finalText = text;
        this.callbacks.onResult = originalCallback;
        resolve(finalText);
      }
    };

    this.engine?.startListening({
      audioSource: {
        type: speechRecognizer.AudioSourceType.FILE,
        filePath: filePath
      },
      enablePunctuation: true
    }).catch(reject);
  });
}

2.4 语音转写页面

// entry/src/main/ets/pages/TranscribePage.ets
import { AsrEngine, AsrState } from '../utils/AsrEngine';

@Entry
@Component
struct TranscribePage {
  private asrEngine: AsrEngine = new AsrEngine();
  @State state: AsrState = AsrState.IDLE;
  @State transcript: string = '';
  @State interimText: string = '';

  aboutToAppear() {
    this.asrEngine.setCallbacks({
      onResult: (text, isFinal) => {
        if (isFinal) {
          this.transcript += (this.transcript ? '\n' : '') + text;
          this.interimText = '';
        } else {
          this.interimText = text;
        }
      },
      onStateChange: (state) => this.state = state,
      onError: (code, msg) => {
        console.error(`ASR错误 ${code}: ${msg}`);
      }
    });
    this.asrEngine.initialize();
  }

  aboutToDisappear() {
    this.asrEngine.release();
  }

  private async toggleListening() {
    if (this.state === AsrState.LISTENING || this.state === AsrState.RECOGNIZING) {
      await this.asrEngine.stopListening();
    } else {
      this.interimText = '';
      await this.asrEngine.startListening();
    }
  }

  build() {
    Column({ space: 16 }) {
      Text('语音转文字')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 32 })

      // 转写结果区域
      Column() {
        Scroll() {
          Column({ space: 8 }) {
            Text(this.transcript)
              .fontSize(16)
              .width('100%')
              .lineHeight(24)

            if (this.interimText) {
              Text(this.interimText)
                .fontSize(16)
                .fontColor('#2196F3')
                .width('100%')
                .lineHeight(24)
            }
          }
          .width('100%')
          .padding(16)
        }
        .width('100%')
        .layoutWeight(1)
      }
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#F5F5F5')
      .borderRadius(12)
      .margin(16)

      // 状态指示
      Text(this.getStateText())
        .fontSize(14)
        .fontColor(this.state === AsrState.LISTENING ? '#F44336' : '#888')

      // 控制按钮
      Button(this.state === AsrState.LISTENING ? '停止' : '开始说话')
        .width(160)
        .height(56)
        .fontSize(18)
        .backgroundColor(this.state === AsrState.LISTENING ? '#F44336' : '#4CAF50')
        .onClick(() => this.toggleListening())
        .margin({ bottom: 32 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }

  private getStateText(): string {
    switch (this.state) {
      case AsrState.IDLE: return '点击开始说话';
      case AsrState.LISTENING: return '正在聆听...';
      case AsrState.RECOGNIZING: return '识别中...';
      case AsrState.COMPLETED: return '识别完成';
      case AsrState.ERROR: return '识别出错';
      default: return '';
    }
  }
}

三、NLP 文本理解能力

3.1 HiAI NLP 能力介绍

HiAI Foundation Kit的NLP模块提供以下能力:

能力

输入

输出

用途

文本分类

一段文本

类别标签 + 置信度

内容审核、自动标签

实体识别

一段文本

人名/地名/机构名/时间等

信息抽取、待办提取

关键词提取

一段文本

关键词列表

摘要生成、标签推荐

文本相似度

两段文本

相似度分数

去重、推荐

情感分析

一段文本

情感倾向 + 强度

评论分析、用户反馈

3.2 文本分类与实体识别

// entry/src/main/ets/utils/NlpEngine.ets
import { hiai } from '@kit.HiAIFoundationKit';

export interface ClassificationResult {
  label: string;
  confidence: number;
}

export interface Entity {
  text: string;
  type: string; // PERSON / LOCATION / ORGANIZATION / TIME / etc.
  start: number;
  end: number;
}

export class NlpEngine {
  private textClassifier: hiai.TextClassifier | null = null;
  private entityExtractor: hiai.EntityExtractor | null = null;

  async initialize(): Promise<void> {
    // 初始化文本分类器
    this.textClassifier = await hiai.createTextClassifier({
      context: getContext(),
      modelType: hiai.TextClassifierType.GENERAL
    });

    // 初始化实体提取器
    this.entityExtractor = await hiai.createEntityExtractor({
      context: getContext()
    });
  }

  // 文本分类
  async classify(text: string): Promise<ClassificationResult[]> {
    if (!this.textClassifier) throw new Error('分类器未初始化');

    const results = await this.textClassifier.classify(text);
    return results.map(r => ({
      label: r.label,
      confidence: r.confidence
    }));
  }

  // 实体识别
  async extractEntities(text: string): Promise<Entity[]> {
    if (!this.entityExtractor) throw new Error('实体提取器未初始化');

    const results = await this.entityExtractor.extract(text);
    return results.map(r => ({
      text: r.text,
      type: r.type,
      start: r.startIndex,
      end: r.endIndex
    }));
  }

  // 智能提取待办事项
  async extractTodos(text: string): Promise<string[]> {
    const entities = await this.extractEntities(text);

    // 筛选时间相关实体,结合上下文提取待办
    const timeEntities = entities.filter(e =>
      e.type === 'TIME' || e.type === 'DATE'
    );

    const todos: string[] = [];

    // 简单启发式:包含时间表达式的句子可能是待办
    const sentences = text.split(/[。!?\n]/);
    sentences.forEach(sentence => {
      const hasTime = timeEntities.some(e => sentence.includes(e.text));
      const hasAction = /(要|需要|记得|别忘了|必须|应该).+/.test(sentence);

      if (hasTime || hasAction) {
        todos.push(sentence.trim());
      }
    });

    return todos;
  }

  // 释放资源
  async release(): Promise<void> {
    await this.textClassifier?.release();
    await this.entityExtractor?.release();
    this.textClassifier = null;
    this.entityExtractor = null;
  }
}

3.3 智能分析录音内容

// entry/src/main/ets/utils/RecordingAnalyzer.ets
import { AsrEngine } from './AsrEngine';
import { NlpEngine, ClassificationResult, Entity } from './NlpEngine';

export interface RecordingAnalysis {
  transcript: string;
  categories: ClassificationResult[];
  entities: Entity[];
  todos: string[];
  summary: string;
}

export class RecordingAnalyzer {
  private asrEngine: AsrEngine;
  private nlpEngine: NlpEngine;

  constructor() {
    this.asrEngine = new AsrEngine();
    this.nlpEngine = new NlpEngine();
  }

  async initialize(): Promise<void> {
    await Promise.all([
      this.asrEngine.initialize(),
      this.nlpEngine.initialize()
    ]);
  }

  // 分析录音文件
  async analyzeRecording(filePath: string): Promise<RecordingAnalysis> {
    // 1. 语音转文字
    const transcript = await this.asrEngine.transcribeAudioFile(filePath);

    // 2. NLP分析
    const [categories, entities, todos] = await Promise.all([
      this.nlpEngine.classify(transcript),
      this.nlpEngine.extractEntities(transcript),
      this.nlpEngine.extractTodos(transcript)
    ]);

    // 3. 生成摘要(简单版:取前50字)
    const summary = transcript.length > 50
      ? transcript.substring(0, 50) + '...'
      : transcript;

    return {
      transcript,
      categories,
      entities,
      todos,
      summary
    };
  }

  async release(): Promise<void> {
    await Promise.all([
      this.asrEngine.release(),
      this.nlpEngine.release()
    ]);
  }
}

四、端侧AI的性能与优化

4.1 模型加载策略

端侧AI模型通常占用数十到数百MB存储,加载到内存后又占用大量RAM。优化策略:

策略

做法

效果

懒加载

首次使用时再初始化模型

减少应用启动时间

预加载

在空闲时提前加载

减少首次识别等待

模型量化

使用INT8量化模型

减少50%+内存占用

按需释放

长时间不用时释放模型

降低后台内存压力

// 懒加载 + 超时释放的封装
export class LazyAiEngine<T> {
  private engine: T | null = null;
  private factory: () => Promise<T>;
  private releaseFn: (engine: T) => Promise<void>;
  private idleTimer: number = -1;
  private readonly IDLE_TIMEOUT = 5 * 60 * 1000; // 5分钟空闲后释放

  constructor(
    factory: () => Promise<T>,
    releaseFn: (engine: T) => Promise<void>
  ) {
    this.factory = factory;
    this.releaseFn = releaseFn;
  }

  async get(): Promise<T> {
    this.resetIdleTimer();

    if (!this.engine) {
      this.engine = await this.factory();
    }
    return this.engine;
  }

  private resetIdleTimer() {
    if (this.idleTimer !== -1) {
      clearTimeout(this.idleTimer);
    }
    this.idleTimer = setTimeout(() => {
      this.release();
    }, this.IDLE_TIMEOUT);
  }

  private async release() {
    if (this.engine) {
      await this.releaseFn(this.engine);
      this.engine = null;
    }
  }
}

4.2 端侧AI的局限与应对

局限

说明

应对策略

模型能力有限

端侧模型参数量小,理解能力弱于云端大模型

简单任务本地做,复杂任务 fallback 到云端

设备差异大

不同设备的NPU/CPU性能差异显著

运行时检测算力,动态选择模型版本

模型体积大

一个ASR模型可能占用200MB+

按需下载,使用模型压缩技术

更新不灵活

模型随应用版本更新

使用HarmonyOS的模型动态加载能力


五、小结与下篇预告

本文带你进入了HarmonyOS的AI能力域:

  1. 端侧AI定位:延迟低、隐私好、成本低,与云端AI互补

  2. ASR语音识别:实时流式识别、文件转写、状态管理

  3. NLP文本理解:文本分类、实体识别、智能待办提取

  4. 性能优化:懒加载、量化模型、空闲释放

端侧AI让我们的语音备忘录从"录音工具"升级为"智能语音助手"——自动转写、智能分类、待办提取,这些能力构成了产品的差异化壁垒。

但鸿蒙的野心不止于单设备。HarmonyOS的核心理念是"万物互联",而ArkUI-X正是这一理念的延伸——一套ArkTS代码,同时运行在鸿蒙手机、Android、iOS甚至Web上。第18篇将深入ArkUI-X跨平台开发,讲解工程结构、平台适配、条件编译和发布策略,让你的应用真正突破设备边界。


参考阅读


评论