【HarmonyOS AI赋能】AI字幕AICaption详解
【HarmonyOS AI赋能】AI字幕AICaption详解
一、前言
最近在做 HarmonyOS 应用开发时,需要集成 AI 字幕功能。
鸿蒙系统使用AI能力赋能,生成音频字幕的效果。简单说就是把音频流实时转成文字显示在界面上。
翻了官方文档,结合实际写的 demo 代码,踩了几个小坑后终于跑通了。今天就把整个过程拆解开,讲讲 AI 字幕相关的控件、接口怎么用,以及关键流程里要注意的细节,希望能帮到有同样需求的同学。
二、如何使用AI字幕控件
// 导入AI字幕相关组件和服务
import { AICaptionComponent, AICaptionOptions, AICaptionController, AudioData } from '@kit.SpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';const TAG = 'AI_CAPTION_DEMO'// 自定义日志工具类,方便记录不同级别的日志
class Logger {static info(...msg: string[]) {hilog.info(0x0000, TAG, msg.join())}static error(...msg: string[]) {hilog.error(0x0000, TAG, msg.join())}
}@Entry
@Component
struct Index {// AI字幕组件的配置选项private captionOption?: AICaptionOptions;// AI字幕组件的控制器,用于与组件交互private controller: AICaptionController = new AICaptionController();// 控制字幕组件是否显示的状态变量@State isShown: boolean = false;// 标记音频是否正在读取和处理中isReading: boolean = false;// 组件即将显示时调用的生命周期函数aboutToAppear(): void {// 初始化AI字幕的配置参数this.captionOption = {// 设置字幕初始透明度initialOpacity: 1,// 当字幕组件准备就绪时的回调函数onPrepared: () => {Logger.info('onPrepared')},// 当字幕组件发生错误时的回调函数onError: (error: BusinessError) => {Logger.error(`AICaption component error. Error code: ${error.code}, message: ${error.message}`)}}}// 读取PCM音频文件并发送到AI字幕组件的方法async readPcmAudio() {this.isReading = true;// 从资源管理器获取音频文件数据let fileData: Uint8Array | undefined;try {fileData = await this.getUIContext()?.getHostContext()?.resourceManager.getMediaContent($r('app.media.langhua').id);} catch (e) {Logger.info(`get fileData fail , msg ${e} `)}if (fileData === undefined) {return;}// 分块处理音频数据const bufferSize = 640;const byteLength = fileData.byteLength;let offset = 0;Logger.info(`Pcm data total bytes: ${byteLength.toString()}`)let startTime = new Date().getTime();// 循环读取音频数据块while (offset < byteLength) {let nextOffset = offset + bufferSize// 检查是否超出文件长度if (offset > byteLength) {this.isReading = false;return}// 截取当前块的音频数据const arrayBuffer = fileData.buffer.slice(offset, nextOffset);let data = new Uint8Array(arrayBuffer);const audioData: AudioData = {data: data}// 将音频数据写入AI字幕控制器if (this.controller) {try {this.controller.writeAudio(audioData)} catch (e) {Logger.error(`writeAudio exception`)}}// 移动偏移量并等待一段时间,模拟真实音频流offset = offset + bufferSize;const waitTime = bufferSize / 32await this.sleep(waitTime)}// 处理完成,记录总耗时let endTime = new Date().getTime()this.isReading = false;Logger.info(`Audio play time: ${JSON.stringify(endTime - startTime)}`)}// 辅助函数,用于暂停指定时间sleep(time: number): Promise<void> {return new Promise(resolve => setTimeout(resolve, time))}// 构建UI界面build() {Column({ space: 20 }) {// 切换字幕显示状态的按钮Button('切换字幕显示状态:' + (this.isShown ? '显示' : '隐藏')).backgroundColor('#B8BDA0').width(200).onClick(() => {this.isShown = !this.isShown;})// 读取并处理PCM音频的按钮Button('读取PCM音频').backgroundColor('#B8BDA0').width(200).onClick(() => {if (!this.isReading) {this.readPcmAudio()}})Divider()// AI字幕组件AICaptionComponent({isShown: this.isShown,controller: this.controller,options: this.captionOption}).width('100%').height(100)Divider()// 仅在字幕显示时显示提示文本if (this.isShown) {Text('上面是字幕区域').fontColor(Color.White)}}.width('100%').height('100%').padding(10).backgroundColor('#7A7D6A')}
}
首先会从本地资源加载PCM格式的音频文件,分块传给AI字幕控制器,最后通过AICaptionComponent
把识别出的字幕显示在界面上。整个过程依赖HarmonyOS的SpeechKit
(提供AI字幕核心能力)
AI字幕功能的核心就是三个“主角”:AICaptionComponent
(字幕显示控件)、AICaptionController
(字幕控制器,负责交互)、AICaptionOptions
(字幕配置项),再加上AudioData
(音频数据格式)。先一个个说清楚它们的作用和用法。
1、 字幕配置项:AICaptionOptions
这个是用来给AICaptionComponent
设置初始参数和回调的,必须在组件渲染前初始化(demo里放在aboutToAppear
生命周期里,很合理)。里面主要配置三个东西:
initialOpacity
:字幕区域的初始透明度,取值0~1。demo里设为1,就是完全不透明;如果想让字幕淡一点,比如0.8也可以。onPrepared
:字幕控件准备就绪的回调。控件初始化完成后会触发这个方法,我们可以在这里打印日志,或者做一些“准备好后的操作”(比如启用音频读取按钮)。onError
:错误回调。AI字幕识别过程中如果出问题(比如权限不够、音频格式不对),会通过这个接口返回BusinessError
,里面有错误码和错误信息——这个一定要加,不然出了问题都不知道在哪排查。
demo里的配置代码很标准,直接套着用就行,重点是错误回调里要把error.code
和error.message
打出来,方便调试。
2、字幕控制器:AICaptionController
这是整个AI字幕功能的“桥梁”——一边连音频数据,一边连显示控件。我们需要手动实例化它(demo里用private controller: AICaptionController = new AICaptionController()
),然后传给AICaptionComponent
。
它最核心的方法是writeAudio(audioData: AudioData)
:把音频数据(必须是AudioData
格式)传给AI引擎,引擎识别后会自动把字幕传给AICaptionComponent
显示。这里要注意两点:
- 控制器必须和
AICaptionComponent
绑定(通过组件的controller
参数),不然数据传过去也没法显示; - 调用
writeAudio
时要加try-catch,避免音频数据格式不对导致程序崩溃(demo里已经加了,这点很重要)。
3、字幕显示控件:AICaptionComponent
这是界面上实际显示字幕的“容器”,直接在build
方法里用就行。它需要三个参数:
isShown
:控制字幕是否显示(布尔值),demo里用@State
修饰的isShown
变量绑定,点击按钮切换状态;controller
:就是上面说的AICaptionController
,必须传,不然控件没法接收字幕数据;options
:前面配置的AICaptionOptions
,传初始化好的配置项就行。
另外要给控件设宽高(demo里width('100%')
、height(100)
),不然可能显示不出来。我一开始没设高度,结果字幕区域缩成一条线,后来调了高度才正常——这个细节别忽略。
4、 音频数据格式:AudioData
AI字幕只认AudioData
格式的音频数据,它的结构很简单,就一个data
字段,类型是Uint8Array
(二进制数据)。我们从本地读的PCM文件、或者从麦克风实时获取的音频流,都要转成这种格式才能传给控制器。
5、加载本地PCM音频文件
demo里是从entry\src\main\resources\base\media
路径加载PCM文件,核心是用resourceManager.getMediaContent()
方法。这里要注意两个点:
- 资源引用要正确:demo里写的是
$r('app.media.startIcon')
,这明显是个图标资源,实际开发中要改成自己的PCM文件,比如$r('app.media.test_pcm')
(前提是把test_pcm.pcm
放在media文件夹里); - 异步处理:
getMediaContent()
是异步方法,必须用await
,而且要包在try-catch里——万一文件没找到、或者权限不够,能捕获到错误,避免程序卡死。
加载成功后,会得到Uint8Array
类型的fileData
,这就是音频的二进制数据。
6、分块处理音频数据
为什么要分块?因为PCM文件可能很大,如果一次性把所有数据传给控制器,会导致两个问题:一是内存占用太高,二是AI识别跟不上(实际场景中音频是实时流,不是一次性加载的)。
demo里的处理逻辑很经典:
- 设一个
bufferSize
(640字节),每次取640字节的数据; - 用
offset
记录当前读取到的位置,循环截取fileData
的一部分(从offset
到offset+bufferSize
); - 把截取的部分转成
AudioData
格式,调用controller.writeAudio()
传给控制器; - 每次传完后,用
sleep
方法等一段时间(bufferSize/32
毫秒)——这个等待时间是模拟“实时音频流”的速度,不然读取速度比识别速度快,字幕会乱跳。
这里的bufferSize
和waitTime
不是固定的,要根据实际音频的采样率调整。比如采样率16kHz、16位单声道的PCM,每毫秒的字节数是32(16000Hz * 16bit / 8bit),所以640字节刚好是20毫秒的音频,waitTime
设20毫秒更准确——demo里用bufferSize/32
,算下来也是20毫秒,刚好匹配这个场景。
7、 第三步:字幕显示与状态控制
当控制器把音频数据传给AI引擎后,识别出的字幕会自动送到AICaptionComponent
——我们不需要手动处理字幕文本,控件会自己显示。
界面上的“切换字幕显示状态”按钮,本质是修改isShown
变量:isShown=true
时,控件显示字幕;isShown=false
时,控件隐藏。另外还加了isReading
变量,防止用户重复点击“读取PCM音频”按钮(点击后isReading
设为true,直到音频处理完才设为false),这个细节能提升用户体验,避免并发问题。