HarmonyOS 实战第六篇:让 AI 建议“说出来”——语音播报与推荐卡片联动方案
摘要 本文介绍了如何在HarmonyOS应用中实现AI建议的语音播报功能,以"知行生活小助手"为例。文章重点讲解了语音播报服务封装、推荐卡片交互设计和状态管理。通过VoiceBroadcastManager将TTS引擎封装为单例,实现播放/停止控制和状态管理。AIRecommendCard组件集成建议展示、语音播报和采纳反馈功能,形成完整交互闭环。关键点包括:语音文本优化生成(建议+原因组合)、播
HarmonyOS 实战第六篇:让 AI 建议“说出来”——语音播报与推荐卡片联动方案
摘要
在生活助手类应用里,文字建议只完成了一半体验。真正能让用户感到“贴心”的,是系统不只会写建议,还能把建议自然地说出来。本文以“知行生活小助手”为例,讲解如何用 HarmonyOS 的语音播报能力把 AIRecommendCard 中的建议内容转换成可听的播报结果,并通过状态机管理播放、停止、加载和错误处理。文章重点放在语音播报服务的封装方式、推荐卡片的交互设计,以及实际落地时最容易忽略的细节。

目录
- 为什么要做语音播报
- 推荐卡片的交互目标
- 语音播报服务如何封装
- 推荐卡片如何联动播放状态
- 语音文本如何生成
- 常见异常与处理方式
- 总结
一、为什么要做语音播报
生活建议类产品最大的价值,不是“给出一条建议”,而是“把建议变成动作”。文字适合浏览,语音适合更自然的接收:开车、做家务、走路、休息时,用户未必愿意盯着屏幕,但愿意听一句简洁的提示。
AIRecommendCard 中的语音播报按钮就是这个场景的入口。用户可以在首页直接收听当前建议,而不是先点进详情页再读一遍文字。这个设计有两个好处:
- 降低阅读成本,让建议更像提醒。
- 让“采纳建议”前的决策过程更自然。
二、推荐卡片的交互目标
推荐卡片并不是简单展示文本,而是一个带有完整状态反馈的交互组件。它需要同时处理:
| 目标 | 具体表现 |
|---|---|
| 当前建议展示 | 显示建议正文、理由和图标 |
| 语音播报 | 一键朗读建议内容 |
| 播放状态反馈 | 播放中、加载中、空闲、错误 |
| 采纳反馈 | 用户采纳后卡片状态变化 |
| 换一条操作 | 生成新建议并刷新卡片 |
对应的组件就是 AIRecommendCard,它把推荐内容、按钮状态和语音播报状态放在同一个 UI 里,避免用户在多个页面之间跳转。
三、语音播报服务如何封装
语音逻辑集中在 VoiceBroadcastManager,它把底层 TTS 引擎、监听器和状态机做成单例。
export type VoiceBroadcastState = 'idle' | 'loading' | 'speaking' | 'stopping' | 'error';
export class VoiceBroadcastManager {
private static instance: VoiceBroadcastManager | null = null;
private engine: textToSpeech.TextToSpeechEngine | null = null;
private listeners: VoiceBroadcastListener[] = [];
private currentState: VoiceBroadcastState = 'idle';
private currentRequestId: string = '';
}
这类封装的核心目标很明确:让页面不用直接面对 TTS 引擎初始化细节,只需要调用 speak() 和 stop()。
1. 引擎初始化
ensureEngine() 会在第一次播报时创建引擎,并在多个 preset 中自动尝试:
private async createEngine(): Promise<textToSpeech.TextToSpeechEngine> {
let lastErrorMessage = '未知错误';
for (const preset of ENGINE_PRESETS) {
try {
const engine = await textToSpeech.createEngine({
language: preset.language,
person: preset.person,
online: preset.online
});
return engine;
} catch (error) {
lastErrorMessage = (error as Error).message ?? lastErrorMessage;
}
}
throw new Error(`语音引擎初始化失败:${lastErrorMessage}`);
}
这种写法的好处是兼容性更强。不同设备、不同语音配置可能支持的参数不同,多个 preset 轮询能提升成功率。
2. 播放与停止
async speak(text: string): Promise<void> {
const content = this.normalizeText(text);
const engine = await this.ensureEngine();
if (engine.isBusy()) {
try {
engine.stop();
} catch (_) {}
}
const requestId = `${Date.now()}_${Math.floor(Math.random() * 1000000)}`;
this.currentRequestId = requestId;
this.setState('speaking');
engine.speak(content, { requestId });
}
requestId 的设计很关键。它让回调可以区分当前这次播报和历史播报,避免旧回调误修改新状态。
四、推荐卡片如何联动播放状态
AIRecommendCard 会订阅 VoiceBroadcastManager 的状态变化:
private voiceListener = (state: VoiceBroadcastState) => {
this.voiceState = state;
};
aboutToAppear() {
this.unsubscribeVoice = this.voiceService.subscribe(this.voiceListener);
}
这样一来,播放按钮的图标和颜色可以根据当前状态实时变化:
| 状态 | UI 表现 |
|---|---|
idle |
显示默认播报图标 |
loading |
显示加载进度 |
speaking |
显示播放中状态和停止能力 |
stopping |
过渡到空闲 |
error |
回到可重试状态 |
推荐卡片中的按钮不是孤立按钮,它和整个播报状态机绑定,这也是文章里值得强调的工程化思路。

五、语音文本如何生成
播报时并不是简单把建议正文读出来,而是把“建议正文 + 推荐原因”拼起来:
private getVoiceText(): string {
const content = this.suggestion?.content?.trim() ?? '';
const reasoning = this.suggestion?.reasoning?.trim() ?? '';
const parts: string[] = [];
if (content) {
parts.push(`今日建议:${content}`);
}
if (reasoning) {
parts.push(`推荐原因:${reasoning}`);
}
return parts.join('。');
}
这种文本组织方式比直接读一段长文本更清晰,因为语音播报的目标不是“复述页面”,而是“把用户需要听的核心信息讲明白”。
六、常见异常与处理方式
1. 没有可播报内容
当当前卡片为空时,speak() 会抛出“没有可播报的内容”。这比静默失败更好,因为用户能明确知道为什么没播报。
2. 引擎忙碌
如果引擎正在播别的内容,当前实现会先尝试 stop() 再开始新播报,避免多个请求互相干扰。
3. 错误回退
播报失败时,AIRecommendCard 会通过 toast 告知用户:
promptAction.showToast({
message: err.message ?? '语音播报失败,请稍后重试',
duration: 2200
});
对于生活助手类产品来说,失败提示很重要。它不是技术细节,而是产品信任感的一部分。
七、卡片阅读节奏
AIRecommendCard 的阅读顺序是刻意设计过的:先标题,再主建议,再理由,最后才是动作按钮。它不是把所有内容平均摊开,而是按优先级分层。
| 层级 | 作用 |
|---|---|
| 第一层 | 让用户立即知道今天的建议是什么 |
| 第二层 | 告诉用户推荐原因 |
| 第三层 | 提供语音、采纳和换一条动作 |
| 第四层 | 给出当前状态提示 |
这种节奏很适合生活助手产品,因为用户通常只会停留几秒钟。卡片必须先“让人看懂”,再“让人行动”。
八、按压动画为什么重要
卡片里的按钮做了按压缩放和阴影变化,这看起来是很小的细节,但实际能显著提升交互确认感:
.scale({ x: this.adoptScale, y: this.adoptScale })
.animation({ duration: 150, curve: Curve.EaseInOut })
.onTouch((e) => {
if (e.type === TouchType.Down) {
this.adoptScale = 0.95;
} else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
this.adoptScale = 1;
}
})
这类反馈在 AI 产品里尤其重要,因为用户面对的是“系统判断”,而不是一个纯机械按钮。轻微的动态变化会让用户更容易相信系统真的在响应。
九、首页链路如何闭环
语音播报功能不是孤立的,它和首页建议生成形成了一个完整闭环:
生成建议
-> 推荐卡片显示内容和理由
-> 用户点击语音播报
-> 系统播报建议正文和推荐原因
-> 用户采纳或换一条
-> 首页刷新
这条链路能把“看建议”变成“听建议、理解建议、采纳建议”,也让首页不只是一个展示页,而是真正的决策入口。
十、测试建议
建议至少做下面几类测试:
| 场景 | 预期结果 |
|---|---|
| 建议为空 | 播报按钮提示无内容 |
| 首次播报 | 引擎先进入 loading 再播放 |
| 播放中再次点击 | 触发 stop 而不是重复播放 |
| 播报失败 | toast 显示错误 |
| 切换到新建议 | 旧 requestId 不污染新状态 |
如果你要把文章写得更像完整实战复盘,还可以补一句“我在真机上反复测试了播报、停止和换一条三个链路,确保状态没有串”。这类话特别加分。
十一、总结
语音播报让知行生活小助手从“会推荐”进一步变成“会提醒”。VoiceBroadcastManager 负责状态和引擎管理,AIRecommendCard 负责交互和展示,两者共同组成了一个自然、可打断、可恢复的播报链路。这个功能的工程亮点不在语音本身,而在状态机、资源释放和 UI 联动的完整性。
推荐标签
HarmonyOS ArkTS 语音播报 TTS 组件联动
更多推荐



所有评论(0)