本文介绍如何使用移动端Harmony SDK来支持实时记录场景下的音频识别流程。
前提条件
SDK关键接口
initialize:初始化SDK。
/** * 初始化SDK,SDK可多实例,请先释放后再次进行初始化。请勿在UI线程调用,可能会引起阻塞。 * @param callback:事件监听回调,参见下文具体回调。 * @param parameters:json string形式的初始化参数,参见下方说明或接口说明:https://help.aliyun.com/document_detail/173298.html。 * @param level:log打印级别,值越小打印越多。 * @param save_log:是否保存log为文件,存储目录为ticket中的debug_path字段值。注意,log文件无上限,请注意持续存储导致磁盘存满。 * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。 */ public initialize(callback:INativeNuiCallback , parameters:string , level:number , save_log:boolean=false ):number
其中,parameters详细说明:
参数
类型
是否必选
说明
workspace
String
是
工作目录路径,SDK从该路径读取配置文件。
app_key
String
是
必须填“default”。
token
String
是
必须填“default”。
url
String
是
创建听悟实时记录任务时返回的会议MeetingJoinUrl作为音频流推送地址,在后续实时音频流识别时通过该地址进行推流。
service_mode
String
是
必须填“1”,表示启用在线功能。
device_id
String
是
设备标识,唯一表示一台设备(如Mac地址/SN/UniquePsuedoID等)。
debug_path
String
否
debug目录。当初始化SDK时的save_log参数取值为true时,该目录用于保存日志文件。
save_wav
String
否
当初始化SDK时的save_log参数取值为true时,该参数生效。表示是否保存音频debug,该数据保存在debug目录中,需要确保debug_path有效可写。
注意,音频文件无上限,请注意持续存储导致磁盘存满。
其中,INativeNuiCallback类型包含如下回调。
onNuiAudioStateChanged:根据音频状态进行录音功能的开关。
/** * 当start/stop/cancel等接口调用时,SDK通过此回调通知App进行录音的开关操作。 * @param state:录音需要的状态(打开/停止/关闭) */ onNuiAudioStateChanged:(state:Constants.AudioState)=>void
onNuiNeedAudioData:在回调中提供音频数据。
/** * 开始识别时,此回调被连续调用,App需要在回调中进行语音数据填充。 * @param buffer:填充语音的存储区。 * @return:实际填充的字节数。 */ onNuiNeedAudioData:(buffer:ArrayBuffer)=>number;
onNuiEventCallback:SDK事件回调。
/** * SDK主要事件回调 * @param event:回调事件,参见如下事件列表。 * @param resultCode:参见错误码,在出现EVENT_ASR_ERROR事件时有效。 * @param arg2:保留参数。 * @param kwsResult:语音唤醒功能(暂不支持)。 * @param asrResult:语音识别结果。 */ onNuiEventCallback:(event:Constants.NuiEvent, resultCode:number, arg2:number, kwsResult:KwsResult, asrResult:AsrResult)=>void;
事件列表:
名称
说明
EVENT_VAD_START
检测到人声起点。
EVENT_VAD_END
检测到人声尾点。
EVENT_ASR_PARTIAL_RESULT
语音识别中间结果。
EVENT_ASR_ERROR
根据错误码信息判断出错原因。
EVENT_MIC_ERROR
录音错误,表示SDK连续2秒未收到任何音频,可检查录音系统是否正常。
EVENT_SENTENCE_START
实时语音识别事件,表示检测到一句话开始。
EVENT_SENTENCE_END
实时语音识别事件,表示检测到一句话结束,返回一句完整的结果。
EVENT_SENTENCE_SEMANTICS
暂不使用。
EVENT_RESULT_TRANSLATED
翻译结果。
EVENT_TRANSCRIBER_COMPLETE
停止语音识别后最终事件
onNuiAudioRMSChanged:音频能量值回调。
/** * 音频能量值回调 * @param val: 音频数据能量值回调,范围-160至0,一般用于UI展示语音动效 */ onNuiAudioRMSChanged:(val:number)=>number;
setParams:以JSON格式设置SDK参数。
/** * 以JSON格式设置参数 * @param params:参见接口说明:https://help.aliyun.com/document_detail/173298.html。 * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。 */ public setParams(params:string):number
params详细说明:
参数
类型
是否必选
说明
service_type
Integer
是
必须填“4”。此为需要请求的语音服务类型,听悟实时推流为“4”。
nls_config
JsonObject
是
访问语音服务相关的参数配置,详见如下。
nls_config.sr_format
String
是
必须填“pcm”。对应的《CreateTask - 创建听悟任务》中,创建听悟任务时也请指定音频流数据的编码格式为pcm。
nls_config.sample_rate
Integer
是
音频采样率,默认值:16000Hz。对应的《CreateTask - 创建听悟任务》中,创建听悟任务时也请指定音频流数据的采样率,当前支持 8000 和 16000。
startDialog:开始识别。
/** * 开始识别 * @param vad_mode:多种模式,对于识别场景,请使用P2T。 * @param dialog_params:json string形式的对话参数,参见接口说明:https://help.aliyun.com/document_detail/173298.html。 * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。 */ public startDialog(vad_mode:Constants.VadMode, dialog_params:string):number
stopDialog:结束识别。
/** * 结束识别,调用该接口后,服务端将返回最终识别结果并结束任务。 * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。 */ public stopDialog():number
cancelDialog:立即结束识别。
/** * 立即结束识别,调用该接口后,不等待服务端返回最终识别结果就立即结束任务。 * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。 */ public cancelDialog():number
release:释放SDK。
/** * 释放SDK资源 * @return:参见错误码:https://help.aliyun.com/document_detail/459864.html。 */ public release():number
GetVersion:获得当前SDK版本信息。
/** * 获得当前SDK版本信息 * @return: 字符串形式的SDK版本信息 */ public GetVersion():string
调用步骤
请下载后在听悟的样例初始化代码中将Appkey和Token置为default,url置为您创建听悟实时记录返回的会议MeetingJoinUrl。
初始化SDK、录音实例。
根据业务需求设置参数。
调用startDialog开始识别。
根据音频状态回调onNuiAudioStateChanged,打开录音机。
在onNuiNeedAudioData回调中提供录音数据。
在EVENT_SENTENCE_START事件回调中表示当前开始识别一个句子,在EVENT_ASR_PARTIAL_RESULT事件回调中获取识别中间结果,在EVENT_SENTENCE_END事件回调中获得这句话完整的识别结果和各相关信息,在EVENT_RESULT_TRANSLATED事件回调中获得翻译结果。
调用stopDialog结束识别。并从EVENT_TRANSCRIBER_COMPLETE事件回调确认已停止识别。
结束调用,使用release接口释放SDK资源。
代码示例
您如果有多例需求,也可以直接new对象进行使用。也可采用GetInstance获得单例。
NUI SDK初始化
//定义类NativeNuiCallbackHandle 实现回调接口INativeNuiCallback
class NativeNuiCallbackHandle implements INativeNuiCallback{
//内部实现INativeNuiCallback中的5个接口函数
//此处省略
}
let context = getContext(this) as common.UIAbilityContext;
this.filesDir = context.filesDir;
this.resourceDir = context.resourceDir;
//这里获得资源路径, 由于资源文件存放在工程的resfiles目录下,所以使用沙箱路径下的resfiles目录
let asset_path:string = this.resourceDir+"/resources_cloud"
//由于用户无法直接操作设备目录,因此调试路径设置为APP所在的沙箱路径下的公共目录filesDir
let debug_path:string = this.filesDir
//初始化SDK,注意用户需要在genInitParams中填入相关ID信息才可以使用。
cbhandle:NativeNuiCallbackHandle = new NativeNuiCallbackHandle()
g_asrinstance:NativeNui = new NativeNui(Constants.ModeType.MODE_DIALOG, "asr")
let ret:number = this.g_asrinstance.initialize(this.cbhandle, this.genInitParams(asset_path,debug_path), Constants.LogLevel.LOG_LEVEL_VERBOSE, false);
console.info("result = " + ret);
if (ret == Constants.NuiResultCode.SUCCESS) {
console.error(`call g_asrinstance.initialize() return success`);
} else {
//抛出错误异常信息。
console.error(`call g_asrinstance.initialize() return error:${ret}`);
}
其中,genInitParams生成为String JSON字符串,包含资源目录和用户信息。其中用户信息包含如下字段。
genInitParams(workpath:string, debugpath:string):string {
let str:string = "";
let object:Map<string, string|number|boolean|object> = new Map();
//账号和项目创建
// ak_id ak_secret app_key如何获得,请查看https://help.aliyun.com/document_detail/72138.html
object.set("app_key","default"); // 必填
object.set("token", "default"); // 必填
object.set("device_id", "meta60protestdevice"/*Utils.getDeviceId()*/); // 必填, 推荐填入具有唯一性的id, 方便定位问题
// url中填入生成的MeetingJoinUrl。
// 由于MeetingJoinUrl生成过程涉及ak/sk,移动端不可存储账号信息,故需要在服务端生成,并下发给移动端。
// 详细请看: https://help.aliyun.com/zh/tingwu/api-tingwu-2023-09-30-createtask?spm=a2c4g.11186623.0.i32
object.set("url", "wss://tingwu-realtime-cn-hangzhou-pre.aliyuncs.com/api/ws/v1?XXXX"); // 必填
object.set("workspace", workpath); // 必填, 且需要有读写权限
//当初始化SDK时的save_log参数取值为true时,该参数生效。表示是否保存音频debug,该数据保存在debug目录中,需要确保debug_path有效可写。
// object.put("save_wav", "true");
//debug目录,当初始化SDK时的save_log参数取值为true时,该目录用于保存中间音频文件。
object.set("debug_path", debugpath);
object.set("service_mode", Constants.ModeFullCloud); // 必填
str = MapToJson(object)
console.info("configinfo genInitParams:" + str);
return str;
}
参数设置
以JSON字符串形式进行设置。
import {MapToJson} from 'neonui'
//设置相关识别参数,具体参考API文档
// initialize()之后startDialog之前调用
this.g_asrinstance.setParams(this.genParams());
genParams():string {
let params:string = "";
let nls_config:Map<string, string|number|boolean|object> = new Map();
nls_config.set("sample_rate", 16000);
nls_config.set("sr_format", "pcm");
let parameters:Map<string, string|number|boolean|object> = new Map();
parameters.set("nls_config", Object( JSON.parse(MapToJson(nls_config)) ) );
parameters.set("service_type", Constants.kServiceTypeSpeechTranscriber); // 必填
params = MapToJson(parameters);
console.log("configinfo genParams" + params)
return params;
}
开始识别
通过startDialog接口开启监听。
import {MapToJson} from 'neonui'
let ret:number = this.g_asrinstance.startDialog(Constants.VadMode.TYPE_P2T,
this.genDialogParams());
console.info("start done . ret = ", ret);
if (ret != 0) {
console.info("call startDialog return error. ", ret);
}
nui_instance.startDialog(Constants.VadMode.TYPE_P2T, genDialogParams());
genDialogParams():string {
let params:string = "";
let dialog_param:Map<string, string|number|boolean|object> = new Map();
params = MapToJson(dialog_param);
console.info("configinfo dialog params: " + params);
return params;
}
推送录音数据
updateAudio:在AudioCapturer的on('readData',)注册的回调函数中,直接调用updateAudio接口把录音数据送入SDK内部。
//g_asrinstance.updateAudio(buffer,false) /*AudioCapturer中注册的'readData'接口是AudioCapturer.readDataCallback *AudioCapturer.audioCapturer.on('readData', AudioCapturer.readDataCallback); */ class AudioCapturer{ static readDataCallback = (buffer: ArrayBuffer) => { console.log(`${TAG} read data bytelength is ${buffer.byteLength}. uid[${process.uid}] pid[${process.pid}] tid[${process.tid}]`); AudioCapturer.g_asrinstance.updateAudio(buffer,false) } }
回调处理
onNuiAudioStateChanged:录音状态回调,SDK内部维护录音状态,根据该状态的回调进行录音机的开关操作。
/* 对于鸿蒙开发环境IDE版本5.0.3.403 以前的版本,AudioCapturer模块如果使用注册回调[on("readData",)]的方式读取录音数据, 存在AudioCapturer.stop后直接start不会触发回调的情况。此时必须按照 (stop,realease)再(createAudioCapturer,start)的流程才能正常工作。 升级为 IDE版本5.0.3.403版本后,以上问题已经解决。所以以下示例代码中注释掉了create/release相关接口的调用。 */ onNuiAudioStateChanged(state:Constants.AudioState):void { console.info(`womx onUsrNuiAudioStateChanged(${state})`) if (state === Constants.AudioState.STATE_OPEN){ console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder start`) //AudioCapturer.init(g_asrinstance) AudioCapturer.start() console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder start done`) } else if (state === Constants.AudioState.STATE_CLOSE){ console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder close`) AudioCapturer.stop() //AudioCapturer.release() console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder close done`) } else if (state === Constants.AudioState.STATE_PAUSE){ console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder pause`) AudioCapturer.stop() //AudioCapturer.release() console.info(`womx onUsrNuiAudioStateChanged(${state}) audio recorder pause done`) } }
onNuiNeedAudioData:录音数据回调,在该回调中填充录音数据。
public int onNuiNeedAudioData(byte[] buffer, int len) { console.info(`warning,this callback should not be called in HarmonyOS Next`) return 0; }
onNuiEventCallback:NUI SDK事件回调,请勿在事件回调中调用SDK的接口,可能引起死锁。
//以下几个变量,用于在event回调过程中,进行实时识别结果、历史识别结果、实时翻译结果、历史翻译结果的汇总相关工作。 asrspeechrealtimeResultOld:string="" asrmessage:string=""; message:string=""; //保存当前回调的事件信息 asr_message_id:string=""; asrspeechrealtimeResultTranslatedOld:string="" asrmessageTranslated:string=""; onNuiEventCallback(event:Constants.NuiEvent, resultCode:number, arg2:number, kwsResult:KwsResult, asrResult:AsrResult):void { let asrinfo:string = "" console.log("onUsrNuiEventCallback last this callback handle.asrmessage is " + this.asrmessage) console.log("onUsrNuiEventCallback new this callback handle is " + JSON.stringify(this)) console.log("onUsrNuiEventCallback event is " + event) if (event === Constants.NuiEvent.EVENT_TRANSCRIBER_COMPLETE){ // 实时识别结束 this.message = "EVENT_TRANSCRIBER_COMPLETE" this.asrspeechrealtimeResultOld="" this.asrspeechrealtimeResultTranslatedOld="" } else if (event === Constants.NuiEvent.EVENT_SENTENCE_START){ this.message = "EVENT_SENTENCE_START" } else if (event === Constants.NuiEvent.EVENT_ASR_PARTIAL_RESULT || event === Constants.NuiEvent.EVENT_SENTENCE_END){ if (event === Constants.NuiEvent.EVENT_ASR_PARTIAL_RESULT ) { // 例如展示当前句子的识别中间结果 this.message = "EVENT_ASR_PARTIAL_RESULT" } else if(event === Constants.NuiEvent.EVENT_SENTENCE_END){ // 例如展示当前句子的完整识别结果 this.message = "EVENT_SENTENCE_END" } } else if (event === Constants.NuiEvent.EVENT_RESULT_TRANSLATED){ // 例如展示当前句子的翻译结果 this.message = "EVENT_RESULT_TRANSLATED" } else if (event === Constants.NuiEvent.EVENT_ASR_ERROR){ // asrResult在EVENT_ASR_ERROR中为错误信息,搭配错误码resultCode和其中的task_id更易排查问题,请用户进行记录保存。 this.message = "EVENT_ASR_ERROR" } else if (event === Constants.NuiEvent.EVENT_MIC_ERROR){ // EVENT_MIC_ERROR表示2s未传入音频数据,请检查录音相关代码、权限或录音模块是否被其他应用占用。 this.message = "EVENT_MIC_ERROR" } else if (event === Constants.NuiEvent.EVENT_DIALOG_EX){ /* unused */ // 此事件可不用关注 this.message = "EVENT_DIALOG_EX" } if (asrResult) { asrinfo = asrResult.asrResult console.log(`asrinfo asrResult string is ${asrinfo}. all response is [${asrResult.allResponse}]`) if (asrinfo) { try { let asrresult_json:object|null = JSON.parse(asrinfo) //console.log(JSON.stringify(asrresult_json)) if (asrresult_json) { let payload:object|null = asrresult_json["payload"]; if (payload) { //console.log(JSON.stringify(payload)) if (event === Constants.NuiEvent.EVENT_RESULT_TRANSLATED) { //出现翻译结果事件时,进行翻译结果的实时更新。 if (payload["translate_result"]) { //实时更新当前的翻译结果。 console.log(`translate_result is [${payload["translate_result"][0]["text"]}], with oldinfo [${this.asrspeechrealtimeResultTranslatedOld}]`) //this.asrmessageTranslated中保存的是start以后所有句子的翻译结果。包括历史句子的翻译汇总结果 + 当前句子的实时翻译结果。 this.asrmessageTranslated = this.asrspeechrealtimeResultTranslatedOld + payload["translate_result"][0]["text"]; } if (this.asr_message_id == asrresult_json["header"]["source_message_id"]){ //当翻译结果对应的source_message_id 是 EVENT_SENTENCE_END对应的message_id时,说明EVENT_SENTENCE_END对应语句的翻译结束。此时可以更新历史句子的总翻译结果。 this.asrspeechrealtimeResultTranslatedOld = this.asrmessageTranslated } } else { //出现识别结果事件时,进行识别结果的实时更新。 if (payload["result"]) { //this.asrmessage中保存的是start以后所有句子的识别结果。包括历史句子的识别汇总结果 + 当前句子的实时识别结果。 this.asrmessage = this.asrspeechrealtimeResultOld + payload["result"]; } } if(event === Constants.NuiEvent.EVENT_SENTENCE_END){ //出现EVENT_SENTENCE_END事件事,进行历史识别结果的汇总、暂存最后一句识别结果的message_id作为该句识别结果的翻译结果过滤依据 //进行历史识别结果的汇总 this.asrspeechrealtimeResultOld = this.asrmessage //暂存最后一句识别结果的message_id作为该句识别结果的翻译结果过滤依据 this.asr_message_id = asrresult_json["header"]["message_id"] } } } } catch (e){ console.error("got asrinfo asrResult not json, so donot fresh asrinfo." + JSON.stringify(e)) } } } console.info(`womx onUsrNuiEventCallback(${event}, ${resultCode},${arg2}, kwsResult:${kwsResult},asrResult:"${this.asrmessage}") done`) }
结束识别
nui_instance.stopDialog();
释放SDK
nui_instance.release();