系列第 4 篇。本文讲一个比赛现场很实用的增强:用户点 A 队 +1 或 B 队 +1 后,App 自动播报当前比分;如果设备不支持,也不能影响手动计分。
一、真实问题背景
羽毛球双打现场,计分员经常同时要看球、点分、提醒换边、记录最终比分。如果每次都低头看手机,比赛节奏会被打断。比分播报的价值不是“炫技”,而是降低现场操作负担。
项目里的实时计分页已经有手动 +1、撤销、重置、结束比赛等基础操作。TTS 播报作为可选增强出现:用户可以选择“播报开/关”,也可以选择“每分播报”或“仅关键分”。
二、目标与边界
本文只讲当前已落地的比分播报,不把语音识别计分混在一起。目标包括:
1. 创建 Core Speech Kit TTS 引擎。2. 每次比分变化时生成播报文案。3. 支持关键分策略。4. 引擎失败时自动关闭播报,不阻断手动计分。
边界是:当前 TTS 是短文本播报,不做后台长文朗读、不接 AVSession 播控卡片、不承诺所有设备都有离线语音包。语音识别和手表端计分属于后续文章。
三、服务层封装
项目把 TTS 能力封装在common/src/main/ets/service/ScoreSpeechService.ets,页面不直接操作引擎。
import { textToSpeech } from '@kit.CoreSpeechKit'; export class ScoreSpeechService { private static engine: textToSpeech.TextToSpeechEngine | undefined = undefined; private static available: boolean = true; private static async ensureEngine(): Promise<textToSpeech.TextToSpeechEngine | undefined> { const engine = await textToSpeech.createEngine({ language: 'zh-CN', person: 0, online: 1 }); ScoreSpeechService.engine = engine; return engine; } }这里的online: 1按当前项目规则作为离线模式使用。正式发布前仍要在目标设备上验证语音包、音量、系统版本和失败回调表现。
四、只播最新一条
比赛现场连续加分很快,如果每次都排队播完,会出现“比分已经 8 比 6,语音还在播 5 比 4”的错位。项目的做法是在播报前先尝试停止上一条。
static async speak(text: string): Promise<boolean> { const content = text.trim(); if (content.length === 0 || !ScoreSpeechService.available) { return false; } const engine = await ScoreSpeechService.ensureEngine(); if (engine === undefined) { return false; } engine.stop(); const requestId = `score_tts_${Date.now()}`; engine.speak(content, { requestId }); return true; }这不是最复杂的媒体队列,但非常适合短比分播报。它优先保证“当前比分正确”,而不是保证每条历史语音都播完。
五、页面侧策略
features/src/main/ets/scoring/LiveScoringPage.ets维护两个状态:speechEnabled和speechCriticalOnly。前者决定是否播报,后者决定每分播还是只播关键分。
@State speechEnabled: boolean = false; @State speechCriticalOnly: boolean = false; speakScoreIfNeeded(matchId: string, previousScoreA: number = -1, previousScoreB: number = -1): void { if (!this.speechEnabled) { return; } if (this.speechCriticalOnly && !this.isCriticalScore(match)) { return; } this.speakText(this.scoreAnnouncement(match)); }这个状态设计有一个好处:播报只是计分流程的旁路。即使 TTS 失败,比分仍然写入页面状态,比赛仍然可以继续。
六、失败兜底
设备不支持、离线语音包不可用、引擎创建失败,都可能导致播报失败。项目的处理方式是:ScoreSpeechService.speak返回false后,页面关闭播报开关并给出提示。
ScoreSpeechService.speak(text).then((success: boolean) => { if (!success) { this.speechEnabled = false; this.speechRenderTick += 1; Feedback.toast('当前设备暂不可用语音播报'); } });这条边界非常重要。比赛现场不能因为一个增强能力不可用,就让手动计分崩掉。TTS 是锦上添花,手动计分才是主流程。
七、取舍与风险
当前实现没有做后台播控,也没有做长时间音频任务。原因是比分播报通常是短文本、短时触发,不需要把它包装成完整播放器。后续如果要做“后台长文朗读”或“系统播控卡片”,就应接入 AVSession 和后台音频任务,那是另一类工程。
另一个风险是语音播报与快速点击的竞态。项目通过“播前 stop”降低队列积压,但仍需要真机验证高频点击、切后台、音量为零、语音包缺失等场景。
八、验证命令
构建验证:
& 'D:\HuaweiDevelopFormalStudy\DevEco Studio\tools\hvigor\bin\hvigorw.bat' assembleHap --mode module -p product=default --no-daemon验证时间:2026-06-28。当前构建结果为BUILD SUCCESSFUL。真机验收建议至少覆盖:开启播报、连续加分、切换关键分、结束比赛播报、禁用语音包时自动关闭。
九、官方参考
Core Speech Kit 相关 API 需要以官方文档为准,可从 HarmonyOS AI / 语音能力文档入口 查询当前 SDK 的@kit.CoreSpeechKit能力和设备限制。
十、工程验收清单
- TTS 封装在common服务层,页面不直接持有复杂引擎逻辑。- 播报开关默认不干扰手动计分。- 快速加分时优先播最新比分。- TTS 失败后关闭播报,不破坏比分记录。- 关键分策略在页面侧控制,便于以后扩展。- 语音识别计分不与本文混淆,后续单独验证。
十一、小结
系统语音能力最适合从“小而稳”的场景切入。比分播报就是这样的切入点:短文本、低权限、失败可兜底、业务价值直接。把这一步做好,再考虑语音计分、手表端计分和多设备同步,工程风险会低很多。
十二、下一篇衔接
下一篇讲分享闭环:如何把费用计算结果截图成图片,写入缓存文件,再通过 ShareKit 分享给其他应用。