在这里插入图片描述

前言

在 HarmonyOS NEXT 的智能生态中,语音是最自然的人机交互入口。与纯云端方案相比,端侧语音处理具备响应时延低、离线可用、隐私安全等显著优势——用户的语音数据无需离开设备,指令可以毫秒级执行。本文将围绕 speechRecognizer 语音识别TextToSpeech 语音合成 两大核心能力,结合「实时字幕」与「语音助手」两个典型场景,深入剖析 API 调用链路、状态机管理、权限配置,以及性能调优的实战技巧。所有代码均基于 HarmonyOS NEXT / API 12+ 编写,可直接移植到项目中运行。


一、架构概览:端侧语音的两大支柱

HarmonyOS NEXT 提供了两套互补的端侧语音引擎:

Speech Recognizer(语音识别)负责将麦克风采集的音频流实时转换为文本。它支持两种模式:一是流式识别(streaming),边说边出文字,适合对话和字幕;二是一次性识别(once),等用户说完再返回完整文本,适合短指令。

**TextToSpeech(语音合成)**则反向工作:将文本字符串转换为自然语音输出,支持语速、语调、音量调节,可配合语音助手实现人机对话闭环。

两端之间,还有一条隐含的数据流:**VAD(Voice Activity Detection,语音活动检测)**负责判断用户何时开始说话、何时结束,这直接影响流式识别的用户体验。

┌──────────────┐     ┌──────────────────┐     ┌──────────────┐
│   麦克风采集  │ ──▶ │  speechRecognizer │ ──▶ │   识别结果   │
└──────────────┘     └──────────────────┘     └──────────────┘
                                                      │
                                                      ▼
┌──────────────┐     ┌──────────────────┐     ┌──────────────┐
│  合成音频播放 │ ◀── │   TextToSpeech   │ ◀── │   输入文本   │
└──────────────┘     └──────────────────┘     └──────────────┘

在实际应用中,识别结果往往会触发合成播报,形成完整的语音交互闭环——这就是「语音助手」的核心逻辑。接下来,我们逐一拆解每个环节。


二、权限配置:一切的开始

端侧语音能力涉及敏感硬件(麦克风)和系统能力(语音服务),必须在 module.json5 中声明对应权限,并在应用启动时动态申请。

2.1 在 module.json5 中声明权限

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

ohos.permission.MICROPHONE 是识别能力的硬性前提;ohos.permission.INTERNET 则用于某些需要联网语言模型增强识别的场景(如中文方言识别)。纯端侧模型通常不需要联网,这里按需配置。

2.2 动态权限申请

仅在配置文件中声明还不够,应用需要在 UIAbility 中显式请求麦克风权限:

import { abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';
import { microphone } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class EntryAbility extends UIAbility {
  private atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();

  async requestPermissions(): Promise<boolean> {
    let grantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;

    // 检查是否已有权限
    let data = await this.atManager.checkAccessToken(
      this.context.abilityInfo!.accessTokenId,
      'ohos.permission.MICROPHONE'
    );

    if (data === grantStatus) {
      console.info('麦克风权限已授权');
      return true;
    }

    // 发起动态申请
    let permissions: Array<string> = ['ohos.permission.MICROPHONE'];
    try {
      const result = await this.atManager.requestPermissionsFromUser(
        this.context, permissions
      );
      return result.authResults.every(r => r === 0);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`权限申请失败: ${error.code} ${error.message}`);
      return false;
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 启动时即申请权限
    this.requestPermissions();
    // ... 后续窗口配置
  }
}

权限申请是用户在设备上首次使用语音功能的必经门槛。许多开发者在此处卡住——应用能跑但识别无响应,根源往往就是权限未授予。务必在 onWindowStageCreate 或主动触发语音入口前完成申请流程。


三、语音识别:流式识别引擎深度解析

3.1 核心类与生命周期

speechRecognizer 的核心类是 SpeechRecognizer,但直接使用低阶 API 较为繁琐。HarmonyOS NEXT 推荐使用封装了状态机的 ContinuousRecognizer(连续识别)和 ShortRecognizer(短词识别)。我们以 流式连续识别 为例,这是实时字幕场景的核心依赖。

识别器的基本生命周期如下:

创建 → 配置 → 启动 → (识别中:多次 onPartialResult) → 结束 → 销毁

关键状态通过 RecognizerState 枚举体现:IDLEREADYRECOGNIZINGCOMPLETEERROR。建议在 UI 层绑定状态显示,让用户直观感知当前是否在收音。

3.2 完整识别器实现

以下是一个完整封装了生命周期管理的 SpeechRecognitionManager,支持流式识别、临时中断恢复、以及错误自动重试:

// SpeechRecognitionManager.ets
import { speechRecognizer } from '@kit.SpeechSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { picker } from '@kit.CoreFileKit';

export type RecognizerCallback = {
  onStart?: () => void;
  onPartialResult?: (text: string) => void;
  onFinalResult?: (text: string) => void;
  onError?: (code: number, message: string) => void;
  onComplete?: () => void;
};

export class SpeechRecognitionManager {
  private recognizer: speechRecognizer.SpeechRecognizer | null = null;
  private audioRecorder?: speechRecognizer.AudioRecorder;
  private callback: RecognizerCallback;
  private isListening: boolean = false;
  private retryCount: number = 0;
  private readonly MAX_RETRIES: number = 2;

  constructor(callback: RecognizerCallback) {
    this.callback = callback;
  }

  // 初始化识别器:设置语言、场景模式、回调
  async initialize(lang: string = 'zh-CN'): Promise<void> {
    try {
      // 构建识别配置
      const recognizerConfig: speechRecognizer.SpeechRecognizerConfig = {
        language: lang,                    // 识别语言
        period: 15000,                     // 流式识别最大持续时间(ms)
        mode: speechRecognizer.RecognizerMode.REALTIME, // 流式模式
        interimResults: true,              // 返回中间结果(实时字幕必需)
        punctuationMode: speechRecognizer.PunctuationMode.MANDATORY, // 强制标点
      };

      // 创建识别器实例
      this.recognizer = await speechRecognizer.createSpeechRecognizer(recognizerConfig);

      // 注册结果回调
      this.recognizer.on('recognizerResult', (res: speechRecognizer.RecognizerResult) => {
        // res.isFinal === true 时为最终结果,false 为中间过程
        if (res.result && res.result.text) {
          if (res.isFinal) {
            this.callback.onFinalResult?.(res.result.text);
            this.retryCount = 0; // 重置重试计数
          } else {
            this.callback.onPartialResult?.(res.result.text);
          }
        }
      });

      // 注册错误回调
      this.recognizer.on('recognizerError', (err: speechRecognizer.RecognizerError) => {
        console.error(`识别错误 [${err.errorCode}]: ${err.errorMessage}`);
        this.callback.onError?.(err.errorCode, err.errorMessage);

        // 自动重试逻辑
        if (this.retryCount < this.MAX_RETRIES) {
          this.retryCount++;
          console.info(`触发自动重试 (${this.retryCount}/${this.MAX_RETRIES})`);
          this.restart();
        } else {
          this.callback.onComplete?.();
        }
      });

      // 监听识别开始事件
      this.recognizer.on('recognizerStart', () => {
        console.info('识别器已启动');
        this.isListening = true;
        this.callback.onStart?.();
      });

      console.info('识别器初始化完成');
    } catch (err) {
      const error = err as BusinessError;
      console.error(`初始化失败: ${error.code} - ${error.message}`);
      throw err;
    }
  }

  // 启动识别
  async start(): Promise<void> {
    if (!this.recognizer) {
      throw new Error('识别器未初始化');
    }
    if (this.isListening) {
      console.warn('识别器已在运行中,跳过重复启动');
      return;
    }
    await this.recognizer.startListening();
  }

  // 主动停止识别
  async stop(): Promise<string> {
    if (!this.recognizer) {
      return '';
    }
    try {
      // stopListening 返回最终识别结果
      const result = await this.recognizer.stopListening();
      this.isListening = false;
      return result?.result?.text ?? '';
    } catch (err) {
      const error = err as BusinessError;
      console.error(`停止识别失败: ${error.code}`);
      this.isListening = false;
      return '';
    }
  }

  // 错误后重启识别
  private async restart(): Promise<void> {
    this.isListening = false;
    if (this.recognizer) {
      try {
        await this.recognizer.stopListening();
      } catch {}
      // 短暂延迟后重启,避免状态冲突
      await new Promise(resolve => setTimeout(resolve, 300));
      await this.start();
    }
  }

  // 销毁识别器,释放资源
  async destroy(): Promise<void> {
    if (this.recognizer) {
      try {
        if (this.isListening) {
          await this.recognizer.stopListening();
        }
        this.recognizer.off('recognizerResult');
        this.recognizer.off('recognizerError');
        this.recognizer.off('recognizerStart');
        this.recognizer = null;
        this.isListening = false;
      } catch (err) {
        console.error(`销毁识别器异常: ${(err as BusinessError).message}`);
      }
    }
    console.info('识别器已销毁');
  }
}

这段封装体现了几个实战要点:第一,interimResults: true 保证了实时字幕的流畅体验——没有中间结果,字幕会「一跳一跳」地更新,观感极差;第二,自动重试机制是生产级应用的标配,麦克风抢占或系统资源紧张时偶发的 1003 错误(引擎未就绪)可以被优雅处理;第三,生命周期管理(onWindowStageDestroy 中调用 destroy)是防止内存泄漏的关键。

3.3 在 ArkUI 页面中使用识别管理器

将识别管理器接入 UI 层需要一个 EntryAbility 级别的单例注入,以及页面的状态绑定:

// pages/SpeechToTextPage.ets
import { SpeechRecognitionManager } from '../manager/SpeechRecognitionManager';

@Entry
@Component
struct SpeechToTextPage {
  @State partialText: string = '正在聆听...';
  @State finalText: string = '';
  @State statusText: string = '点击麦克风开始识别';
  @State isRecording: boolean = false;

  private manager?: SpeechRecognitionManager;
  private controller: TextInputController = new TextInputController();

  aboutToAppear(): void {
    // 初始化识别管理器
    this.manager = new SpeechRecognitionManager({
      onStart: () => {
        this.isRecording = true;
        this.statusText = '🎙️ 正在聆听...';
      },
      onPartialResult: (text: string) => {
        this.partialText = text;
      },
      onFinalResult: (text: string) => {
        this.finalText = this.finalText.length > 0
          ? this.finalText + '\n' + text
          : text;
        this.partialText = '';
      },
      onError: (code, message) => {
        this.statusText = `❌ 错误 ${code}: ${message}`;
        this.isRecording = false;
      },
      onComplete: () => {
        this.isRecording = false;
        this.statusText = '识别已结束';
      }
    });

    // 异步初始化识别器
    this.manager.initialize().catch((e: Error) => {
      this.statusText = `初始化失败: ${e.message}`;
    });
  }

  async toggleRecording() {
    if (!this.manager) return;

    if (this.isRecording) {
      await this.manager.stop();
    } else {
      await this.manager.start();
    }
  }

  aboutToDisappear(): void {
    this.manager?.destroy();
  }

  build() {
    Column({ space: 16 }) {
      Text('实时语音识别')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 24 });

      // 状态指示
      Text(this.statusText)
        .fontSize(14)
        .fontColor(this.isRecording ? '#1890ff' : '#666');

      // 识别结果展示区
      Scroll() {
        Column({ space: 8 }) {
          if (this.finalText) {
            Text(this.finalText)
              .fontSize(16)
              .fontColor('#333')
              .width('100%');
          }
          if (this.partialText) {
            Text(this.partialText)
              .fontSize(18)
              .fontColor('#1890ff')
              .fontStyle(FontStyle.Italic)
              .width('100%');
          }
        }
        .padding(16)
        .alignItems(HorizontalAlign.Start);
      }
      .layoutWeight(1)
      .backgroundColor('#f5f5f5')
      .borderRadius(12)
      .margin({ top: 16, left: 16, right: 16 });

      // 麦克风控制按钮
      Row({ space: 32 }) {
        Button() {
          Text('🔴')
            .fontSize(32);
        }
        .width(72)
        .height(72)
        .shape(ShapeType.Circle)
        .backgroundColor(this.isRecording ? '#ff4d4f' : '#1890ff')
        .onClick(() => this.toggleRecording());

        Button('清空') {
          Text('清空')
            .fontSize(16);
        }
        .height(40)
        .onClick(() => {
          this.finalText = '';
          this.partialText = '';
        });
      }
      .margin({ bottom: 32 });
    }
    .width('100%')
    .height('100%');
  }
}

在这里插入图片描述

在 UI 层,partialText 使用斜体和蓝色区分中间过程,finalText 追加显示历史结果——这正是实时字幕的标准交互范式。值得注意的是,aboutToDisappear 中必须调用 destroy,否则在页面切换时识别器仍占用麦克风资源,导致其他模块无法正常录音。


四、语音合成:TextToSpeech 完整指南

4.1 TTS 的两种使用路径

HarmonyOS NEXT 的 TextToSpeech 提供了两套接口:单次合成(直接播放)和流式合成(获取音频数据自行处理)。前者适合语音播报场景,后者适合需要后处理(变声、混音、写入文件)的场景。

我们先看最常用的单次合成。

4.2 单次合成:语音播报的最简方案

// TtsManager.ets
import { textToSpeech } from '@kit.SpeechSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class TtsManager {
  private ttsEngine: textToSpeech.TextToSpeechEngine | null = null;
  private speakListener?: textToSpeech.SpeakProgressCallback;
  private speakListenerId: number = -1;

  // 初始化 TTS 引擎
  async initialize(): Promise<void> {
    const initParams: textToSpeech.InitializeParams = {
      language: 'zh-CN',           // 默认中文
      person: 0,                    // 音色:0=女声,1=男声(各语言包不同)
      speed: 1.0,                   // 语速:0.5~2.0
      pitch: 1.0,                  // 语调:0.5~2.0
      volume: 1.0,                  // 音量:0.0~1.0
    };

    const config: textToSpeech.CreateEngineParams = {
      params: initParams,
    };

    try {
      this.ttsEngine = await textToSpeech.createTextToSpeechEngine(config);

      // 注册播报进度回调(用于字幕同步)
      this.speakListener = {
        onStart (utteranceId: string) {
          console.info(`[TTS] 开始播报: ${utteranceId}`);
        },
        onProgress (utteranceId: string, progress: number) {
          // 进度回调,可用于高亮当前朗读到的字
          console.info(`[TTS] 进度 ${progress}%: ${utteranceId}`);
        },
        onFinish (utteranceId: string) {
          console.info(`[TTS] 播报完成: ${utteranceId}`);
        },
        onError (utteranceId: string, error: textToSpeech.SpeechError) {
          console.error(`[TTS] 错误 [${error.code}]: ${utteranceId}`);
        }
      };

      this.speakListenerId = this.ttsEngine.setListener(this.speakListener);
      console.info('TTS 引擎初始化完成');
    } catch (err) {
      const error = err as BusinessError;
      console.error(`TTS 初始化失败: ${error.code} - ${error.message}`);
      throw err;
    }
  }

  // 合成并播报文本
  async speak(text: string, options?: {
    speed?: number;
    pitch?: number;
    volume?: number;
  }): Promise<string> {
    if (!this.ttsEngine) {
      throw new Error('TTS 引擎未初始化');
    }

    const speakParams: textToSpeech.SpeakParams = {
      options: {
        speed: options?.speed ?? 1.0,
        pitch: options?.pitch ?? 1.0,
        volume: options?.volume ?? 1.0,
        voiceId: '', // 使用默认音色
      }
    };

    try {
      const utteranceId = await this.ttsEngine.speak(text, speakParams);
      console.info(`[TTS] 已提交播报任务: ${utteranceId}`);
      return utteranceId;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[TTS] 播报失败: ${error.code} - ${error.message}`);
      throw err;
    }
  }

  // 暂停当前播报
  async pause(): Promise<void> {
    try {
      await this.ttsEngine?.pause();
    } catch (err) {
      console.warn(`暂停失败: ${(err as BusinessError).message}`);
    }
  }

  // 恢复暂停的播报
  async resume(): Promise<void> {
    try {
      await this.ttsEngine?.resume();
    } catch (err) {
      console.warn(`恢复失败: ${(err as BusinessError).message}`);
    }
  }

  // 停止播报
  async stop(): Promise<void> {
    try {
      await this.ttsEngine?.stop();
    } catch (err) {
      console.warn(`停止失败: ${(err as BusinessError).message}`);
    }
  }

  // 销毁引擎
  async destroy(): Promise<void> {
    await this.stop();
    if (this.ttsEngine && this.speakListenerId >= 0) {
      this.ttsEngine.removeListener(this.speakListenerId);
    }
    this.ttsEngine = null;
  }
}

上述代码展示了 TTS 引擎的完整生命周期管理。setListener 返回的 ID 必须保存,后续 removeListener 时需要使用——这是容易遗漏的内存管理细节。

4.3 语音助手场景:识别 + 合成的双向闭环

当识别结果触发播报时,需要注意防止「语音反馈循环」——设备的合成声音被麦克风再次识别,形成回声。解决方案是:在 TTS 播报期间将识别器静音(stop 麦克风监听),播报结束后恢复。

// VoiceAssistant.ets
import { SpeechRecognitionManager } from './SpeechRecognitionManager';
import { TtsManager } from './TtsManager';

export interface AssistantResponse {
  replyText: string;
  action?: 'none' | 'navigate' | 'open_app';
}

export class VoiceAssistant {
  private recognizer: SpeechRecognitionManager;
  private tts: TtsManager;
  private isSpeaking: boolean = false;

  constructor() {
    this.tts = new TtsManager();
    this.recognizer = new SpeechRecognitionManager({
      onFinalResult: async (text: string) => {
        await this.processCommand(text);
      },
      onError: (_, msg) => {
        console.error(`识别异常: ${msg}`);
      }
    });
  }

  async start(): Promise<void> {
    await this.tts.initialize();
    await this.recognizer.initialize();
    await this.recognizer.start();
    await this.tts.speak('你好,我是语音助手,有什么可以帮你的?');
  }

  // 处理用户指令并生成回复
  private async processCommand(input: string): Promise<void> {
    // 停止识别,避免自己的声音被再次识别
    await this.recognizer.stop();
    this.isSpeaking = true;

    // 模拟 NLU 处理(实际项目替换为本地 NLP 引擎或远程接口)
    const response = await this.nluProcess(input);

    // 播报回复
    await this.tts.speak(response.replyText, { speed: 1.1 });

    // 播报结束后恢复识别
    this.tts.ttsEngine?.setListener({
      onFinish: async () => {
        this.isSpeaking = false;
        await this.recognizer.start();
      },
      onError: async () => {
        this.isSpeaking = false;
        await this.recognizer.start();
      }
    } as any);
  }

  // NLU 处理:意图识别 + 回复生成
  private async nluProcess(text: string): Promise<AssistantResponse> {
    // 关键词匹配式简单 NLU
    const lower = text.toLowerCase();

    if (lower.includes('天气')) {
      return { replyText: '今天天气晴朗,气温26度,适合出行。', action: 'none' };
    }
    if (lower.includes('时间') || lower.includes('几点')) {
      const now = new Date();
      return {
        replyText: `现在是 ${now.getHours()}${now.getMinutes()} 分。`,
        action: 'none'
      };
    }
    if (lower.includes('打开')) {
      const appName = text.replace(/.*打开/, '').trim();
      return {
        replyText: `好的,正在打开 ${appName}`,
        action: 'open_app'
      };
    }

    return {
      replyText: '抱歉,我没有理解你的意思,请再说一次?',
      action: 'none'
    };
  }

  async destroy(): Promise<void> {
    await this.recognizer.destroy();
    await this.tts.destroy();
  }
}

回声消除是语音助手实现中的高频痛点。上述方案通过 recognizer.stop()recognizer.start() 手动控制识别器状态,是端侧方案中最轻量的解法。若对实时性要求更高,可以结合系统 VAD 信号(speechRecognizer 提供了 VAD 回调)来自动触发这个逻辑。


五、性能优化:延迟、功耗与准确率的三角平衡

5.1 延迟的三个关键节点

端侧语音系统的总延迟由三部分构成:

前端处理延迟(Frontend Latency):麦克风采集音频到送入识别引擎的时间。HarmonyOS NEXT 的音频子系统默认使用 16kHz 采样,帧长 40ms,帧移 20ms——这意味着用户说话后,最快 60ms 才能拿到第一个识别结果。要缩短这个值,可以将帧长从 40ms 调整为 30ms(在部分设备上可配),但会略微增加 CPU 占用。

识别引擎延迟(Engine Latency):模型推理消耗的时间。端侧模型的优势在于无网络往返,但在低端设备上,ASR 模型推理仍可能占用 15-30ms/帧。建议在设备的 deviceType2in1tablet 时启用高精度模式,为 phone 设备选择均衡模式。

后处理延迟(Post-processing Latency):标点恢复、数字规范化(“2024年” vs “二〇二四年”)等后处理步骤。HarmonyOS 的后处理通常在 5ms 以内,不是主要瓶颈。

总延迟控制在 150ms 以内 即为优秀水平,用户感知接近「说到即显示」。

5.2 功耗优化策略

麦克风持续收音是功耗大户。在「实时字幕」场景下,可以采用变速采样策略:识别器空闲(无语音活动)时,将采样率从 16kHz 降至 8kHz;检测到 VAD 信号后再切回 16kHz 全速采集。这能有效降低待机功耗。

// AdaptiveAudioSampler.ets
import { audio } from '@kit.AudioKit';

export class AdaptiveAudioSampler {
  private currentSampleRate: number = 16000;
  private vadThreshold: number = 0.02; // RMS 能量阈值
  private isLowPowerMode: boolean = true;
  private audioCapturer?: audio.AudioCapturer;
  private energyHistory: number[] = [];

  async initialize() {
    const audioStreamInfo: audio.AudioStreamInfo = {
      samplingRate: this.currentSampleRate,
      channels: audio.AudioChannel.CHANNEL_1,
      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
    };

    const audioCapturerOptions: audio.AudioCapturerOptions = {
      streamInfo: audioStreamInfo,
      capturerInfo: {
        source: audio.SourceType.SOURCE_TYPE_MIC,
        capturerFlags: 0,
      }
    };

    this.audioCapturer = await audio.createAudioCapturer(audioCapturerOptions);
  }

  // 根据音频能量动态调整采样率
  adjustSampleRate(rmsEnergy: number): number {
    this.energyHistory.push(rmsEnergy);
    if (this.energyHistory.length > 10) {
      this.energyHistory.shift();
    }

    const avgEnergy = this.energyHistory.reduce((a, b) => a + b, 0) / this.energyHistory.length;
    const isSpeechActive = avgEnergy > this.vadThreshold;

    if (isSpeechActive && this.isLowPowerMode) {
      // 切换到高质量模式
      this.switchToHighQuality();
      this.isLowPowerMode = false;
    } else if (!isSpeechActive && !this.isLowPowerMode) {
      // 持续无语音时切换到低功耗模式
      this.switchToLowPower();
      this.isLowPowerMode = true;
    }

    return this.currentSampleRate;
  }

  private async switchToHighQuality() {
    if (this.audioCapturer) {
      await this.audioCapturer.setParams({
        streamInfo: {
          samplingRate: 16000,
          channels: audio.AudioChannel.CHANNEL_1,
          sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
          encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
        }
      } as audio.AudioCapturerParameters);
      this.currentSampleRate = 16000;
      console.info('切换至高质量模式 (16kHz)');
    }
  }

  private async switchToLowPower() {
    if (this.audioCapturer) {
      await this.audioCapturer.setParams({
        streamInfo: {
          samplingRate: 8000,
          channels: audio.AudioChannel.CHANNEL_1,
          sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
          encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
        }
      } as audio.AudioCapturerParameters);
      this.currentSampleRate = 8000;
      console.info('切换至低功耗模式 (8kHz)');
    }
  }
}

5.3 准确率调优:从配置到场景适配

语音识别的准确率高度依赖场景配置。HarmonyOS 识别引擎支持通过 setSmartConfig 注入领域知识:

// 注入领域词汇,提升专业术语识别率
async function optimizeForDomain(manager: SpeechRecognitionManager) {
  // 场景一:会议记录场景,注入时间、人物、地点词汇
  const meetingConfig = {
    boostWords: ['会议', '议题', '表决', '纪要', '主持人', '发言人', '决议'],
    boostScore: 2.0,          // 提升权重
    ignoreOov: false,         // 是否忽略未登录词
    customization: 'meeting'  // 场景模板
  };

  // 场景二:导航场景
  const navConfig = {
    boostWords: ['路口', '左转', '右转', '直行', '掉头', '高速', '省道'],
    boostScore: 2.5,
    ignoreOov: false,
    customization: 'navigation'
  };

  // 实际使用时根据应用场景选择配置
  // manager.recognizer.setSmartConfig(meetingConfig);
  console.info('领域词汇配置完成');
}

针对中文方言,HarmonyOS NEXT 支持 zh-CN(普通话)、zh-CY(粤语)、zh-TW(台湾话)等语言标记。对于港澳用户较多的应用,切换到 zh-CY 可以显著提升识别率——用户无需刻意说普通话,自然语言交互的门槛大幅降低。


六、实战场景一:实时字幕系统

实时字幕是流式识别的典型应用。其核心挑战不在于识别本身,而在于文本的实时渲染历史滚动的平滑体验

6.1 完整页面实现

// pages/LiveCaptionPage.ets
import { SpeechRecognitionManager } from '../manager/SpeechRecognitionManager';

interface CaptionItem {
  id: number;
  startTime: number;
  endTime: number;
  text: string;
  isFinal: boolean;
}

@Entry
@Component
struct LiveCaptionPage {
  @State captions: CaptionItem[] = [];
  @State currentCaption: string = '';
  @State isActive: boolean = false;
  @State sessionId: string = '';

  private manager?: SpeechRecognitionManager;
  private captionIdCounter: number = 0;
  private scrollController: Scroller = new Scroller();

  aboutToAppear(): void {
    this.manager = new SpeechRecognitionManager({
      onStart: () => {
        this.isActive = true;
        this.sessionId = Date.now().toString();
        console.info(`字幕会话开始: ${this.sessionId}`);
      },
      onPartialResult: (text: string) => {
        // 中间结果:更新当前行的显示(实时刷新)
        animateTo({ duration: 100 }, () => {
          this.currentCaption = text;
        });
      },
      onFinalResult: (text: string) => {
        if (text.trim().length === 0) return;

        // 最终结果:固化到历史列表
        const newItem: CaptionItem = {
          id: this.captionIdCounter++,
          startTime: Date.now(),
          endTime: Date.now() + 1000,
          text: text,
          isFinal: true
        };

        animateTo({ duration: 300 }, () => {
          this.captions.push(newItem);
          this.currentCaption = '';
        });

        // 滚动到底部
        setTimeout(() => {
          this.scrollController.scrollEdge(Edge.Bottom);
        }, 100);
      },
      onError: (code, msg) => {
        console.error(`字幕识别错误: [${code}] ${msg}`);
        if (this.isActive) {
          this.currentCaption = `⚠️ 识别暂停: ${msg}`;
        }
      },
      onComplete: () => {
        this.isActive = false;
      }
    });

    this.manager.initialize().catch(e => {
      console.error(`初始化失败: ${e.message}`);
    });
  }

  async toggle() {
    if (!this.manager) return;

    if (this.isActive) {
      await this.manager.stop();
    } else {
      this.captions = [];
      this.currentCaption = '';
      await this.manager.start();
    }
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      // 背景
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('#0a0a0a');

      // 字幕区域
      Column({ space: 12 }) {
        // 历史字幕
        Scroll(this.scrollController) {
          Column({ space: 10 }) {
            ForEach(this.captions, (item: CaptionItem) => {
              Row() {
                Text(`[${this.formatTime(item.startTime)}]`)
                  .fontSize(12)
                  .fontColor('#888')
                  .width(60);
                Text(item.text)
                  .fontSize(18)
                  .fontColor('#fff')
                  .layoutWeight(1);
              }
              .width('100%')
              .padding({ left: 16, right: 16 });
            });
          }
          .padding({ top: 24, bottom: 60 });
        }
        .width('100%')
        .layoutWeight(1);

        // 当前识别行(高亮)
        if (this.currentCaption) {
          Row() {
            Text('▶')
              .fontSize(12)
              .fontColor('#1890ff')
              .width(20);
            Text(this.currentCaption)
              .fontSize(20)
              .fontColor('#1890ff')
              .fontStyle(FontStyle.Italic)
              .layoutWeight(1);
          }
          .width('100%')
          .padding({ left: 16, right: 16, bottom: 16 })
          .transition(TransitionEffect.OPACITY.animation({ duration: 200 }));
        }
      }
      .width('100%')
      .height('100%');

      // 底部控制栏
      Row() {
        Text(this.isActive ? '🔴 录制中' : '⏸️ 已暂停')
          .fontSize(14)
          .fontColor(this.isActive ? '#ff4d4f' : '#888');

        Blank();

        Text(`${this.captions.length} 条字幕`)
          .fontSize(12)
          .fontColor('#666');

        Button() {
          Text('切换')
            .fontSize(14)
            .fontColor('#fff');
        }
        .height(32)
        .width(72)
        .backgroundColor('#1890ff')
        .borderRadius(16)
        .onClick(() => this.toggle());
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 12, bottom: 12 })
      .backgroundColor('#1a1a1a');
    }
    .width('100%')
    .height('100%');
  }

  private formatTime(timestamp: number): string {
    const d = new Date(timestamp);
    return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}:${d.getSeconds().toString().padStart(2, '0')}`;
  }

  aboutToDisappear(): void {
    this.manager?.destroy();
  }
}

实时字幕页面的 UI 设计有几个要点:深色背景配合白色文字确保可读性;当前识别行使用蓝色斜体,与最终结果(白色正体)形成视觉区分;时间戳辅助用户定位内容片段;历史字幕采用追加而非覆盖,保证阅读连贯性。


七、实战场景二:语音控制智能家居网关

在智能家居场景中,语音识别的核心价值是低时延指令执行。用户说「打开客厅灯」,系统需要在 300ms 内完成识别 + 意图解析 + 设备下发全流程。以下是结合 Intent Framework(HarmonyOS 的跨 Ability 通信机制)的完整实现。

7.1 意图注册:声明语音能力

在智能家居应用的 module.json5 中,需要声明对语音能力的依赖:

{
  "abilities": [
    {
      "name": "HomeControlAbility",
      "skills": [
        {
          "entities": ["entity.system.home", "entity.system.device"],
          "actions": ["action.control.light", "action.control.ac", "action.control.tv"]
        }
      ]
    }
  ]
}

这样,其他应用(如语音助手)可以通过 Intent 查询并唤起 HomeControlAbility,实现跨应用的设备控制。

7.2 语音指令处理器

// HomeIntentHandler.ets
import { wantConstant, Want } from '@kit.AbilityKit';
import { deviceManager } from '@kit.DeviceManagementKit';

// 设备控制映射表
const DEVICE_MAP: Record<string, string> = {
  '灯': 'light',
  '空调': 'ac',
  '电视': 'tv',
  '窗帘': 'curtain',
  '扫地机': 'robot'
};

const ACTION_MAP: Record<string, string> = {
  '打开': 'ON',
  '关闭': 'OFF',
  '调亮': 'BRIGHTEN',
  '调暗': 'DIM',
  '调高': 'INCREASE',
  '调低': 'DECREASE'
};

export interface HomeCommand {
  deviceType: string;
  deviceId: string;
  action: string;
  value?: number;
}

export class HomeIntentHandler {
  // 解析语音命令文本
  parseCommand(text: string): HomeCommand | null {
    for (const [keyword, deviceType] of Object.entries(DEVICE_MAP)) {
      if (text.includes(keyword)) {
        let action = 'ON'; // 默认动作
        let value: number | undefined;

        for (const [actionKeyword, actionValue] of Object.entries(ACTION_MAP)) {
          if (text.includes(actionKeyword)) {
            action = actionValue;
            break;
          }
        }

        // 提取数值(如"调高到26度")
        const numMatch = text.match(/(\d+)/);
        if (numMatch) {
          value = parseInt(numMatch[1]);
        }

        return { deviceType, deviceId: '', action, value };
      }
    }
    return null;
  }

  // 跨 Ability 发送控制指令
  async sendDeviceCommand(command: HomeCommand): Promise<boolean> {
    const want: Want = {
      bundleName: 'com.example.smarthome',
      abilityName: 'HomeControlAbility',
      parameters: {
        deviceType: command.deviceType,
        action: command.action,
        value: command.value ?? 0
      }
    };

    try {
      const context = AppStorage.get<Context>('abilityContext');
      if (context) {
        await context.startAbility(want);
        console.info(`设备指令已下发: ${command.deviceType} -> ${command.action}`);
        return true;
      }
      return false;
    } catch (err) {
      console.error(`指令下发失败: ${(err as Error).message}`);
      return false;
    }
  }

  // 生成语音反馈文本
  generateFeedback(command: HomeCommand, success: boolean): string {
    if (!success) {
      return '抱歉,设备响应失败,请检查设备连接。';
    }

    const deviceNames: Record<string, string> = {
      'light': '灯',
      'ac': '空调',
      'tv': '电视',
      'curtain': '窗帘',
      'robot': '扫地机'
    };

    const deviceName = deviceNames[command.deviceType] || command.deviceType;
    const actionNames: Record<string, string> = {
      'ON': '已打开',
      'OFF': '已关闭',
      'BRIGHTEN': '已调亮',
      'DIM': '已调暗',
      'INCREASE': '已调高',
      'DECREASE': '已调低'
    };

    const actionName = actionNames[command.action] || command.action;
    return `${deviceName}${actionName}`;
  }
}

7.3 整合语音识别与设备控制

// SmartHomeVoiceControl.ets
import { SpeechRecognitionManager } from '../manager/SpeechRecognitionManager';
import { TtsManager } from '../manager/TtsManager';
import { HomeIntentHandler, HomeCommand } from './HomeIntentHandler';

export class SmartHomeVoiceControl {
  private recognizer: SpeechRecognitionManager;
  private tts: TtsManager;
  private intentHandler: HomeIntentHandler;
  private isProcessingCommand: boolean = false;

  constructor() {
    this.intentHandler = new HomeIntentHandler();
    this.tts = new TtsManager();
    this.recognizer = new SpeechRecognitionManager({
      onFinalResult: async (text: string) => {
        await this.handleVoiceCommand(text);
      }
    });
  }

  async initialize(): Promise<void> {
    await this.tts.initialize();
    await this.recognizer.initialize();
  }

  async startListening(): Promise<void> {
    await this.recognizer.start();
    console.info('智能家居语音控制已启动');
  }

  private async handleVoiceCommand(text: string): Promise<void> {
    if (this.isProcessingCommand) {
      console.info('上一条命令处理中,忽略本次输入');
      return;
    }

    this.isProcessingCommand = true;

    // 1. 停止识别(避免回声)
    await this.recognizer.stop();

    // 2. 解析命令
    const command = this.intentHandler.parseCommand(text);
    console.info(`识别命令: "${text}" -> ${JSON.stringify(command)}`);

    // 3. 执行设备控制
    let success = false;
    if (command) {
      success = await this.intentHandler.sendDeviceCommand(command);
    } else {
      await this.tts.speak('抱歉,我没有理解这个指令。');
      this.isProcessingCommand = false;
      await this.recognizer.start();
      return;
    }

    // 4. 生成语音反馈
    const feedback = this.intentHandler.generateFeedback(command!, success);
    await this.tts.speak(feedback, { speed: 1.2 });

    // 5. 等待播报结束后恢复识别
    setTimeout(async () => {
      this.isProcessingCommand = false;
      await this.recognizer.start();
    }, 500);
  }

  async destroy(): Promise<void> {
    await this.recognizer.destroy();
    await this.tts.destroy();
  }
}

至此,一个完整的语音控制闭环已经形成:麦克风 → 语音识别 → NLU 解析 → 设备指令下发 → 语音反馈。这个链路中,端侧识别的低延迟保证了「说出即执行」的体验,而 Intent Framework 则打通了跨应用的能力调用——即使控制逻辑不在同一个 Ability 中,也能通过 Want 参数无缝流转。


八、调试与排障:常见问题清单

在端侧语音系统的开发过程中,有几类高频问题的根源相对集中,整理如下供快速定位:

症状 可能原因 解决方案
识别器 createSpeechRecognizer 报错 1002 设备不支持当前语言或模式 确认 language 参数合法,检查设备是否安装了对应语言包
流式识别无中间结果 interimResults 未设为 true 显式设置 interimResults: true
TTS 播报无声 volume 被设为 0 或 speak 返回后立即销毁引擎 确认 volume 在 0.0~1.0 之间,且在播报完成回调后再销毁
识别器启动后持续报 1003 麦克风被其他应用占用 检查 audio.AudioCapturer 是否冲突,尝试 stopListening 后重新启动
方言识别率低 语言参数设置为 zh-CN(普通话) 切换为 zh-CY(粤语)或 zh-TW(台湾话)
页面退出后识别器仍占用麦克风 aboutToDisappear 未调用 destroy() 确保在页面生命周期回调中清理资源
语音助手回声问题 TTS 播报时未停止识别器 speak() 开始前调用 recognizer.stop(),结束后 start()

九、总结与展望

本文围绕 HarmonyOS NEXT / API 12+ 的端侧语音能力,从权限体系、识别引擎、合成引擎、两个实战场景(实时字幕与语音家居控制)以及性能优化五个维度,完整拆解了一条可投产的语音交互链路。

核心要点可以归纳为三条经验:第一,生命周期管理是根基——识别器和 TTS 引擎的创建、启动、停止、销毁每一步都必须严格配对,资源泄漏在长时间运行的应用中会以「麦克风失灵」的形式集中爆发;第二,中间结果(interimResults)是体验的灵魂——关闭中间结果,实时字幕从「流」退化成了「弹幕」,用户感知会断崖式下降;第三,回声消除是语音助手的生死线——不处理 TTS 声音回录,再好的 NLU 也救不了产品体验。

展望未来,随着 NPU 在移动设备上的普及,端侧语音模型的参数量和精度还将持续提升。HarmonyOS NEXT 的智能能力层(Intelligent Capability Layer)正在逐步开放更底层的模型加载接口,开发者将能够接入自定义的 ASR / TTS 模型,甚至在端侧运行完整的本地对话系统。端侧语音不是过渡方案,而是下一代交互基础设施的重要组成部分。


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐