鸿蒙(HarmonyOS) 原生智能语音识别

224 阅读7分钟

背景

公司很多业务场景使用到了语音识别功能,当时我们的语音团队自研了语音识别模型,方案是云端模型加端侧SDK交互,端侧负责做语音采集、VAD、opus编码,实时传输给云端,云端识别后返回识别结果。这些业务场景在适配鸿蒙的过程发现HarmonyOS 原生智能中提供了本地语音识别SDK,动手封装一波。

场景介绍

原生语音识别能力支持两种模式:

  • 短语音模式(不超过60s)
  • 长语音模式(不超过8h)

开发步骤

1.引擎初始化

let asrEngine: speechRecognizer.SpeechRecognitionEngine;
let requestId: string = '123456';
// 创建引擎,通过callback形式返回
// 设置创建引擎参数
let extraParam: Record<string, Object> = {"locate": "CN", "recognizerMode": "short"};
let initParamsInfo: speechRecognizer.CreateEngineParams = {
  language: 'zh-CN',
  online: 1,
  extraParams: extraParam
};
// 调用createEngine方法
speechRecognizer.createEngine(initParamsInfo, (err: BusinessError, speechRecognitionEngine: speechRecognizer.SpeechRecognitionEngine) => {
  if (!err) {
    console.info('createEngine is succeeded');
    //接收创建引擎的实例
    asrEngine = speechRecognitionEngine;
  } else {
    //无法创建引擎时返回错误码1002200008,原因:引擎正在销毁中
    console.error("errCode: " + err.code +  " errMessage: " + err.message);
  }
});

tips:

  1. // 无法创建引擎时返回错误码1002200001,原因:语种不支持、模式不支持、初始化超时、资源不存在等导致创建引擎失败
  2. // 无法创建引擎时返回错误码1002200006,原因:引擎正在忙碌中,一般多个应用同时调用语音识别引擎时触发
  3. // 无法创建引擎时返回错误码1002200008,原因:引擎正在销毁中

2.实例化RecognitionListener对象,调用setListener方法设置回调

// 创建回调对象
let setListener: speechRecognizer.RecognitionListener = {
  // 开始识别成功回调
  onStart(sessionId: string, eventMessage: string) {
    console.info("onStart sessionId: " + sessionId + "eventMessage: " + eventMessage);
  },
  // 事件回调
  onEvent(sessionId: string, eventCode: number, eventMessage: string) {
    console.info("onEvent sessionId: " + sessionId + "eventCode: " + eventCode + "eventMessage: " + eventMessage);
  },
  // 识别结果回调,包括中间结果和最终结果
  onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult) {
    console.info("onResult sessionId: " + sessionId);
  },
  // 识别完成回调
  onComplete(sessionId: string, eventMessage: string) {
    console.info("onComplete sessionId: " + sessionId + "eventMessage: " + eventMessage);
  },
  // 错误回调,错误码通过本方法返回
  // 如:返回错误码1002200006,识别引擎正忙,引擎正在识别中
  // 更多错误码请参考错误码参考
  onError(sessionId: string, errorCode: number, errorMessage: string) {
    console.error("onError sessionId: " + sessionId + "errorCode: " + errorCode + "errorMessage: " + errorMessage);
  }
}
// 设置回调
asrEngine.setListener(setListener);

3.开始识别

let audioParam: speechRecognizer.AudioInfo = {audioType: 'pcm', sampleRate: 16000, soundChannel: 1, sampleBit: 16};
let extraParam: Record<string, Object> = {"vadBegin": 2000, "vadEnd": 3000, "maxAudioDuration": 40000};
let recognizerParams: speechRecognizer.StartParams = {
  sessionId: requestId,
  audioInfo: audioParam,
  extraParams: extraParam
};
// 调用开始识别方法
asrEngine.startListening(recognizerParams);

主要是设置开始识别的相关参数:

sessionId:会话id,与onResult回调中的sessionId要对应

audioInfo:音频配置信息,可选

audioType:目前只支持PCM,如果要识别MP3文件等需要解码后再传给引擎

sampleRate:音频的采样率,当前仅支持16000采样率

sampleBit:音频返回的采样位数,当前仅支持16位

soundChannel:音频返回的通道数信息,当前仅支持通道1

extraParams:音频的压缩率,pcm格式音频默认为0

extraParams:额外配置信息,主要包含:

recognitionMode:实时语音识别模式(不传时默认为1)

0:实时录音识别(需应用开启录音权限:ohos.permission.MICROPHONE),若需结束录音,则调用finish方法

1:实时音频转文字识别,开启此模式时需要额外调用writeAudio方法,传入待识别音频流; vadBegin:Voice Activity Detection(VAD)前端点设置,参数范围是[500,10000],不传参时默认为10000ms

vadEnd:Voice Activity Detection(VAD)后端点设置。参数范围是[500,10000],不传参时默认为800ms。

maxAudioDuration:最大支持音频时长

短语音模式支持范围[20000-60000]ms,不传参时默认20000ms。

长语音模式支持范围[20000 - 8 * 60 * 60 * 1000]ms。

VAD作用主要是语音活动检测,对静音数据不进行识别

4.传入音频流

let uint8Array: Uint8Array = new Uint8Array();
// 可以通过如下方式获取音频流:1、通过录音获取音频流;2、从音频文件中读取音频流
//2、从音频文件中读取音频流:demo参考
// 写入音频流,音频流长度仅支持640或1280
asrEngine.writeAudio(requestId, uint8Array);

开发实践

点击按钮,将一段音频信息转换为文本。

import {speechRecognizer} from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';

const TAG: string = 'AsrDemo';
let asrEngine: speechRecognizer.SpeechRecognitionEngine;

@Entry
@Component
struct Index {
  @State createCount: number = 0;
  @State result: boolean = false;
  @State voiceInfo: string = "";
  @State sessionId: string = "123456";

  build() {
    Column() {
      Scroll() {
        Column() {
          Button() {
            Text("CreateEngineByCallback")
              .fontColor(Color.White)
              .fontSize(20)
          }
          .type(ButtonType.Capsule)
          .backgroundColor("#0x317AE7")
          .width("80%")
          .height(50)
          .margin(10)
          .onClick(() => {
            this.createCount++;
            console.info(`CreateasrEngine:createCount:${this.createCount}`);
            this.createByCallback();
          })
 
          Button() {
            Text("setListener")
              .fontColor(Color.White)
              .fontSize(20)
          }
          .type(ButtonType.Capsule)
          .backgroundColor("#0x317AE7")
          .width("80%")
          .height(50)
          .margin(10)
          .onClick(() => {
            this.setListener();
          })

          Button() {
            Text("startListening")
              .fontColor(Color.White)
              .fontSize(20)
          }
          .type(ButtonType.Capsule)
          .backgroundColor("#0x317AE7")
          .width("80%")
          .height(50)
          .margin(10)
          .onClick(() => {
            this.startListening();
          })

          Button() {
            Text("writeAudio")
              .fontColor(Color.White)
              .fontSize(20)
          }
          .type(ButtonType.Capsule)
          .backgroundColor("#0x317AE7")
          .width("80%")
          .height(50)
          .margin(10)
          .onClick(() => {
            this.writeAudio();
          })

          Button() {
            Text("queryLanguagesCallback")
              .fontColor(Color.White)
              .fontSize(20)
          }
          .type(ButtonType.Capsule)
          .backgroundColor("#0x317AE7")
          .width("80%")
          .height(50)
          .margin(10)
          .onClick(() => {
            this.queryLanguagesCallback();
          })

          Button() {
            Text("finish")
              .fontColor(Color.White)
              .fontSize(20)
          }
          .type(ButtonType.Capsule)
          .backgroundColor("#0x317AE7")
          .width("80%")
          .height(50)
          .margin(10)
          .onClick(() => {
            // 结束识别
            console.info("finish click:-->");
            asrEngine.finish(this.sessionId);
          })

          Button() {
            Text("cancel")
              .fontColor(Color.White)
              .fontSize(20)
          }
          .type(ButtonType.Capsule)
          .backgroundColor("#0x317AE7")
          .width("80%")
          .height(50)
          .margin(10)
          .onClick(() => {
            // 取消识别
            console.info("cancel click:-->");
            asrEngine.cancel(this.sessionId);
          })

          Button() {
            Text("shutdown")
              .fontColor(Color.White)
              .fontSize(20)
          }
          .type(ButtonType.Capsule)
          .backgroundColor("#0x317AA7")
          .width("80%")
          .height(50)
          .margin(10)
          .onClick(() => {
            //释放引擎
            asrEngine.shutdown();
          })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .height('100%')

    }
  }

  // 创建引擎,通过callback形式返回
  private createByCallback() {
    // 设置创建引擎参数
    let extraParam: Record<string, Object> = {"locate": "CN", "recognizerMode": "short"};
    let initParamsInfo: speechRecognizer.CreateEngineParams = {
      language: 'zh-CN',
      online: 1,
      extraParams: extraParam
    };

  // 调用createEngine方法
  speechRecognizer.createEngine(initParamsInfo, (err: BusinessError, speechRecognitionEngine: 
speechRecognizer.SpeechRecognitionEngine) => {
    if (!err) {
      console.info('createEngine is succeeded');
      // 接收创建引擎的实例
      asrEngine = speechRecognitionEngine;
    } else {
      // 无法创建引擎时返回错误码1002200001,原因:语种不支持、模式不支持、初始化超时、资源不存在等导致创建引擎失败
      // 无法创建引擎时返回错误码1002200006,原因:引擎正在忙碌中,一般多个应用同时调用语音识别引擎时触发
      // 无法创建引擎时返回错误码1002200008,原因:引擎正在销毁中
      console.error("errCode: " + err.code +  " errMessage: " + err.message);
    }
  });
}
    
  // 查询语种信息,以callback形式返回
  private queryLanguagesCallback() {
    // 设置查询相关参数
    let languageQuery: speechRecognizer.LanguageQuery = {
      sessionId: '123456'
    };
    // 调用listLanguages方法
    asrEngine.listLanguages(languageQuery, (err: BusinessError, languages: Array<string>) => {  
      if (!err) {
        //接收目前支持的语种信息
        console.info(TAG, 'listLanguages  succeeded result: ' + languages);
      } else {
        console.error("errCode is " + err.code);
        console.error("errCode is " + err.message);
      }
    });
  };

  // 开始识别
  private startListening() {
    // 设置开始识别的相关参数
    let recognizerParams: speechRecognizer.StartParams = {
      sessionId: this.sessionId,
      audioInfo: { audioType: 'pcm', sampleRate: 16000, soundChannel: 1, sampleBit: 16 }
    }
    // 调用开始识别方法
    asrEngine.startListening(recognizerParams);
  };

  // 写音频流
   private async writeAudio() {
     let ctx = getContext(this);
     let filenames: string[] = fileIo.listFileSync(ctx.filesDir);
     if (filenames.length <= 0) {
        return;
     }
     let filePath: string = ctx.filesDir + '/' + filenames[0];
     let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE);
     try {
       let buf: ArrayBuffer = new ArrayBuffer(1280);
       let offset: number = 0;
       while (1280 == fileIo.readSync(file.fd, buf, {
         offset: offset
       })) { 
         let uint8Array: Uint8Array = new Uint8Array(buf);
         asrEngine.writeAudio("123456", uint8Array);
         await this.countDownLatch(1);
         offset = offset + 1280;
       }
     } catch (e) {
       console.error(TAG, "read file error " + e);
     } finally {
       if (null != file) {
         fileIo.closeSync(file);
       }
     }
   }
  // 计时
  public async countDownLatch(count: number) {
     while (count > 0) {
       await this.sleep(40);
       count--;
     }
   }
   // 睡眠
  private sleep(ms: number):Promise<void> {
     return new Promise(resolve => setTimeout(resolve, ms));
  }

  // 设置回调
  private setListener() {
    // 创建回调对象
    let setListener: speechRecognizer.RecognitionListener = {
      // 开始识别成功回调
      onStart(sessionId: string, eventMessage: string) {
        console.info("onStart sessionId: " + sessionId + "eventMessage: " + eventMessage);
      },
      // 事件回调
      onEvent(sessionId: string, eventCode: number, eventMessage: string) {
        console.info("onEvent sessionId: " + sessionId + "eventCode: " + eventCode + "eventMessage: " + eventMessage);
      },
      // 识别结果回调,包括中间结果和最终结果
      onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult) {
        console.info("onResult sessionId: " + sessionId );
      },
      //识别完成回调
      onComplete(sessionId: string, eventMessage: string) {
        console.info("onComplete sessionId: " + sessionId + "eventMessage: " + eventMessage);
      },
       // 错误回调,错误码通过本方法返回
       // 如:返回错误码1002200006,识别引擎正忙,引擎正在识别中
       // 更多错误码请参考错误码参考
      onError(sessionId: string, errorCode: number, errorMessage: string) {
        console.error("onError sessionId: " + sessionId + "errorCode: " + errorCode + "errorMessage: " + errorMessage);
      },
    }
    // 设置回调
    asrEngine.setListener(setListener);
  };
}