鸿蒙学习实战之路-语音识别-离线转文本实现

最近好多朋友问我:“西兰花啊,我想做个鸿蒙应用,需要离线语音转文字功能,这玩意儿难不难啊?” 害,这问题可问对人了!作为一个正在把 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 引擎已被销毁 重新初始化引擎

下一步行动

学会了离线语音识别,是不是感觉自己又掌握了一项新技能?接下来你可以尝试:

  1. 把这个功能集成到你的鸿蒙应用中
  2. 结合文本翻译功能,实现多语言语音翻译
  3. 优化识别结果的显示效果,比如添加实时字幕

推荐资源

📚 推荐资料:

我是盐焗西兰花,
不教理论,只给你能跑的代码和避坑指南。
下期见!🥦

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐