鸿蒙学习实战之路-语音识别-离线转文本实现
最近好多朋友问我:“西兰花啊,我想做个鸿蒙应用,需要离线语音转文字功能,这玩意儿难不难啊?” 害,这问题可问对人了!作为一个正在把npm install炒成ohpm install的前端厨子_,我刚好用鸿蒙的 SpeechKit 实现过类似功能~
今天这篇,我就手把手带你实现离线语音识别转文本,全程不超过 10 分钟(不含下载依赖时间)~
功能概述
简单来说,这玩意儿就是把中文音频(支持中文普通话及中文语境下的英文)转换成文字,支持 PCM 音频文件或实时语音输入。短语音模式不超过 60 秒,长语音模式不超过 8 小时,特别适合手机/平板等设备在无网状态下使用。
场景介绍
适用于听障人士辅助、会议记录、语音笔记等场景,尤其是在地铁、山区等无网环境下,依然能正常工作,相当实用!
约束与限制
| 约束项 | 具体说明 |
|---|---|
| 支持语种 | 中文普通话 |
| 模型类型 | 离线 |
| 语音时长 | 短语音 ≤60 秒,长语音 ≤8 小时 |
| 音频格式 | PCM 格式,16000Hz 采样率,单声道,16 位采样位深 |
🥦西兰花警告:
我有个朋友第一次做的时候,随便用了个 MP3 格式的音频,结果识别率为 0!血泪教训啊朋友们,必须严格按照要求的音频格式来~
开发步骤
1. 导入依赖
首先咱们得把需要的工具库导进来,就像炒菜前先备菜一样~
import{speechRecognizer}from"@kit.CoreSpeechKit";import{BusinessError}from"@kit.BasicServicesKit";2. 初始化引擎
接下来要创建语音识别引擎实例,这就像是给咱们的语音识别系统装个发动机~
letasrEngine:speechRecognizer.SpeechRecognitionEngine;letsessionId:string="123456";// 自定义会话ID,随便起个就行// 配置引擎参数constinitParams:speechRecognizer.CreateEngineParams={language:"zh-CN",// 支持的语种,咱们就用中文online:1,// 0:离线模式, 1:在线模式(预留),这里虽然写1,但实际是离线extraParams:{locate:"CN",recognizerMode:"short",// short/long,根据需求选},};// 创建引擎speechRecognizer.createEngine(initParams,(err:BusinessError,engine:speechRecognizer.SpeechRecognitionEngine)=>{if(!err){console.info("引擎初始化成功,准备开工!");asrEngine=engine;// 设置回调监听setRecognitionListener();}else{// 错误码说明:// 1002200001: 语种不支持/模式不支持/初始化超时/资源不存在// 1002200006: 引擎忙碌中(多应用同时调用)// 1002200008: 引擎已被销毁console.error(`引擎初始化失败:${err.code}-${err.message}`);}});3. 设置识别回调
现在咱们得给引擎装个"耳朵",让它能把听到的内容反馈给咱们~
functionsetRecognitionListener(){constlistener:speechRecognizer.RecognitionListener={// 开始识别成功回调onStart(sessionId:string,eventMessage:string){console.info(`开始识别: sessionId=${sessionId}, message=${eventMessage}`);},// 事件回调onEvent(sessionId:string,eventCode:number,eventMessage:string){console.info(`事件通知: sessionId=${sessionId}, code=${eventCode}, message=${eventMessage}`);},// 识别结果回调(包含中间结果和最终结果)onResult(sessionId:string,result:speechRecognizer.SpeechRecognitionResult){console.info(`识别结果:${JSON.stringify(result)}`);// 结果格式: {result: "识别文本", isFinal: true/false}},// 识别完成回调onComplete(sessionId:string,eventMessage:string){console.info(`识别完成:${eventMessage}`);},// 错误回调onError(sessionId:string,errorCode:number,errorMessage:string){console.error(`识别错误:${errorCode}-${errorMessage}`);// 1002200002: 重复调用startListening},};asrEngine.setListener(listener);}4. 配置识别参数并启动
接下来咱们要给引擎设置一些具体参数,比如音频格式、采样率之类的,就像给汽车调校参数一样~
// 音频配置constaudioConfig:speechRecognizer.AudioInfo={audioType:"pcm",// 音频类型,必须是PCMsampleRate:16000,// 采样率(Hz),必须是16000soundChannel:1,// 声道数(1:单声道)sampleBit:16,// 采样位深,必须是16};// 识别参数constrecognizerParams:speechRecognizer.StartParams={sessionId:sessionId,audioInfo:audioConfig,extraParams:{recognitionMode:0,// 0:连续识别, 1:一句话识别vadBegin:2000,// 开始静音检测时长(ms)vadEnd:3000,// 结束静音检测时长(ms)maxAudioDuration:20000,// 最大音频时长(ms)},};// 启动识别asrEngine.startListening(recognizerParams);🥦西兰花小贴士:
这里的音频参数一定要严格按照约束与限制里的要求设置,不然识别结果会惨不忍睹哦~
5. 写入音频流
现在咱们需要把音频数据喂给引擎,就像给发动机加油一样~
// 从文件读取或麦克风获取PCM音频流functionwriteAudioStream(audioData:Uint8Array){// 音频流长度需为640或1280字节,这点很重要!asrEngine.writeAudio(sessionId,audioData);}6. 资源释放
最后,咱们用完引擎后一定要记得释放资源,就像用完厨房要打扫卫生一样~
// 结束识别asrEngine.finish(sessionId);// 取消识别asrEngine.cancel(sessionId);// 释放引擎资源asrEngine.shutdown();7. 权限配置
别忘记在module.json5中添加麦克风权限,不然应用会直接崩溃哦~
{"module":{"requestPermissions":[{"name":"ohos.permission.MICROPHONE","reason":"需要麦克风权限进行语音识别","usedScene":{"abilities":["EntryAbility"],"when":"inuse"}}]}}开发实例
说了这么多,咱们直接上完整的示例代码吧!我给大家准备了一个简单的页面,包含录音识别和文件识别两种功能~
完整页面实现(Index.ets)
import{speechRecognizer}from'@kit.CoreSpeechKit';import{BusinessError}from'@kit.BasicServicesKit';import{fileIo}from'@kit.CoreFileKit';import{PromptAction}from'@kit.ArkUI';constTAG='SpeechRecognitionDemo';letasrEngine:speechRecognizer.SpeechRecognitionEngine;@Entry @Component struct SpeechRecognitionPage{@State resultText:string="识别结果将显示在这里~";@State isRecording:boolean=false;privatesessionId:string=Date.now().toString();// 用时间戳当会话ID,避免重复privatefileCapturer:FileCapturer=newFileCapturer();build(){Column(){Text("语音识别演示").fontSize(24).fontWeight(FontWeight.Bold).margin(20).fontColor('#333');Text(this.resultText).width('90%').height(200).borderWidth(1).borderRadius(5).padding(10).margin(10).textAlign(TextAlign.Start).backgroundColor('#f5f5f5');Row(){Button(this.isRecording?"停止识别":"开始录音识别").onClick(()=>this.toggleRecording()).type(ButtonType.Capsule).backgroundColor(this.isRecording?'#ff4d4f':'#007dff').width(150).height(40).margin(10);Button("文件识别").onClick(()=>this.startFileRecognition()).type(ButtonType.Capsule).backgroundColor('#007dff').width(150).height(40).margin(10);}}.width('100%').height('100%').justifyContent(FlexAlign.Center);}// 初始化引擎(用Promise封装,更方便使用)privateinitEngine(){returnnewPromise<void>((resolve,reject)=>{if(asrEngine){resolve();return;}constinitParams:speechRecognizer.CreateEngineParams={language:'zh-CN',online:1,extraParams:{"locate":"CN","recognizerMode":"short"}};speechRecognizer.createEngine(initParams,(err:BusinessError,engine:speechRecognizer.SpeechRecognitionEngine)=>{if(!err){asrEngine=engine;this.setListener();resolve();}else{this.resultText=`引擎初始化失败:${err.message}`;reject(err);}});});}// 设置识别回调privatesetListener(){constlistener:speechRecognizer.RecognitionListener={onResult:(sessionId:string,result:speechRecognizer.SpeechRecognitionResult)=>{this.resultText=result.result;},onError:(sessionId:string,errorCode:number,errorMessage:string)=>{this.resultText=`识别错误:${errorCode}-${errorMessage}`;this.isRecording=false;},onComplete:()=>{this.isRecording=false;PromptAction.showToast({message:"识别完成"});}};asrEngine.setListener(listener);}// 开始/停止录音识别privateasynctoggleRecording(){if(this.isRecording){// 停止识别asrEngine.finish(this.sessionId);this.isRecording=false;}else{// 开始识别try{awaitthis.initEngine();constaudioParam:speechRecognizer.AudioInfo={audioType:'pcm',sampleRate:16000,soundChannel:1,sampleBit:16};asrEngine.startListening({sessionId:this.sessionId,audioInfo:audioParam});this.isRecording=true;this.resultText="正在识别...";}catch(err){this.resultText=`启动识别失败:${(errasBusinessError).message}`;}}}// 文件识别privateasyncstartFileRecognition(){try{awaitthis.initEngine();this.resultText="正在读取文件并识别...";// 配置音频参数constaudioParam:speechRecognizer.AudioInfo={audioType:'pcm',sampleRate:16000,soundChannel:1,sampleBit:16};// 启动识别asrEngine.startListening({sessionId:this.sessionId,audioInfo:audioParam});// 读取并写入音频文件constcontext=this.getUIContext().getHostContext()asContext;constfilePath=context.resourceDir+"/test.pcm";// 资源目录下的PCM文件awaitthis.fileCapturer.startRead(filePath,(data:Uint8Array)=>{asrEngine.writeAudio(this.sessionId,data);});}catch(err){this.resultText=`文件识别失败:${(errasBusinessError).message}`;}}}文件读取工具类(FileCapturer.ets)
为了方便大家使用,我还写了个文件读取工具类,专门用来读取 PCM 音频文件~
import{fileIo}from"@kit.CoreFileKit";constSEND_SIZE:number=1280;// 每次发送的音频数据大小,必须是640或1280exportdefaultclassFileCapturer{privatemIsReading:boolean=false;privatemFile:fileIo.File|null=null;// 读取PCM文件并通过回调返回数据asyncstartRead(filePath:string,callback:(data:Uint8Array)=>void):Promise<void>{returnnewPromise((resolve,reject)=>{try{this.mIsReading=true;this.mFile=fileIo.openSync(filePath,fileIo.OpenMode.READ_ONLY);constreadLoop=async()=>{if(!this.mIsReading)returnresolve();constbuffer=newArrayBuffer(SEND_SIZE);constbytesRead=fileIo.readSync(this.mFile!.fd,buffer,{offset:0,});if(bytesRead>0){callback(newUint8Array(buffer.slice(0,bytesRead)));awaitnewPromise((resolve)=>setTimeout(resolve,40));// 模拟实时音频流readLoop();}else{fileIo.closeSync(this.mFile!);resolve();}};readLoop();}catch(e){reject(e);}});}// 停止读取stop(){this.mIsReading=false;if(this.mFile){fileIo.closeSync(this.mFile);}}}🥦西兰花警告:
示例中的test.pcm文件需要你自己准备哦,记得放在资源目录下,并且格式要符合要求!
错误码参考
为了避免大家遇到问题时手忙脚乱,我整理了一些常见的错误码和解决方案~
| 错误码 | 说明 | 解决方案 |
|---|---|---|
| 1002200001 | 引擎初始化失败 | 检查语种设置/确保模型文件存在 |
| 1002200002 | 重复调用 startListening | 确保前一次识别已完成 |
| 1002200006 | 引擎忙碌中 | 等待其他应用释放引擎资源 |
| 1002200008 | 引擎已被销毁 | 重新初始化引擎 |
下一步行动
学会了离线语音识别,是不是感觉自己又掌握了一项新技能?接下来你可以尝试:
- 把这个功能集成到你的鸿蒙应用中
- 结合文本翻译功能,实现多语言语音翻译
- 优化识别结果的显示效果,比如添加实时字幕
推荐资源
📚 推荐资料:
- Core Speech Kit 概述
- API 参考文档
- 错误码完整列表
我是盐焗西兰花,
不教理论,只给你能跑的代码和避坑指南。
下期见!🥦