上周,我接到一个需求:为公司的鸿蒙应用添加语音输入功能。用户可以通过说话来输入指令,无需手动打字。这听起来很酷,但我心里直打鼓——之前从没接触过鸿蒙的语音模块,不知道实现难度大不大。

没想到,经过一番摸索,我发现鸿蒙的Core Speech Kit(基础语音服务)设计得非常人性化。从环境配置到功能上线,我只用了两天时间。今天,我就把整个实战过程整理出来,希望能帮你少走弯路。

一、Core Speech Kit能做什么?

简单说,这个工具包主要解决两个问题:让APP“说话”让APP“听懂”

文本转语音(TTS):把文字变成声音播报。比如,你可以让APP朗读新闻内容,或者播放操作指引。Core Speech Kit支持离线播报,最高能处理10000字符的长文本,满足绝大多数场景需求。

语音识别(ASR):把用户说的话转成文字。这又分为两种模式:

  • 短语音模式:60秒以内的语音指令识别,适合即时搜索、语音输入
  • 长语音模式:最长8小时的连续录音转写,适合会议记录、录音整理

我这次主要用的是语音识别功能。实测下来,它的中文识别准确率相当不错,即使在嘈杂环境下(我特意在咖啡厅测试过),也能保持较高的识别率。

二、环境配置:三步完成初始化

1. 添加权限声明

语音识别需要麦克风权限,可能还需要网络权限(如果你使用云端识别)。在module.json5文件中添加:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:reason_microphone",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:reason_internet",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      }
    ]
  }
}

注意:reason字段需要在string.json中定义对应的字符串资源。

2. 安装依赖

在项目的oh-package.json5中添加语音识别依赖:

{
  "dependencies": {
    "@kit.CoreSpeechKit": "^1.0.0"
  }
}

然后运行ohpm install安装依赖包。

3. 初始化识别引擎

创建一个专门管理语音识别的类,我将其命名为VoiceRecognitionManager

// VoiceRecognitionManager.ets
import { speechRecognizer } from '@kit.CoreSpeechKit'
import { BusinessError } from '@kit.BasicServicesKit'

export class VoiceRecognitionManager {
  private asrEngine: speechRecognizer.SpeechRecognitionEngine | null = null
  private sessionId: string = 'voice_session_' + Date.now()

  // 初始化识别引擎
  initEngine(): Promise<boolean> {
    return new Promise((resolve, reject) => {
      let extraParam = {
        "locate": "CN",                // 中国地区
        "recognizerMode": "short"      // 短语音模式
      }

      let initParams: speechRecognizer.CreateEngineParams = {
        language: 'zh-CN',             // 中文识别
        online: 1,                     // 在线模式(准确率更高)
        extraParams: extraParam
      }

      speechRecognizer.createEngine(initParams, (err: BusinessError, engine: speechRecognizer.SpeechRecognitionEngine) => {
        if (!err) {
          this.asrEngine = engine
          console.info('语音识别引擎初始化成功')
          this.setRecognitionListener()
          resolve(true)
        } else {
          console.error(`引擎创建失败:${err.message},错误码:${err.code}`)
          reject(err)
        }
      })
    })
  }

  // 设置识别回调
  private setRecognitionListener() {
    const listener: speechRecognizer.RecognitionListener = {
      onStart: (sessionId: string) => {
        console.info(`开始识别,会话ID:${sessionId}`)
        // 这里可以触发UI更新,比如显示声波动画
      },

      onResult: (sessionId: string, result: speechRecognizer.SpeechRecognitionResult) => {
        console.info(`识别结果:${JSON.stringify(result)}`)
        // result.result是识别到的文本
        // result.isFinal表示当前这段话是否已经说完
        if (result.isFinal) {
          this.handleFinalResult(result.result)
        } else {
          this.handleIntermediateResult(result.result)
        }
      },

      onError: (sessionId: string, errorCode: number, errorMessage: string) => {
        console.error(`识别异常:${errorCode} - ${errorMessage}`)
        this.handleRecognitionError(errorCode)
      },

      onComplete: (sessionId: string) => {
        console.info(`识别完成,会话ID:${sessionId}`)
      }
    }

    this.asrEngine?.setListener(listener)
  }

  // 开始录音识别
  startListening(): void {
    if (!this.asrEngine) {
      console.error('请先初始化识别引擎')
      return
    }

    let recognizerParams: speechRecognizer.StartParams = {
      sessionId: this.sessionId,
      audioInfo: {
        audioType: 'pcm',
        sampleRate: 16000,   // 16kHz采样率
        soundChannel: 1,     // 单声道
        sampleBit: 16        // 16位采样位
      },
      extraParams: {
        "recognitionMode": 0,    // 流式识别
        "vadBegin": 2000,        // 静音2秒后开始识别
        "vadEnd": 3000,          // 静音3秒后停止识别
        "maxAudioDuration": 20000 // 最长录音20秒
      }
    }

    try {
      this.asrEngine.startListening(recognizerParams)
      console.info('开始语音识别')
    } catch (error) {
      console.error('启动识别失败:', error)
    }
  }

  // 停止识别
  stopListening(): void {
    this.asrEngine?.finish(this.sessionId)
    console.info('停止语音识别')
  }

  // 释放资源
  release(): void {
    this.asrEngine?.shutdown()
    this.asrEngine = null
    console.info('语音识别资源已释放')
  }

  // 处理最终识别结果
  private handleFinalResult(text: string): void {
    console.info(`最终识别结果:${text}`)
    // 这里可以将结果传递给UI组件或业务逻辑
    // 比如:EventBus.emit('voice_result', text)
  }

  // 处理中间识别结果
  private handleIntermediateResult(text: string): void {
    console.info(`中间识别结果:${text}`)
    // 实时更新UI,比如显示正在识别的文字
  }

  // 处理识别错误
  private handleRecognitionError(errorCode: number): void {
    let errorMessage = '未知错误'
    switch (errorCode) {
      case 1002200002:
        errorMessage = '重复启动识别,请先停止上一次识别'
        break
      case 1002200006:
        errorMessage = '识别引擎忙碌,请稍后重试'
        break
      case 1002200010:
        errorMessage = '网络异常,请检查网络连接'
        break
      default:
        errorMessage = `错误码:${errorCode}`
    }
    console.error(`语音识别错误:${errorMessage}`)
  }
}

三、UI集成:让语音交互更友好

代码写完了,接下来要设计用户界面。我遵循鸿蒙的设计规范,创建了一个简洁的语音输入组件:

// VoiceInputComponent.ets
@Component
export struct VoiceInputComponent {
  @State isRecording: boolean = false
  @State recognizedText: string = ''
  @State errorMessage: string = ''
  
  private voiceManager: VoiceRecognitionManager = new VoiceRecognitionManager()

  aboutToAppear(): void {
    // 初始化语音识别引擎
    this.voiceManager.initEngine()
      .then(() => {
        console.info('语音组件初始化完成')
      })
      .catch((error) => {
        this.errorMessage = '语音功能初始化失败'
        console.error(error)
      })
  }

  aboutToDisappear(): void {
    // 释放资源
    this.voiceManager.release()
  }

  build() {
    Column({ space: 20 }) {
      // 标题
      Text('语音输入')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.Black)

      // 录音按钮
      Button(this.isRecording ? '正在录音...' : '点击说话')
        .width(150)
        .height(150)
        .backgroundColor(this.isRecording ? Color.Red : Color.Blue)
        .fontColor(Color.White)
        .fontSize(18)
        .borderRadius(75)
        .onClick(() => {
          this.toggleRecording()
        })

      // 识别结果显示
      if (this.recognizedText) {
        Text(this.recognizedText)
          .fontSize(16)
          .fontColor(Color.Gray)
          .width('90%')
          .textAlign(TextAlign.Center)
          .padding(10)
          .backgroundColor(Color.White)
          .borderRadius(8)
          .border({ width: 1, color: Color.Grey })
      }

      // 错误提示
      if (this.errorMessage) {
        Text(this.errorMessage)
          .fontSize(14)
          .fontColor(Color.Red)
          .width('90%')
          .textAlign(TextAlign.Center)
      }

      // 使用提示
      Text('提示:说话时请靠近麦克风,保持环境安静')
        .fontSize(12)
        .fontColor(Color.Grey)
        .width('90%')
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }

  private toggleRecording(): void {
    if (this.isRecording) {
      this.voiceManager.stopListening()
      this.isRecording = false
    } else {
      this.recognizedText = ''
      this.errorMessage = ''
      this.voiceManager.startListening()
      this.isRecording = true
      
      // 30秒后自动停止(防止用户忘记)
      setTimeout(() => {
        if (this.isRecording) {
          this.toggleRecording()
        }
      }, 30000)
    }
  }
}

四、避坑指南:我遇到的四个典型问题

1. 权限申请时机不对

一开始,我在组件初始化时就申请麦克风权限,结果发现有些用户会拒绝。后来我改成了按需申请:只有当用户点击录音按钮时,才检查并申请权限。如果用户拒绝,就给出友好的引导提示。

2. 音频格式不符合要求

Core Speech Kit对音频格式有严格要求:必须是16kHz采样率、16位采样位、单声道的PCM数据。我最初尝试用44.1kHz的录音,结果识别率极低。后来使用系统推荐的参数,问题迎刃而解。

3. 识别引擎重复启动

错误码1002200002让我头疼了很久。原来,如果在上一次识别还没结束时又调用startListening(),就会触发这个错误。解决方法很简单:在开始新识别前,先检查当前是否正在录音。

// 改进后的startListening方法
startListening(): void {
  if (this.isRecording) {
    console.warn('当前正在录音,请先停止')
    return
  }
  // ...原有逻辑
}

4. 长语音模式的内存溢出

测试长语音识别时,APP偶尔会崩溃。经过分析,发现是音频数据积压导致内存溢出。解决方案是流式写入:每采集一小段音频就立即写入引擎,而不是等全部录完再处理。

// 流式写入示例
private writeAudioStream(audioData: Uint8Array): void {
  // 每次写入1280字节(系统推荐值)
  const chunkSize = 1280
  for (let i = 0; i < audioData.length; i += chunkSize) {
    const chunk = audioData.slice(i, i + chunkSize)
    this.asrEngine?.writeAudio(this.sessionId, chunk)
  }
}

五、性能优化:让语音识别更流畅

经过一段时间的优化,我总结了三个提升体验的技巧:

1. 引擎预热

语音识别引擎初始化需要时间(大约1-2秒)。为了做到“零延迟”启动,我建议在APP启动时就初始化引擎,而不是等到用户点击录音按钮。

// 在应用启动时预热
AppStorage.setOrCreate('voiceManager', new VoiceRecognitionManager())
AppStorage.get<VoiceRecognitionManager>('voiceManager').initEngine()

2. 错误降级策略

网络不稳定时,云端识别可能失败。我的做法是:优先使用云端识别,失败时自动切换到离线模式。虽然离线模式准确率稍低,但至少保证了功能可用性。

3. 上下文优化

对于指令类应用,我建立了一个常用词库。当识别结果与词库匹配时,给予更高的置信度,减少误判。

写在最后:从“能用”到“好用”的思考

说实话,语音识别功能的实现并不难。Core Speech Kit提供了完整的API,跟着文档一步步走,基本不会出错。真正的挑战在于用户体验

在我上线的第一个版本中,用户反馈最多的问题是:“我不知道它有没有在听我说话”。于是,我加入了视觉反馈(按钮颜色变化、声波动画)和触觉反馈(振动提示)。这些细节看似微小,却大大提升了用户的使用信心。

另一个深刻的体会是:AI能力需要与业务场景深度结合。单纯把语音转成文字还不够,我们需要理解用户的意图。比如,当用户说“打开空调”时,我们的APP应该能区分是“打开客厅空调”还是“打开卧室空调”。这需要更复杂的自然语言处理,也是我下一步的探索方向。

如果你也想在鸿蒙应用中加入语音功能,我的建议是:从小处着手,快速验证。先做一个最简单的Demo,测试核心功能是否跑通。然后,再逐步优化细节,提升体验。记住,完美的功能是迭代出来的,不是一次性设计出来的。

感谢阅读,希望这篇实战分享对你有帮助。如果你在开发过程中遇到问题,或者有更好的实践经验,欢迎留言交流。让我们一起让鸿蒙应用更智能、更好用。

Logo

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

更多推荐