【鸿蒙开发】 一篇带你掌握“语音转文字技术” --内附详细代码

314 阅读7分钟

前言

在一些场景中,为了方便用户。我们会提供语音转文字的服务。比如说在搜索页中,常规的交互是用户在输入框中输入内容之后,点击搜索按钮进行搜索业务,为了提高用户体验,用户可以通过【语音】的方式输入想要搜索的关键词,省去打字的麻烦


一、语音搜索实现逻辑

我们要实现这一功能的思路: 1. 录制音频 2. 把音频buffer传给语音转化引擎 - 输出文字

在这里插入图片描述

二、使用步骤

1.申请权限

应用在申请权限时,需要在项目的配置文件中,逐个声明需要的权限,否则应用将无法获取授权。

  • 如何在配置文件中声明权限:

应用需要在 module.json5 配置文件的 requestPermissions 标签中声明权限。

代码如下(示例):

{
  "module" : {
    // ...
    "requestPermissions":[
      {
        "name" : "ohos.permission.PERMISSION1", //权限名字
        "reason": "$string:reason",             //使用权限的理由
        "usedScene": {                          //权限使用的场景,该字段用于应用上架校验。
          "abilities": [
            "FormAbility"
          ],
          "when":"inuse"
        }
      },
      {
        "name" : "ohos.permission.PERMISSION2",
        "reason": "$string:reason",
        "usedScene": {
          "abilities": [
            "FormAbility"
          ],
          "when":"always"
        }
      }
    ]
  }
}
属性含义
name需要使用的权限名称 (必填)
reason申请权限的原因 (选填)
usedScene权限使用的场景,该字段用于应用上架校验(必填)
  1. 用if条件语句检查是否有权限,如果用户开启权限则正常进行逻辑的处理。 注:理解下面代码可参考@ohos.abilityAccessCtrl (程序访问控制管理)
//接口依赖的元素bundleManager。应用程序信息,三方应用可以通过bundleManager.getBundleInfoForSelf获取自身的应用程序信息

import { abilityAccessCtrl, bundleManager, Permissions } from "@kit.AbilityKit";


//检查是否有这个permissions权限的方法
async checkPermission(permission:Permissions[],callback: () => void){
	// bundleManager模块提供应用信息查询能力。BundleFlag是包信息标志,指示需要获取的包信息的内容。用于获取包含applicationInfo的bundleInfo
	const getbundleFlags=bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
	//以同步方法根据给定的bundleFlags获取当前应用的BundleInfo
	const appInfo = bundleManager.getBundleInfoForSelfSync(getbundleFlags)
	//访问控制管理:获取访问控制模块对象
	const manager = abilityAccessCtrl.createAtManager()
	//判断每个权限是否都开启
	let isHave = permissions.every(item => {
      	//校验应用是否被授予权限,同步返回结果。 appInfo是程序的配置信息里面的accessTokenId是要校验应用的身份标识。
      	let status = manager.checkAccessTokenSync(appInfo.appInfo.accessTokenId, item)
      	//GrantStatus表示授权状态的枚举。PERMISSION_GRANTED表示已授权。
      	return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    })
    if (isHave) {
      // 此时拥有
      callback() // 对方可以正常进行逻辑的处理了
    } else {
      ...
      ...
	}
}
  1. 如果用户并没有授权我们要使用的权限,我们在else里面进行逻辑处理
	  // 第一次弹窗
      //用于UIAbility拉起弹框请求用户授权。使用promise异步回调。
      //如果用户拒绝授权,将无法再次拉起弹框,需要用户在系统应用“设置”的界面中,手动授予权限。
      // 或是调用requestPermissionOnSetting,拉起权限设置弹框,引导用户授权。
      const result = await manager.requestPermissionsFromUser(getContext(), permissions)
  1. 判断用户是否授权,如果用户开启权限则正常进行逻辑的处理
 	  //每个权限
      let againHave = result.authResults.every(status => {
        return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
      })

      if (againHave) {
        callback() // 对方可以正常进行逻辑的处理了
      } else {
        ...
      }
  1. 若用户拒绝,用户想要使用功能该功能。我们要通过requestPermissionOnSetting拉起权限设置弹框,引导用户授权。
	manager.requestPermissionOnSetting(getContext(), permissions)

(因为这个方法可能会多次调用在不同的地方,所以我们可以把他封装成一个工具类)

//@ohos.abilityAccessCtrl (程序访问控制管理)
//接口依赖的元素bundleManager。应用程序信息,三方应用可以通过bundleManager.getBundleInfoForSelf获取自身的应用程序信息

import { abilityAccessCtrl, bundleManager, Permissions } from "@kit.AbilityKit";

class CheckPermissionManager{
	//检查是否有这个permissions权限的方法
	async checkPermission(permission:Permissions[],callback: () => void){
		...
    	})
	}
}

export const checkPermissionManager = new checkPermissionManager()

2.录制语言

和录音机不同的是,我们在这里使用Audio Kit(音频服务)下的 ArkTS API @ohos.multimedia.audio(音频管理)模块。因为我们录的音频数据要实时转化文字,不需要写入沙箱文件,音频数据以内存形式存在。

1.获取音频渲染器。使用Promise方式异步返回结果。

import { audio } from '@kit.AudioKit';

capturer: audio.AudioCapturer | null = null // 录音对象
async createCapturer() {
	let audioStreamInfo: audio.AudioStreamInfo = {
  		samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
  		channels: audio.AudioChannel.CHANNEL_1,
  		sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
  		encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
	};

	let audioRendererInfo: audio.AudioRendererInfo = {
  		usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
  		rendererFlags: 0
	};

	let audioRendererOptions: audio.AudioRendererOptions = {
  		streamInfo: audioStreamInfo,
  		rendererInfo: audioRendererInfo
	};

 	this.capturer = await audio.createAudioCapturer(audioCapturerOptions)
}
  1. 开始录音方法
async start(callback: (bf: ArrayBuffer) => void) {
	//是否存在录音对象,存在才可以录音
    if (!this.capturer) {
      await this.createCapturer() // 创建录音对象
    }
    // 先监听 再开始录音
    this.capturer?.on("readData", (bf) => {
      // bf 一段段的语音转化二进制对象的具体参数
      //console.log('',bf.byteLength.toString())
      callback(bf)
    })
    this.capturer?.start() // 开启录音

  }
  1. 停止录音方法
 // 停止录音的方法
  async stop() {
    if (this.capturer) {
      await this.capturer.stop() // 停止录音
      await this.capturer.release() // 释放资源
      this.capturer = null // 释放内存对象 垃圾回收 gc
      // 正常的局部变量
    }

  }

因为这个方法可能会多次调用在不同的地方,所以我们可以把他封装成一个工具类

import { audio } from "@kit.AudioKit"

export class AudioRecordManager {
	...
	步骤1
	步骤2
	步骤3
}

export const audioRecordManager = new AudioRecordManager()

3.语音识别引擎

对引擎进行初始化,并创建SpeechRecognitionEngine实例。

engine: speechRecognizer.SpeechRecognitionEngine | null = null
sessionId: string = "123456"
textCallBack?: (text: string) => void
  1. 创建语音识别引擎

语音识别类,用于执行语音识别过程中的相关操作。在调用SpeechRecognitionEngine的方法前,需要先通过createEngine方法创建一个SpeechRecognitionEngine实例。

async createEngine() {
    // 创建引擎,通过callback形式返回
    // 设置创建引擎参数
    let extraParam: Record<string, Object> = { "locate": "CN", "recognizerMode": "short" };
    let initParamsInfo: speechRecognizer.CreateEngineParams = {
      language: 'zh-CN',
      online: 1,
      extraParams: extraParam
    };
    // 调用createEngine方法
    this.engine = await speechRecognizer.createEngine(initParamsInfo)
  }
  1. 只初始化一次
async init(callBack?: (text: string) => void) {
    this.textCallBack = callBack
    if (!this.engine) {
      await this.createEngine() // 创建引擎
    }
    // 此时引擎已经创建了
    // this.engine?.setListener({})
    this.setListener() // 设置监听项

    // 开启监听
    // 设置开始识别的相关参数
    let recognizerParams: speechRecognizer.StartParams = {
      sessionId: this.sessionId,
      audioInfo: {
        audioType: 'pcm',
        sampleRate: 16000,
        soundChannel: 1,
        sampleBit: 16
      } //audioInfo参数配置请参考AudioInfo
    }
    // 调用开始识别方法
    this.engine?.startListening(recognizerParams);

  }
  1. 传入音频流,调用writeAudio方法,开始写入音频流。读取音频文件时,开发者需预先准备一个pcm格式音频文件。

可以通过如下方式获取音频流: 1、通过录音获取音频流; 2、从音频文件中读取音频流(本文所选用的方法) // 写入音频流,音频流长度仅支持640或1280

// 连续不断的输入
  start(bf: ArrayBuffer) {
    this.engine?.writeAudio(this.sessionId, new Uint8Array(bf))
  }
  1. 停止输入
stop() {
    this.engine?.finish(this.sessionId) // 先结束
    this.engine?.shutdown() // 释放资源
    this.engine = null // 真正的释放资源
  }
  1. 用来接收语音识别相关的回调信息。
setListener() {
    // let that = this // this给到一个变量
    // 创建回调对象
    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) => {
        // Logger.info(result)
        if (this.textCallBack) {
          this.textCallBack(result.result)
        }
        // console.info(`onResult, sessionId: ${sessionId} sessionId: ${JSON.stringify(result)}`);
      },
      // 识别完成回调
      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}`);
      }
    }
    // 设置回调
    this.engine?.setListener(setListener);
  }

因为这个方法可能会多次调用在不同的地方,所以我们可以把他封装成一个工具类

代码如下(示例):

import { speechRecognizer } from "@kit.CoreSpeechKit"

export class SpeechToTextMananger {
	...把上面的代码块按顺序放入
}

export const speechToTextMananger = new SpeechToTextMananger()

三、实现功能

通过上面的代码编写我们已经有三个可供使用的工具类。

  1. 长按语音时
async startRecord() {
    //进行鉴权-只在长按语音时触发
    checkPermissionManager.checkPermission(["ohos.permission.MICROPHONE"], () => {
       //已经授权则
       // 开始录音
      	speechToTextMananger.init((text) => {
        	 this.keyword = text // 拿到识别文本 显示到页面上
      	}) // 先初始化
      	audioCapturerManager.start((bf) => {
        	// 拿到语音转化 => 语音识别
        	speechToTextMananger.start(bf) // 传入音频buffer得到转化文字
      	})
    })

  1. 停止长按
 async closeRecord() {
    audioRecordManager.stop()
    speechToTextMananger.stop()
  }
  1. 使用该功能需要用到手势处理的API,为组件绑定不同类型的手势事件,并设置事件的响应方法。 我们使用用于触发长按手势事件LongPressGesture接口。
export enum VoiceState {
  DEFAULT,
  VOICING,
  VOICEOVER
}
@State voiceState: VoiceState = VoiceState.DEFAULT

Button() {
        Row({ space: 4 }) {
        //可以在这个位置放置一个语音搜索图标
          if (this.voiceState === VoiceState.VOICING) {
            Text('松开立即搜索')
              .fontSize(14)
              .fontColor($r('[basic].color.white'))
          } else {
            Text('长按语音搜索')
              .fontSize(14)
              .fontColor($r('[basic].color.white'))
          }
        }
      }
      .gesture(
        LongPressGesture()
          //LongPress手势识别成功回调。
          .onAction(() => {
            this.startRecord()
          })
          //LongPress手势识别成功,最后一根手指抬起后触发回调。
          .onActionEnd(() => {
            this.closeRecord()
          })
          //LongPress手势识别成功,接收到触摸取消事件触发回调。
          .onActionCancel(() => {
            this.closeRecord()
          })
      )

总结

以上就是我们实现语音转文字这一技术的全面讲解,希望大家能有收获噢~