HarmonyOS 实战第六篇:让 AI 建议“说出来”——语音播报与推荐卡片联动方案

摘要

在生活助手类应用里,文字建议只完成了一半体验。真正能让用户感到“贴心”的,是系统不只会写建议,还能把建议自然地说出来。本文以“知行生活小助手”为例,讲解如何用 HarmonyOS 的语音播报能力把 AIRecommendCard 中的建议内容转换成可听的播报结果,并通过状态机管理播放、停止、加载和错误处理。文章重点放在语音播报服务的封装方式、推荐卡片的交互设计,以及实际落地时最容易忽略的细节。

在这里插入图片描述

目录

  1. 为什么要做语音播报
  2. 推荐卡片的交互目标
  3. 语音播报服务如何封装
  4. 推荐卡片如何联动播放状态
  5. 语音文本如何生成
  6. 常见异常与处理方式
  7. 总结

一、为什么要做语音播报

生活建议类产品最大的价值,不是“给出一条建议”,而是“把建议变成动作”。文字适合浏览,语音适合更自然的接收:开车、做家务、走路、休息时,用户未必愿意盯着屏幕,但愿意听一句简洁的提示。

AIRecommendCard 中的语音播报按钮就是这个场景的入口。用户可以在首页直接收听当前建议,而不是先点进详情页再读一遍文字。这个设计有两个好处:

  1. 降低阅读成本,让建议更像提醒。
  2. 让“采纳建议”前的决策过程更自然。

二、推荐卡片的交互目标

推荐卡片并不是简单展示文本,而是一个带有完整状态反馈的交互组件。它需要同时处理:

目标 具体表现
当前建议展示 显示建议正文、理由和图标
语音播报 一键朗读建议内容
播放状态反馈 播放中、加载中、空闲、错误
采纳反馈 用户采纳后卡片状态变化
换一条操作 生成新建议并刷新卡片

对应的组件就是 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 组件联动

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐