鸿蒙学习实战之路-语音识别-离线转文本实现
简单来说,这玩意儿就是把中文音频(支持中文普通话及中文语境下的英文)转换成文字,支持 PCM 音频文件或实时语音输入。短语音模式不超过 60 秒,长语音模式不超过 8 小时,特别适合手机/平板等设备在无网状态下使用。适用于听障人士辅助、会议记录、语音笔记等场景,尤其是在地铁、山区等无网环境下,依然能正常工作,相当实用!
鸿蒙学习实战之路-语音识别-离线转文本实现
最近好多朋友问我:“西兰花啊,我想做个鸿蒙应用,需要离线语音转文字功能,这玩意儿难不难啊?” 害,这问题可问对人了!作为一个正在把 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. 初始化引擎
接下来要创建语音识别引擎实例,这就像是给咱们的语音识别系统装个发动机~
let asrEngine: speechRecognizer.SpeechRecognitionEngine;
let sessionId: string = "123456"; // 自定义会话ID,随便起个就行
// 配置引擎参数
const initParams: 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. 设置识别回调
现在咱们得给引擎装个"耳朵",让它能把听到的内容反馈给咱们~
function setRecognitionListener() {
const listener: 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. 配置识别参数并启动
接下来咱们要给引擎设置一些具体参数,比如音频格式、采样率之类的,就像给汽车调校参数一样~
// 音频配置
const audioConfig: speechRecognizer.AudioInfo = {
audioType: "pcm", // 音频类型,必须是PCM
sampleRate: 16000, // 采样率(Hz),必须是16000
soundChannel: 1, // 声道数(1:单声道)
sampleBit: 16, // 采样位深,必须是16
};
// 识别参数
const recognizerParams: 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音频流
function writeAudioStream(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';
const TAG = 'SpeechRecognitionDemo';
let asrEngine: speechRecognizer.SpeechRecognitionEngine;
@Entry
@Component
struct SpeechRecognitionPage {
@State resultText: string = "识别结果将显示在这里~";
@State isRecording: boolean = false;
private sessionId: string = Date.now().toString(); // 用时间戳当会话ID,避免重复
private fileCapturer: FileCapturer = new FileCapturer();
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封装,更方便使用)
private initEngine() {
return new Promise<void>((resolve, reject) => {
if (asrEngine) {
resolve();
return;
}
const initParams: 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);
}
});
});
}
// 设置识别回调
private setListener() {
const listener: 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);
}
// 开始/停止录音识别
private async toggleRecording() {
if (this.isRecording) {
// 停止识别
asrEngine.finish(this.sessionId);
this.isRecording = false;
} else {
// 开始识别
try {
await this.initEngine();
const audioParam: 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 = `启动识别失败: ${(err as BusinessError).message}`;
}
}
}
// 文件识别
private async startFileRecognition() {
try {
await this.initEngine();
this.resultText = "正在读取文件并识别...";
// 配置音频参数
const audioParam: speechRecognizer.AudioInfo = {
audioType: 'pcm',
sampleRate: 16000,
soundChannel: 1,
sampleBit: 16
};
// 启动识别
asrEngine.startListening({
sessionId: this.sessionId,
audioInfo: audioParam
});
// 读取并写入音频文件
const context = this.getUIContext().getHostContext() as Context;
const filePath = context.resourceDir + "/test.pcm"; // 资源目录下的PCM文件
await this.fileCapturer.startRead(filePath, (data: Uint8Array) => {
asrEngine.writeAudio(this.sessionId, data);
});
} catch (err) {
this.resultText = `文件识别失败: ${(err as BusinessError).message}`;
}
}
}
文件读取工具类(FileCapturer.ets)
为了方便大家使用,我还写了个文件读取工具类,专门用来读取 PCM 音频文件~
import { fileIo } from "@kit.CoreFileKit";
const SEND_SIZE: number = 1280; // 每次发送的音频数据大小,必须是640或1280
export default class FileCapturer {
private mIsReading: boolean = false;
private mFile: fileIo.File | null = null;
// 读取PCM文件并通过回调返回数据
async startRead(
filePath: string,
callback: (data: Uint8Array) => void
): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.mIsReading = true;
this.mFile = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
const readLoop = async () => {
if (!this.mIsReading) return resolve();
const buffer = new ArrayBuffer(SEND_SIZE);
const bytesRead = fileIo.readSync(this.mFile!.fd, buffer, {
offset: 0,
});
if (bytesRead > 0) {
callback(new Uint8Array(buffer.slice(0, bytesRead)));
await new Promise((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 | 引擎已被销毁 | 重新初始化引擎 |
下一步行动
学会了离线语音识别,是不是感觉自己又掌握了一项新技能?接下来你可以尝试:
- 把这个功能集成到你的鸿蒙应用中
- 结合文本翻译功能,实现多语言语音翻译
- 优化识别结果的显示效果,比如添加实时字幕
推荐资源
📚 推荐资料:
我是盐焗西兰花,
不教理论,只给你能跑的代码和避坑指南。
下期见!🥦
更多推荐




所有评论(0)