在这里插入图片描述

HarmonyOS技术精讲-Core Speech Kit(基础语音服务)第2篇:语音识别核心功能——流式与非流式实现

语音识别在HarmonyOS应用开发中,很多场景都绕不开。比如语音搜索、语音转写、语音控制等。官方提供的Core Speech Kit(基础语音服务)里,SpeechRecognizer这个API是核心入口。很多人第一次接触时,会发现官方示例能运行,但放到实际项目里,状态同步、生命周期、回调处理这些细节很容易出问题。

这个功能本身不复杂,但不同场景选错模式,体验会差很多。流式识别适合实时反馈,比如边说话边出文字;一次性识别适合离线快速处理一段固定的音频。选错方案,要么延迟太高,要么资源浪费。

它解决什么问题

SpeechRecognizer 提供了两种语音识别模式:

  • 流式识别(Streaming):边输入边输出结果,适合实时语音转写、语音搜索。
  • 非流式识别(One-shot):输入一段完整的音频,一次性返回识别结果,适合语音指令、固定音频转写。
特性 流式识别 非流式识别
输入方式 持续麦克风输入 一次性音频文件
实时性 高,边说话边出结果 低,等待完整音频处理
延迟 低,结果逐步返回 较高,需完整处理后返回
适用场景 实时字幕、语音搜索 离线命令、一键转写
资源消耗 持续占用麦克风和网络 单次请求,占用少
离线支持 支持(需加载离线模型) 支持(需加载离线模型)

推荐场景

  • 流式:语音转字幕、语音搜索、语音助手实时交互
  • 非流式:语音指令(如“打开设置”)、会议录音转写

不推荐场景

  • 不需要实时反馈的,不应该用流式,浪费资源
  • 需要快速响应的,不应该用非流式,用户等待体验差

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机 / 平板

核心实现:流式语音识别

流式识别核心是startListeningstopListening。关键在于正确设置setListener的回调,处理onResultsonErroronEndOfSpeech等事件。

// StreamingRecognizer.ets
import { speechRecognizer } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { audio } from '@kit.AudioKit';

@Entry
@Component
struct StreamingRecognizerPage {
  @State recognizedText: string = '';
  @State isListening: boolean = false;
  private recognizer: speechRecognizer.SpeechRecognizer | null = null;
  private audioCapturer: audio.AudioCapturer | null = null;

  aboutToAppear(): void {
    this.initRecognizer();
  }

  aboutToDisappear(): void {
    this.stopListening();
    this.destroyRecognizer();
  }

  private initRecognizer(): void {
    try {
      this.recognizer = speechRecognizer.createRecognizer();
      this.recognizer.setListener({
        onResults: (text: string) => {
          // 流式结果逐步更新
          this.recognizedText = text;
        },
        onError: (error: BusinessError) => {
          console.error('Recognizer error:', error.message);
          this.isListening = false;
        },
        onEndOfSpeech: () => {
          // 用户停止说话,自动结束
          this.isListening = false;
        },
        onStartListening: () => {
          console.log('开始监听');
        }
      });
    } catch (error) {
      console.error('Create recognizer failed:', error.message);
    }
  }

  private startListening(): void {
    if (!this.recognizer) {
      return;
    }
    try {
      // 配置识别参数:语言、模式
      const config: speechRecognizer.SpeechRecognizerConfig = {
        language: 'zh-CN',          // 简体中文
        recognitionMode: speechRecognizer.RecognitionMode.STREAMING, // 流式模式
        enablePunctuation: true,    // 启用标点
        accuracy: speechRecognizer.RecognitionAccuracy.NORMAL
      };
      this.recognizer.startListening(config);
      this.isListening = true;
      this.recognizedText = '';
    } catch (error) {
      console.error('Start listening failed:', error.message);
    }
  }

  private stopListening(): void {
    if (this.recognizer && this.isListening) {
      try {
        this.recognizer.stopListening();
      } catch (error) {
        console.error('Stop listening failed:', error.message);
      }
      this.isListening = false;
    }
  }

  private destroyRecognizer(): void {
    if (this.recognizer) {
      try {
        this.recognizer.destroy();
      } catch (error) {
        console.error('Destroy recognizer failed:', error.message);
      }
      this.recognizer = null;
    }
  }

  build() {
    Column({ space: 20 }) {
      Text('流式语音识别')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Text(this.recognizedText || '等待语音输入...')
        .width('90%')
        .height(200)
        .backgroundColor('#F5F5F5')
        .borderRadius(12)
        .padding(16)
        .fontSize(18)
        .textAlign(TextAlign.Start)

      Button(this.isListening ? '停止录音' : '开始录音')
        .width(200)
        .height(50)
        .onClick(() => {
          if (this.isListening) {
            this.stopListening();
          } else {
            this.startListening();
          }
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

注意事项

  • setListener必须在startListening之前调用,否则会错过回调
  • 页面销毁时一定要调用destroy(),否则会泄漏资源
  • 流式模式下,onResults会频繁回调,每次更新新的识别结果

核心实现:非流式(一次性)识别

非流式识别适合传入一个完整的音频文件(支持PCM、WAV等格式)。核心是recognize方法,传入音频数据,通过回调返回结果。

// OneShotRecognizer.ets
import { speechRecognizer } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct OneShotRecognizerPage {
  @State resultText: string = '';
  @State isProcessing: boolean = false;
  private recognizer: speechRecognizer.SpeechRecognizer | null = null;

  aboutToAppear(): void {
    this.initRecognizer();
  }

  aboutToDisappear(): void {
    this.destroyRecognizer();
  }

  private initRecognizer(): void {
    try {
      this.recognizer = speechRecognizer.createRecognizer();
      this.recognizer.setListener({
        // 非流式一次性识别结果
        onResults: (text: string) => {
          this.resultText = text;
          this.isProcessing = false;
        },
        onError: (error: BusinessError) => {
          console.error('Recognizer error:', error.message);
          this.isProcessing = false;
        }
      });
    } catch (error) {
      console.error('Create recognizer failed:', error.message);
    }
  }

  private async startRecognition(): Promise<void> {
    if (!this.recognizer || this.isProcessing) {
      return;
    }

    this.isProcessing = true;
    this.resultText = '';

    try {
      // 读取音频文件
      const context = getContext(this) as common.UIAbilityContext;
      const fileUri: string = context.cacheDir + '/test_audio.pcm';
      const file: fileIo.File = await fileIo.open(fileUri, fileIo.OpenMode.READ_ONLY);
      const buffer: ArrayBuffer = new ArrayBuffer(fileIo.statSync(file.fd).size);
      await fileIo.read(file.fd, buffer);
      fileIo.close(file);

      // 配置并执行识别
      const config: speechRecognizer.SpeechRecognizerConfig = {
        language: 'zh-CN',
        recognitionMode: speechRecognizer.RecognitionMode.ONE_SHOT, // 一次性模式
        enablePunctuation: true
      };

      // 第二个参数是音频格式
      this.recognizer.recognize(buffer, config, speechRecognizer.AudioFormat.PCM_16BIT);
    } catch (error) {
      console.error('Recognition failed:', error.message);
      this.isProcessing = false;
    }
  }

  private destroyRecognizer(): void {
    if (this.recognizer) {
      try {
        this.recognizer.destroy();
      } catch (error) {
        console.error('Destroy recognizer failed:', error.message);
      }
      this.recognizer = null;
    }
  }

  build() {
    Column({ space: 20 }) {
      Text('一次性语音识别')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Text(this.resultText || '请选择音频文件进行识别')
        .width('90%')
        .height(200)
        .backgroundColor('#F5F5F5')
        .borderRadius(12)
        .padding(16)
        .fontSize(18)
        .textAlign(TextAlign.Start)

      Button(this.isProcessing ? '识别中...' : '开始识别(从缓存读取音频)')
        .width(200)
        .height(50)
        .enabled(!this.isProcessing)
        .onClick(() => {
          this.startRecognition();
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

注意事项

  • 一次性识别的音频格式必须与recognize方法传入的参数一致,否则识别失败
  • 音频文件路径需要是应用沙箱内的路径,不能是外部路径
  • 一次性识别不支持部分结果,只有等处理完成后才回调onResults

配置热词与自定义词库

热词功能可以提升特定词汇的识别准确率。比如医疗、法律等专业领域的术语。

// 配置热词
private addCustomWords(words: string[]): void {
  if (!this.recognizer) {
    return;
  }
  try {
    // 自定义词汇接口
    this.recognizer.addWords(words);
    console.log('Custom words added:', words.join(', '));
  } catch (error) {
    console.error('Add custom words failed:', error.message);
  }
}

// 使用示例
private addHotWords(): void {
  const hotWords: string[] = ['鸿蒙', 'ArkTS', 'HarmonyOS', '语音识别'];
  this.addCustomWords(hotWords);
}

注意事项

  • 热词最好在startListeningrecognize之前添加
  • 热词列表长度有限制(约50-100个),具体看设备能力
  • 热词只对当前识别器实例有效,重新创建后需要重新添加

常见问题与踩坑记录

问题1:页面返回后识别器状态丢失

现象:页面跳转到其他页面再返回后,调用startListening报错,或者回调不触发。

原因SpeechRecognizer实例在页面销毁时没有被正确释放,重新进入页面时创建了新实例,但旧的实例可能还持有音频资源。

解决方案

  • 严格遵循生命周期:aboutToAppear中创建,aboutToDisappear中销毁
  • 不要在页面间共享同一个识别器实例,每个页面独立创建
  • 如果使用单页面架构,确保在onPageHide时停止识别,onPageShow时恢复

问题2:多次快速开始识别导致崩溃

现象:用户连续快速点击“开始录音”按钮,应用闪退或卡死。

原因SpeechRecognizer内部状态机不支持在startListening后未stopListening前再次调用startListening。连续快速点击会破坏状态。

解决方案

// 使用防抖或状态锁定
private startListeningSafe(): void {
  if (this.isListening || this.isProcessing) {
    console.warn('Recognizer is busy, ignore this request');
    return;
  }
  // 确保先停止再启动
  if (this.recognizer) {
    try {
      this.recognizer.stopListening();
    } catch (e) {
      // 忽略未启动时的错误
    }
  }
  this.startListening();
}

最佳实践

  1. 不要在回调里直接做耗时操作onResults回调是在异步线程中执行的,直接在里面更新UI没有问题,但如果做文件IO、网络请求,会阻塞后续回调处理。建议使用postMessagerunOnMainThread回到主线程处理。

  2. 初始化识别器时检查权限。语音识别需要ohos.permission.MICROPHONE权限。如果没有提前授权,startListening会静默失败。建议在页面加载时先请求权限。

  3. 合理选择识别模式。流式识别内部会持续占用麦克风和编解码资源,如果页面不可见(比如被压入后台),应该主动调用stopListening释放资源。一次性识别则不需要,直接完成后自动释放。

FAQ

Q:为什么流式识别在真机上一直返回空字符串,模拟器正常?
A:模拟器通常使用虚拟麦克风输入,真机需要确认是否授权ohos.permission.MICROPHONE权限,以及是否有其他应用占用了麦克风。

Q:页面返回后,重新进入识别页面,识别结果不更新怎么办?
A:检查aboutToAppear中是否重新创建了识别器实例。如果创建了但旧实例没有正确销毁,会导致识别器状态冲突。建议在aboutToDisappear中调用destroy()

Q:离线模式下,首次使用需要下载模型,下载失败如何处理?
A:离线识别需要下载语音模型包(约100-200MB),下载过程是异步的。建议在aboutToAppear中调用speechRecognizer.downloadModel,监听下载进度,如果失败可以提示用户检查网络或切换在线模式。

Logo

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

更多推荐