《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第5篇:实战项目——构建全功能语音助手

当你在 HarmonyOS NEXT 上尝试把语音唤醒、识别和合成串起来时,会发现最大的坑根本不是单个 API 怎么调,而是这三个模块之间的状态怎么同步。

前几天有个读者在群里问:"我用 speechRecognizer 识别成功,然后用 textToSpeech 合成回复,但有时候唤醒模块还没停止,两个语音流就打架了。有什么好的处理方式吗?"

这个问题实际上非常典型。Core Speech Kit 的每个能力单独看都不复杂,但把它们组合成一个语音助手时,状态管理、生命周期和线程同步才是真正的难点。

这个项目解决什么问题

一个完整的语音助手必须处理三个核心动作:唤醒 → 识别 → 合成反馈。三个环节环环相扣:

  • 唤醒成功后,立即开启识别
  • 识别到结果后,调用大模型或规则匹配,生成回复
  • 合成回复文本并播放

很多教程只告诉你每个 API 怎么调,但实际项目中真正麻烦的是:

  1. 状态混乱------唤醒回调还没执行完,识别已经开始了
  2. 资源冲突------多个语音服务同时操作音频设备
  3. 回调线程------在 UI 线程直接修改状态导致崩溃

这个项目使用TaskPool处理耗时回调,使用状态机模式管理三个阶段切换,确保每个环节清晰可控。

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机

架构设计:状态机驱动

直接让代码分布在各个回调里,后期维护会很难受。我采用一个简单的状态机来管理三个阶段的流转:

typescript 复制代码
// 定义语音助手状态
export enum AssistantState {
  IDLE,         // 空闲
  WAKING_UP,    // 唤醒中
  LISTENING,    // 识别中
  THINKING,     // "思考"(处理结果)
  SPEAKING,     // 合成反馈中
  WAITING_INPUT // 等待二次输入
}

状态转换规则:

当前状态 触发事件 下一状态
IDLE 用户说唤醒词 WAKING_UP
WAKING_UP 唤醒成功 LISTENING
WAKING_UP 唤醒失败超时 IDLE
LISTENING 识别到语音 THINKING
LISTENING 超时无语音 IDLE
THINKING 生成回复文本 SPEAKING
SPEAKING 合成播放完成 IDLE

这样的设计让你在写业务逻辑时只需关心onStateChange回调,不需要在各个 API 的回调里处理复杂的 if-else。

核心实现

1. 状态管理器

typescript 复制代码
import { AssistantState } from './AssistantState';

export class StateManager {
  private currentState: AssistantState = AssistantState.IDLE;
  private stateListeners: Array<(state: AssistantState) => void> = [];

  getState(): AssistantState {
    return this.currentState;
  }

  setState(newState: AssistantState): boolean {
    // 校验状态转换是否合法
    if (!this.isValidTransition(this.currentState, newState)) {
      console.warn(`状态转换无效: ${this.currentState} -> ${newState}`);
      return false;
    }
    this.currentState = newState;
    this.notifyListeners();
    return true;
  }

  private isValidTransition(from: AssistantState, to: AssistantState): boolean {
    switch (from) {
      case AssistantState.IDLE:
        return to === AssistantState.WAKING_UP;
      case AssistantState.WAKING_UP:
        return to === AssistantState.LISTENING || to === AssistantState.IDLE;
      case AssistantState.LISTENING:
        return to === AssistantState.THINKING || to === AssistantState.IDLE;
      case AssistantState.THINKING:
        return to === AssistantState.SPEAKING || to === AssistantState.IDLE;
      case AssistantState.SPEAKING:
        return to === AssistantState.IDLE || to === AssistantState.WAITING_INPUT;
      case AssistantState.WAITING_INPUT:
        return to === AssistantState.LISTENING || to === AssistantState.IDLE;
      default:
        return false;
    }
  }

  // 监听状态变化,用于 UI 展示
  onStateChange(listener: (state: AssistantState) => void): void {
    this.stateListeners.push(listener);
  }

  private notifyListeners(): void {
    for (const listener of this.stateListeners) {
      listener(this.currentState);
    }
  }
}

为什么这样写? 状态机转换校验可以避免非预期调用,比如在SPEAKING状态下试图直接开启识别。这是在团队协作中比较有效的一种防御性编程。

2. 唤醒模块(监听模式)

typescript 复制代码
import { speechTrigger } from '@kit.CoreSpeechKit';
import { TaskPool } from '@ohos.taskpool';
import { StateManager } from './StateManager';
import { AssistantState } from './AssistantState';

export class WakeUpManager {
  private triggerEngine: speechTrigger.TriggerEngine;
  private stateManager: StateManager;

  constructor(stateManager: StateManager) {
    this.stateManager = stateManager;
  }

  // 初始化唤醒引擎
  initWakeUp(callback: () => void): void {
    // 使用TaskPool处理初始化,避免卡UI线程
    let task = new TaskPool.Task(
      () => {
        let engine = speechTrigger.createTriggerEngine();
        engine.init({
          keywords: ["小艺小艺", "你好助手"],
          threshold: 0.6,
          callback: (event) => {
            if (event.type === 'wakeup' && event.confidence > 0.6) {
              // 唤醒成功,切换到监听状态
              this.stateManager.setState(AssistantState.LISTENING);
              callback();
            }
          }
        });
        return engine;
      }
    );
    TaskPool.execute(task).then((engine: speechTrigger.TriggerEngine) => {
      this.triggerEngine = engine;
    });
  }

  stopWakeUp(): void {
    if (this.triggerEngine) {
      this.triggerEngine.stop();
    }
  }
}

这里需要注意:TaskPool的任务是隔离的,不能直接访问外部的this.stateManager。所以上面的代码需要调整。正确的做法是在TaskPool中只处理callback,不引用外部对象。

typescript 复制代码
initWakeUp(callback: () => void): void {
  // TaskPool任务只传递调用方传进来的callback
  let task = new TaskPool.Task(
    (keywords: string[], threshold: number, cb: (event: any) => void) => {
      let engine = speechTrigger.createTriggerEngine();
      engine.init({
        keywords: keywords,
        threshold: threshold,
        callback: cb
      });
      return engine;
    }
  );

  let callbackWrapper = (event) => {
    if (event.type === 'wakeup' && event.confidence > 0.6) {
      // 通知外部状态变化
      callback();
    }
  };

  TaskPool.execute(task, ["小艺小艺", "你好助手"], 0.6, callbackWrapper)
    .then((engine: speechTrigger.TriggerEngine) => {
      this.triggerEngine = engine;
    });
}

3. 识别模块

typescript 复制代码
import { speechRecognizer } from '@kit.CoreSpeechKit';
import { taskpool } from '@ohos.taskpool';

export class RecognizerManager {
  private recognizerEngine: speechRecognizer.RecognizerEngine;

  startListening(resultCallback: (text: string) => void, errorCallback: (error: string) => void): void {
    let task = new taskpool.Task(
      (onResult: (text: string) => void, onError: (error: string) => void) => {
        let engine = speechRecognizer.createRecognizerEngine();
        engine.start({
          language: 'zh-CN',
          maxDuration: 10000,
          callback: {
            onResult: (text: string) => {
              onResult(text);
            },
            onError: (code: number, message: string) => {
              onError(`识别错误: ${code} - ${message}`);
            }
          }
        });
        return engine;
      }
    );

    taskpool.execute(task, resultCallback, errorCallback)
      .then((engine: speechRecognizer.RecognizerEngine) => {
        this.recognizerEngine = engine;
      });
  }

  stopListening(): void {
    if (this.recognizerEngine) {
      this.recognizerEngine.stop();
    }
  }
}

4. 合成模块

typescript 复制代码
import { textToSpeech } from '@kit.CoreSpeechKit';
import { taskpool } from '@ohos.taskpool';

export class TTSManager {
  private ttsEngine: textToSpeech.TextToSpeechEngine;

  speak(text: string, callback: () => void): void {
    let task = new taskpool.Task(
      (speechText: string, onFinish: () => void) => {
        let engine = textToSpeech.createTextToSpeechEngine();
        engine.speak({
          text: speechText,
          voiceName: 'zh-CN-XiaoxiaoNeural',
          speed: 1.0,
          callback: {
            onStart: () => {
              console.log('合成开始');
            },
            onComplete: () => {
              onFinish();
            },
            onError: (code: number, message: string) => {
              console.error(`合成失败: ${code}`);
              onFinish();
            }
          }
        });
        return engine;
      }
    );

    taskpool.execute(task, text, callback)
      .then((engine: textToSpeech.TextToSpeechEngine) => {
        this.ttsEngine = engine;
      });
  }

  stopSpeaking(): void {
    if (this.ttsEngine) {
      this.ttsEngine.stop();
    }
  }
}

5. 核心控制器:串联完整流程

typescript 复制代码
import { StateManager } from './StateManager';
import { AssistantState } from './AssistantState';
import { WakeUpManager } from './WakeUpManager';
import { RecognizerManager } from './RecognizerManager';
import { TTSManager } from './TTSManager';

export class VoiceAssistantController {
  private stateManager: StateManager;
  private wakeUpManager: WakeUpManager;
  private recognizerManager: RecognizerManager;
  private ttsManager: TTSManager;

  constructor() {
    this.stateManager = new StateManager();
    this.wakeUpManager = new WakeUpManager(this.stateManager);
    this.recognizerManager = new RecognizerManager();
    this.ttsManager = new TTSManager();

    // 监听状态变化,用于UI展示
    this.stateManager.onStateChange((state: AssistantState) => {
      console.log(`当前状态: ${state}`);
      // 更新UI状态,比如显示"正在听"的动画
    });
  }

  start(): void {
    if (!this.stateManager.setState(AssistantState.WAKING_UP)) {
      console.error('启动失败,当前状态不允许');
      return;
    }

    // 开始唤醒监听
    this.wakeUpManager.initWakeUp(() => {
      // 唤醒成功回调(在TaskPool线程中)
      // 切换状态后,自动开启识别
      this.stateManager.setState(AssistantState.LISTENING);
      this.startRecognition();
    });

    // 设置超时,如果10秒没唤醒,回到空闲
    setTimeout(() => {
      if (this.stateManager.getState() === AssistantState.WAKING_UP) {
        console.log('唤醒超时');
        this.stateManager.setState(AssistantState.IDLE);
        this.wakeUpManager.stopWakeUp();
      }
    }, 10000);
  }

  private startRecognition(): void {
    this.recognizerManager.startListening(
      (text: string) => {
        // 识别到结果,进入思考阶段
        this.stateManager.setState(AssistantState.THINKING);
        // 处理识别结果(这里简化,直接返回回复)
        let reply = this.generateReply(text);
        // 进入合成阶段
        this.stateManager.setState(AssistantState.SPEAKING);
        this.ttsManager.speak(reply, () => {
          // 合成播放完成,回到空闲
          this.stateManager.setState(AssistantState.IDLE);
        });
      },
      (error: string) => {
        console.error(`识别错误: ${error}`);
        // 识别失败,回到空闲
        this.stateManager.setState(AssistantState.IDLE);
      }
    );

    // 识别超时处理(10秒无语音)
    setTimeout(() => {
      if (this.stateManager.getState() === AssistantState.LISTENING) {
        console.log('识别超时,回到空闲');
        this.stateManager.setState(AssistantState.IDLE);
        this.recognizerManager.stopListening();
      }
    }, 10000);
  }

  private generateReply(text: string): string {
    // 实际项目中这里可以集成大模型
    // 这里简单返回一个固定回复
    return `你好,你刚刚说的是:${text}`;
  }

  stop(): void {
    this.wakeUpManager.stopWakeUp();
    this.recognizerManager.stopListening();
    this.ttsManager.stopSpeaking();
    this.stateManager.setState(AssistantState.IDLE);
  }

  getStateManager(): StateManager {
    return this.stateManager;
  }
}

6. UI 入口组件

typescript 复制代码
@Entry
@Component
struct VoiceAssistantPage {
  @State currentState: string = '空闲';
  private controller: VoiceAssistantController = new VoiceAssistantController();

  aboutToAppear(): void {
    // 注册状态监听
    this.controller.getStateManager().onStateChange((state) => {
      this.currentState = this.getStateText(state);
    });
  }

  getStateText(state: AssistantState): string {
    switch (state) {
      case AssistantState.IDLE: return '空闲';
      case AssistantState.WAKING_UP: return '唤醒中...';
      case AssistantState.LISTENING: return '正在听...';
      case AssistantState.THINKING: return '思考中...';
      case AssistantState.SPEAKING: return '说话中...';
      case AssistantState.WAITING_INPUT: return '等待输入';
      default: return '未知';
    }
  }

  build() {
    Column() {
      Text(`当前状态: ${this.currentState}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin(20)

      Button('启动语音助手')
        .onClick(() => {
          this.controller.start();
        })
        .margin(10)

      Button('停止')
        .onClick(() => {
          this.controller.stop();
        })
        .margin(10)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

常见问题 1:TaskPool 回调里无法访问外部对象

现象 :在 WakeUpManager.initWakeUp 中使用 this.stateManager.setState 时,编译器报错。

原因TaskPool的任务是独立运行的,不能访问外部类的实例属性。这是一个 HarmonyOS 线程模型的限制。

解决方案 :把需要传递的状态变更操作封装成回调函数,通过任务参数传入。上面 initWakeUp 中的修正方式就是正确的做法。

常见问题 2:唤醒成功后瞬间开启识别导致设备冲突

现象:唤醒成功后,识别模块立刻启动,结果语音设备被占用,合成播放出来的声音断断续续。

原因:唤醒引擎和识别引擎会同时占用麦克风。正常流程应该是唤醒后先关闭唤醒引擎,再启动识别引擎。

解决方案 :在 WakeUpManager 的回调中,先调用 stopWakeUp(),再触发状态变更。

typescript 复制代码
this.wakeUpManager.initWakeUp(() => {
  // 先停止唤醒
  this.wakeUpManager.stopWakeUp();
  // 再切换状态
  this.stateManager.setState(AssistantState.LISTENING);
  this.startRecognition();
});

最佳实践

  1. 不要在 StateManager.setState 中直接修改 UI 。状态变化通知通过 Listener 传播到 UI 组件,由 ArkUI 框架统一更新。如果在回调里直接调用 this.stateManager.setState 并且触发 UI 更新,容易出现状态竞争。

  2. TaskPool 任务参数必须是可序列化的 。回调函数需要使用 () => void 类型,不能传递对象引用。这是 HarmonyOS 线程模型的一个重要约束。

  3. 合成播放完成回调必须切换状态TTSManagerspeak 回调中,如果发生错误也要调用回调,否则助手会卡在 SPEAKING状态。上面代码中已经在错误回调中调用了 onFinish

FAQ

Q:为什么真机运行正常,模拟器上识别经常超时?

A:模拟器的麦克风驱动和真机不一样,特别是语音触发类功能,对硬件要求较高。建议在真机上测试所有语音功能,模拟器只用于 UI 调试。

Q:唤醒词识别率低,有什么优化方法?

A:检查 threshold 参数,默认 0.6 在安静环境够用,但在嘈杂环境中建议提高到 0.7 以上。另外,唤醒词录制质量很重要,建议在安静环境录制,避免有背景音。

Q:语音合成播放时,点击停止后还是继续播放完当前句子?

A:textToSpeechstop() 是异步的,不一定能立即中断音频流。如果需要强制停止,可以在 stopSpeaking 后立即释放引擎资源。目前版本建议让当前句子播完,下一句再控制是否播放。


如果你也在做语音助手类应用,建议先把状态机跑通,再优化识别精度和合成音质。这比一开始就想着加各种功能要稳定得多。

相关推荐
最爱老式锅包肉1 小时前
《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第4篇:语音唤醒进阶——打造免提交互体验
交互·语音识别·harmonyos
我是小a2 小时前
鸿蒙 ArkUI 数据可视化图例对照表:组件化设计与实现
华为·信息可视化·harmonyos·鸿蒙·鸿蒙系统
TrisighT3 小时前
Electron 鸿蒙 PC 上点关闭就真退出了?我花了两天才把“最小化到托盘“做稳
electron·harmonyos
●VON3 小时前
HarmonyKit | 鸿蒙新特性:HdsTabs 沉浸光感底部导航完全指南
华为·harmonyos·鸿蒙·新特性
在书中成长4 小时前
HarmonyOS 小游戏《对战五子棋》开发第2篇-AppScope与entry模块
华为·harmonyos
●VON4 小时前
HarmonyKit | 鸿蒙新特性对比:Tabs vs HdsTabs 选型深度解析
华为·harmonyos·鸿蒙·新特性
binbin_524 小时前
React Native 鸿蒙环境搭建完整实战:从 RN 工程到 RNOH 运行
深度学习·react native·react.js·harmonyos
最爱老式锅包肉5 小时前
《HarmonyOS技术精讲-Core Speech Kit(基础语音服务)》第1篇:音频管理基础与语音服务入门
华为·音视频·harmonyos
最爱老式锅包肉5 小时前
HarmonyOS技术精讲-Speech Kit(场景化语音服务):AI字幕控件赋能实时语音识别与显示
语音识别·harmonyos