随着科技的发展,人工智能已悄然融入我们的日常生活。作为新兴的自主研发系统,HarmonyOS在AI领域同样表现出了强大的实力,为开发者提供了一系列的 AI 开放能力,帮助开发者轻松将多种AI能力与应用集成,创造更加智能强大的应用。
引言
放眼四周,语音智能助手的身影无处不在,无论是Siri、天猫精灵还是小爱同学,它们的身影遍布车载系统、智能电视以及智能家居设备,成为我们生活中不可或缺的一部分。HarmonyOS凭借其强大的语音识别API,为开发者开启了创造力的新大门。这些API不仅支持语音转文字,还能将文字转换回语音,两者的结合为开发者实现多样化的语音交互场景提供了便利。对于那些希望轻松进入智能家居、车载系统等热门领域的开发者来说,HarmonyOS无疑是一条通往成功的捷径。
语音转文字实践
一种将语音信号巧妙转换为文本信息的前沿技术。比较常见的场景就是类似微信中的语音输入功能和语音转文字等功能。
HarmonyOS 以其卓越的语音转文字功能,不仅能够将 PCM 音频源文件转换为文本信息,还能实时捕捉麦克风输入并转换为文本,极大地丰富了语音交互的可能性。
限制
- 若使用的是录音数据转文本信息,则需要开放麦克风权限,这属于录音功能的约束。目前提供的语音转文字引擎只支持转中文。语音识别支持长语音(最长8h)和短语音识别(最长60s),超过对应时长时,语音识别服务会异常退出。语音识别暂时只支持pcm, 16000采样率, 通道1, 16000采样率,比特率范围: [24000, 96000]
实践
对于类似于语音输入转文字功能的实现,需要依靠录音功能,实时录音转文本有两种实现方式。
- 使用录音接口(需要麦克风授权),监听录音的readData回调,通过writeAudio将录音数据传递给语音识别服务recognitionMode 配置为实时录音模式 --- 需要在 startListening 前开启 ohos.permission.MICROPHONE 权限
而对于类似与微信的语音转文字功能,只需要读取音频文件的ArrayBuffer数据,通过writeAudio将文件数据传递给语音识别服务,离线进行识别输出即可。当然由于目前语音识别的限制,暂时只支持 PCM 音频文件的识别,若是其他格式的音频文件,则需要编码成PCM文件之后才能进行识别。
语音识别服务一个应用只能同时开启一个,因此,需要在应用中创建单例来管理。
export class SpeechRecognizerManager { private static instance: SpeechRecognizerManager; asrEngine: speechRecognizer.SpeechRecognitionEngine | undefined; // 默认回调监听事件 private setListener: speechRecognizer.RecognitionListener = { // 开始识别成功回调 onStart(sessionId: string, eventMessage: string) { console.info(`SpeechRecognizerManager onStart, sessionId: ${sessionId} eventMessage: ${eventMessage}`); }, // 事件回调 onEvent(sessionId: string, eventCode: number, eventMessage: string) { console.info(`SpeechRecognizerManager onEvent, sessionId: ${sessionId} eventCode: ${eventCode} eventMessage: ${eventMessage}`); }, // 识别结果回调,包括中间结果和最终结果 onResult(sessionId: string, result: speechRecognizer.SpeechRecognitionResult) { console.info(`SpeechRecognizerManager onResult, sessionId: ${sessionId} sessionId: ${JSON.stringify(result)}`); }, // 识别完成回调 onComplete(sessionId: string, eventMessage: string) { console.info(`SpeechRecognizerManager onComplete, sessionId: ${sessionId} eventMessage: ${eventMessage}`); }, // 错误回调,错误码通过本方法返回 onError(sessionId: string, errorCode: number, errorMessage: string) { console.error(`SpeechRecognizerManager onError, sessionId: ${sessionId} errorCode: ${errorCode} errorMessage: ${errorMessage}`); } } static getInstance(): SpeechRecognizerManager { if (!SpeechRecognizerManager.instance) { SpeechRecognizerManager.instance = new SpeechRecognizerManager() } return SpeechRecognizerManager.instance }}
- 提供外部可创建实例的接口
// 创建实例 createEngine(options?: Record<string, string>) { // 设置创建引擎参数 let extraParam: Record<string, Object> = { "locate": options?.locate || "CN", // 当前仅支持“CN” "recognizerMode": options?.recognizerMode || "long" // 当前支持“short“和“long” }; let initParamsInfo: speechRecognizer.CreateEngineParams = { language: options?.language || 'zh-CN', // 当前仅支持“zh-CN”中文 online: 1, // 仅支持离线模式 extraParams: extraParam }; // 调用createEngine方法 speechRecognizer.createEngine(initParamsInfo, (err: BusinessError, speechRecognitionEngine: speechRecognizer.SpeechRecognitionEngine) => { if (!err) { console.info('SpeechRecognizerManager Succeeded in creating engine.'); // 接收创建引擎的实例 this.asrEngine = speechRecognitionEngine; } else { console.error(`SpeechRecognizerManager Failed to create engine. Code: ${err.code}, message: ${err.message}.`); } }); }
2. 提供外部可注册回调的接口
// 注册回调 registryCallBack(key: string, callback:( (sessionId: string, eventMessage: string) => void) |((sessionId: string, eventCode: number, eventMessage: string)=> void) |((sessionId: string, result: speechRecognizer.SpeechRecognitionResult)=> void) |((sessionId: string, errorCode: number, errorMessage: string)=> void) ) { if (this.setListener[key]) { this.setListener[key] = callback } }
3. 提供外部可设置回调的接口
// 设置回调 createCallback() { this.asrEngine?.setListener(this.setListener); }
4. 提供外部可开启识别的接口
// 开始识别 startListening(context:Context, sessionId: string, options?: Record<string, string|number>) { let audioParam: speechRecognizer.AudioInfo = { audioType: options?.audioType as string ||'pcm', sampleRate: options?.sampleRate as number || 16000, soundChannel: options?.soundChannel as number || 1, sampleBit: options?.sampleBit as number || 16 } let recognizerParams: speechRecognizer.StartParams = { sessionId: sessionId, audioInfo: audioParam } let extraParam: Record<string, Object> = { "recognitionMode": (options?.recognitionMode as number) === 0 ? 0 : 1, "vadBegin": options?.vadBegin as number || 2000, "vadEnd": options?.vadEnd as number || 3000, "maxAudioDuration": options?.maxAudioDuration as number || 20000 } recognizerParams.extraParams = extraParam if (options?.recognitionMode === 0) { // 实时语音识别需要麦克风授权 requestPermission([PERMISSION_MICROPHONE], context as Context).then(permissionResult => { if (permissionResult[PERMISSION_MICROPHONE]) { this.asrEngine?.startListening(recognizerParams); } else { promptAction.showToast({ message: '请先申请麦克风权限' }) } }); } else { this.asrEngine?.startListening(recognizerParams); } };
5. 提供外部写入音频数据的接口
writeAudio(sessionId: string, data: Uint8Array) { this.asrEngine?.writeAudio(sessionId, data) }
6. 提供外部可完成识别的接口
finish(sessionId: string) { this.asrEngine?.finish(sessionId) this.asrEngine?.cancel(sessionId) }
7. 提供外部可注销实例的接口
release () { this.asrEngine?.shutdown() this.asrEngine = undefined }
文本播报实践
一种将文本信息转化为生动语音播报的技术,比较常见的场景就是手机系统应用无障碍(屏幕朗读)、听书等功能。HarmonyOS 提供的文本播报能力,支持将文本信息通过系统扬声器生动输出,为用户带来更加丰富的听觉体验。
限制
- 目前鸿蒙提供的语音播报音色只有一个,只能使用默认,也就无法定制自己喜欢的音色进行播报了。一次只能将不超过10000字符的中文文本(简体中文、繁体中文、数字、中文语境下的英文)合成为语音并通过扬声器进行输出
实践
文字播报服务一个应用只能同时开启一个,因此,也需要在应用中创建单例来管理,同时需要在实例中判断文字播报服务的状态,避免同一个实例频繁开启播报导致异常。
创建一个单例,存放全应用唯一的 ttsEngine 实例,并提供以下相关接口
export class TextToSpeechManager{ private static instance: TextToSpeechManager; ttsEngine: textToSpeech.TextToSpeechEngine | undefined; // speak的默认回调监听 private speakListener: textToSpeech.SpeakListener = { // 开始播报回调 onStart(requestId: string, response: textToSpeech.StartResponse) { console.info(`onStart, requestId: ${requestId} response: ${JSON.stringify(response)}`); }, // 合成完成及播报完成回调 onComplete(requestId: string, response: textToSpeech.CompleteResponse) { console.info(`onComplete, requestId: ${requestId} response: ${JSON.stringify(response)}`); }, // 停止播报回调 onStop(requestId: string, response: textToSpeech.StopResponse) { console.info(`onStop, requestId: ${requestId} response: ${JSON.stringify(response)}`); }, // 返回音频流 onData(requestId: string, audio: ArrayBuffer, response: textToSpeech.SynthesisResponse) { console.info(`onData, requestId: ${requestId} sequence: ${JSON.stringify(response)} audio: ${JSON.stringify(audio)}`); }, // 错误回调 onError(requestId: string, errorCode: number, errorMessage: string) { console.error(`onError, requestId: ${requestId} errorCode: ${errorCode} errorMessage: ${errorMessage}`); } } static getInstance(): TextToSpeechManager { if (!TextToSpeechManager.instance) { TextToSpeechManager.instance = new TextToSpeechManager() } return TextToSpeechManager.instance }}
- 提供外部可创建 ttsEngine 实例的接口
// 创建实例 createEngine(options?: textToSpeech.CreateEngineParams) { // 设置创建引擎参数 let extraParam: Record<string, Object> = { "style": options?.extraParams?.style??'interaction-broadcast', "locate": options?.extraParams?.locate??'CN', "name": options?.extraParams?.name??'EngineName' }; let initParamsInfo: textToSpeech.CreateEngineParams = { language: options?.language??'zh-CN', person: options?.person??0, online: options?.online??1, extraParams: extraParam }; // 调用createEngine方法 textToSpeech.createEngine(initParamsInfo, (err: BusinessError, textToSpeechEngine: textToSpeech.TextToSpeechEngine) => { if (!err) { console.info('Succeeded in creating engine'); // 接收创建引擎的实例 this.ttsEngine = textToSpeechEngine; } else { console.error(`Failed to create engine. Code: ${err.code}, message: ${err.message}.`); } }); }
2. 提供外部可注册播报服务的回调的接口
// 注册回调 registryCallBack(key: string, callback: ((requestId: string, audio: ArrayBuffer, response: textToSpeech.SynthesisResponse)=> void) |((requestId: string, response: textToSpeech.StartResponse )=> void) |((requestId: string, response: textToSpeech.CompleteResponse)=> void) |((requestId: string, response: textToSpeech.StopResponse)=> void) |((requestId: string, errorCode: number, errorMessage: string)=> void) ) { if (this.speakListener[key]) { this.speakListener[key] = callback } } // 设置回调 createCallback() { this.ttsEngine?.setListener(this.speakListener); }
3. 提供外部可配置播报策略的接口
// 设置播报策略 startSpeech(originalText: string, SpeakParams: textToSpeech.SpeakParams) { if (!SpeakParams.requestId || typeof SpeakParams.requestId !== 'string' || !this.ttsEngine) { console.log('textToSpeech','未提供有效的数据或未初始化') return } // 设置播报相关参数 let extraParams: Record<string, Object> = { "queueMode": SpeakParams?.extraParams?.queueMode??0, "speed": SpeakParams?.extraParams?.speed??1, "volume": SpeakParams?.extraParams?.volume??2, "pitch": SpeakParams?.extraParams?.pitch??1, "languageContext": SpeakParams?.extraParams?.languageContext??'zh-CN', "audioType": SpeakParams?.extraParams?.audioType??"pcm", "soundChannel": SpeakParams?.extraParams?.soundChannel??3, "playType": SpeakParams?.extraParams?.playType??1 }; let speakParams: textToSpeech.SpeakParams = { requestId: SpeakParams.requestId, // requestId在同一实例内仅能用一次,请勿重复设置 extraParams: extraParams }; // 调用播报方法,开发者可以通过修改speakParams主动设置播报策略 this.ttsEngine?.speak(originalText, speakParams); }
4. 提供外部可获取音色的接口(当前服务只支持一种音色)
// 获取语种音色信息 getVoices(requestId: string): Promise<textToSpeech.VoiceInfo[]> { // 设置查询相关参数 let voicesQuery: textToSpeech.VoiceQuery = { requestId: requestId, // requestId在同一实例内仅能用一次,请勿重复设置 online: 1 }; return new Promise((resolve, reject) => { // 调用listVoices方法 this.ttsEngine?.listVoices(voicesQuery, (err: BusinessError, voiceInfo: textToSpeech.VoiceInfo[]) => { if (!err) { // 接收目前支持的语种音色等信息 resolve(voiceInfo); console.info(`Succeeded in listing voices, voiceInfo is ${JSON.stringify(voiceInfo)}`); } else { console.error(`Failed to list voices. Code: ${err.code}, message: ${err.message}`); reject(err) } }); }) }
5. 提供可停止语音播报的接口
// 停止播报 stop() { this.ttsEngine?.stop() }
6. 外部可释放实例的接口
// 释放实例 release () { this.ttsEngine?.shutdown() this.ttsEngine = undefined }
使用时,只需要根据需求调用上述单例中的接口即可,如在页面组件中创建实例并开启播报
Button("创建播报实例").onClick(() => { this.createCount++; TextToSpeechManager.getInstance().createEngine()})TextArea({ placeholder: '输入一段文本', text: `${this.originalText}` }).margin(20).onChange((value: string) => { this.originalText = value;})Button('朗读').onClick(() => { this.createCount++; TextToSpeechManager.getInstance().startSpeech(this.originalText, { requestId: this.requestId });})
语音智能助手实现
根据语音智能助手的工作内容,大致可以分为以下几类:
- 闲聊型:主要用于情感陪伴和日常对话,如微软小冰问答型:专注于知识获取和问题解答,如车载查胎压、家居智能机器人查天气指令型:用于设备 操作和任务执行,如家居机器人打开空调、Siri给通讯录打电话
语音智能助手的工作流程大致可以分为:
- 语音激活:检测环境声音,检测到特定触发词(如“小爱同学”),激活语音识别功能。在终端,也可通过按键进行激活语音识别:处理用户的语音输入意图识别:结合上下文,提取用户核心诉求场景化任务执行:根据用户诉求实现语音操控,如打开导航、播放指定音乐反馈与输出:生成自然流畅的语音反馈
通过鸿蒙的语音识别 API speechRecognizer 和 textToSpeech,并结合必要的 AI 大模型对话式 API,便可实现一个定制化的语音智能助手
在语音智能助手的实现上,除了语音的理解与反馈外,问答型与闲聊型都可以利用 AI 大模型的 API 来完成功能的闭环。作为AI的使用者,我们都知道现在有很多AI工具可以使用,如GPT、DeepSeek、纳米AI、文言一心等,都支持根据上下文生成内容。
最后
以上也只是对于 HarmonyOS 基础语音服务的简单应用,HarmonyOS 还提供了更多强大的AI功能。随着HarmonyOS的不断完善和发展,相信未来会有更多创新的应用出现在我们身边。