
当你在 HarmonyOS NEXT 上尝试把语音唤醒、识别和合成串起来时,会发现最大的坑根本不是单个 API 怎么调,而是这三个模块之间的状态怎么同步。
前几天有个读者在群里问:"我用 speechRecognizer 识别成功,然后用 textToSpeech 合成回复,但有时候唤醒模块还没停止,两个语音流就打架了。有什么好的处理方式吗?"
这个问题实际上非常典型。Core Speech Kit 的每个能力单独看都不复杂,但把它们组合成一个语音助手时,状态管理、生命周期和线程同步才是真正的难点。
这个项目解决什么问题
一个完整的语音助手必须处理三个核心动作:唤醒 → 识别 → 合成反馈。三个环节环环相扣:
- 唤醒成功后,立即开启识别
- 识别到结果后,调用大模型或规则匹配,生成回复
- 合成回复文本并播放
很多教程只告诉你每个 API 怎么调,但实际项目中真正麻烦的是:
- 状态混乱------唤醒回调还没执行完,识别已经开始了
- 资源冲突------多个语音服务同时操作音频设备
- 回调线程------在 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();
});
最佳实践
-
不要在 StateManager.setState 中直接修改 UI 。状态变化通知通过
Listener传播到 UI 组件,由 ArkUI 框架统一更新。如果在回调里直接调用this.stateManager.setState并且触发 UI 更新,容易出现状态竞争。 -
TaskPool 任务参数必须是可序列化的 。回调函数需要使用
() => void类型,不能传递对象引用。这是 HarmonyOS 线程模型的一个重要约束。 -
合成播放完成回调必须切换状态 。
TTSManager的speak回调中,如果发生错误也要调用回调,否则助手会卡在SPEAKING状态。上面代码中已经在错误回调中调用了onFinish。
FAQ
Q:为什么真机运行正常,模拟器上识别经常超时?
A:模拟器的麦克风驱动和真机不一样,特别是语音触发类功能,对硬件要求较高。建议在真机上测试所有语音功能,模拟器只用于 UI 调试。
Q:唤醒词识别率低,有什么优化方法?
A:检查 threshold 参数,默认 0.6 在安静环境够用,但在嘈杂环境中建议提高到 0.7 以上。另外,唤醒词录制质量很重要,建议在安静环境录制,避免有背景音。
Q:语音合成播放时,点击停止后还是继续播放完当前句子?
A:textToSpeech 的 stop() 是异步的,不一定能立即中断音频流。如果需要强制停止,可以在 stopSpeaking 后立即释放引擎资源。目前版本建议让当前句子播完,下一句再控制是否播放。
如果你也在做语音助手类应用,建议先把状态机跑通,再优化识别精度和合成音质。这比一开始就想着加各种功能要稳定得多。