HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(十七):【语音识别】免提声控启动播报------动口不动手
摘要 :上一篇我们为菜谱详情页装上了"嘴巴"------接入 CoreSpeechKit 的 TTS 引擎实现了烹饪步骤分步语音播报。但你很快会发现:炒菜时手上全是油,根本没法点屏幕上的播放按钮。真正的免提厨房需要------进页面后直接说"开始播放",菜谱自动念起来 。本篇,我们将接入 HarmonyOS 6.1.0 CoreSpeechKit 的 **SpeechRecognizer(语音识别)**模块,为《灵犀厨房》装上"耳朵"。你将学会:进页面自动开启声控、免提启动 TTS 播报、播完后自动重开声控供重听、1 分钟无指令自动关闭以省电。关键设计决策:声控不用于播报过程中的操作控制(避免 TTS 音频被麦克风回采),而是专职于免提启动播报。 严格遵循 API 23 规范,代码即文档。
一、引言与系列定位
经过第 16 篇的语音合成,菜谱详情页有了播报按钮。但你很快会发现尴尬的现实:
| 场景 | 问题 | 解决 |
|---|---|---|
| 进页面后手上有油 | 没法点▶️播放按钮 | 直接说"开始播放" |
| TTS 播完一轮,想重听 | 屏幕已息屏,不想碰手机 | 声控自动重启,说"重新播放" |
| 只想重听某一步 | 想听步骤2但不想从头来 | 屏幕按钮点"下一步"跳转 |
| 声控开着但不用了 | 不想一直耗电监听 | 1 分钟无指令自动关闭 |
设计决策:为何声控不用于"下一步""暂停"等播报中途控制?
经过多次真机验证,我们发现:当 TTS 正在大声播报菜谱步骤时,手机麦克风会捕获自己的喇叭声,导致 ASR 把播报内容识别为语音指令 。例如 TTS 正在说"第三步,蒸锅水开后放入蛋液碗......",ASR 识别到长文本,尝试匹配指令时可能因关键词碰撞而误触发。这是一个物理层面的回采问题,目前在模拟器调试场景中暂时无法通过纯软件可靠解决。感兴趣读者,后续可以通过采用利用 AudioKit 的音频焦点与会话隔离、分时复用 + 短暂静音、唤醒词 + 录音回采过滤、将声控能力下移到系统级语音助手等方案进行验证和解决,菜品详情页面中预留了手动和自动切换的按钮,便于读者后续可以利用这个按钮进行功能扩展、增强和改进。
因此,本项目的声控设计为:
- TTS 播报前:声控自动开启,用户说"开始播放"启动播报
- TTS 播报中:声控自动关闭,用户通过屏幕按钮控制(暂停/上下步/停止)
- TTS 播报后:声控自动重启,用户可说"重新播放"重听,1 分钟无指令自动关闭
这就是声控操作的核心价值------进页面免提启动,播完免提重听。不是指挥每一步,而是"开始"和"重来"两个关键动作。
二、核心原理与底层机制深度解读
2.1 SpeechRecognizer:系统级的"云端+本地"语音识别
HarmonyOS 6.1.0(API 23)的 @kit.CoreSpeechKit 提供了 speechRecognizer 模块,封装了华为自研的语音识别引擎。它的工作模式是"持续监听 + 静默检测自动结束":
TtsSpeechManager RecipeDetailPage VoiceControlManager SpeechRecognizer 引擎 📱 手机 👤 用户 TtsSpeechManager RecipeDetailPage VoiceControlManager SpeechRecognizer 引擎 📱 手机 👤 用户 用户进入菜谱详情页 🎤 声控自动开启 🔊 播报步骤1、步骤2、步骤3... 🎤 声控自动重启 🔊 从头播报 initVoiceControl() initialize() + startListening() 说"开始播放" onResult("开始播放", isFinal=true) parseCommand() → VoiceCommand.RESUME onCommandExecuted(RESUME) handleTtsPlay() → TTS启动 stopListening() (TTS启动后关声控) onAllStepsComplete() restartVoiceControl() reset() + startListening() 说"重新播放" onResult("重新播放", isFinal=true) parseCommand() → VoiceCommand.REPEAT onCommandExecuted(REPEAT) currentStepIndex=0 → handleTtsPlay()
核心机制 :每次识别会话由 VAD(Voice Activity Detection,语音活动检测)控制生命周期。用户开始说话 → VAD 检测到语音 → 开始录制并识别 → 用户停顿超过 1.5 秒 → VAD 判定说话结束 →
onEnd回调 → 300ms 后自动重启监听。整个过程对用户透明,形成了"说完指令 → 系统执行 → 自动等下一句"的持续对话体验。
2.2 离线 vs 在线识别
| 维度 | 在线识别(online=1) | 离线识别(online=0) |
|---|---|---|
| 准确率 | 高(云端大模型) | 中(本地模型) |
| 延迟 | 网络延迟 ~200-500ms | 无延迟,~50ms |
| 网络依赖 | ✅ 必须联网 | ❌ 无需网络 |
| 适用场景 | 识别复杂长句 | 识别固定短指令 |
| 选型 | ✅ 本篇采用(烹饪场景需高准确率) | 备选(弱网环境兜底) |
厨房环境通常有 WiFi,在线识别是首选。但考虑到油烟机噪音、炒菜声等背景噪声,需要调高 VAD 灵敏度阈值。
2.3 指令别名匹配策略
用户不是机器人,不会每次都说标准的"开始播放"。真实场景中:
| 用户口语 | 期望指令 | 行为 |
|---|---|---|
| "开始播放"、"开始"、"播放" | resume | 启动 TTS 播报 |
| "重新播放"、"重播"、"从头开始" | repeat | 回到步骤1从头播报 |
| "暂停"、"停一下"、"停止" | pause/stop | 停止声控监听 |
这就是指令别名匹配 :每个 VoiceCommand 对应一组口语别名列表,使用 includes 子串匹配容忍口语化表达。
三、架构设计 / 核心逻辑图解
3.1 声控功能的四层架构
🏗️ 基础设施层
⚙️ 服务能力层
🧠 业务功能层
🎨 用户交互层
RecipeDetailPage
🎤 声控麦克风按钮
实时识别文字展示
VoiceControlManager
指令别名匹配
自动重启监听
指令→TTS操作映射
TtsSpeechManager
(上一篇已实现)
SpeechRecognizerHelper
封装 speechRecognizer
Listener → Promise 包装
VAD 参数配置
TtsServiceHelper
(上一篇已实现)
@kit.CoreSpeechKit
speechRecognizer 引擎
@kit.CoreSpeechKit
textToSpeech 引擎
设计原则 :声控(VoiceControlManager)与播报(TtsSpeechManager)是独立的 Business 层模块。VoiceControlManager 的职责是"听指令 + 解析指令 + 回调通知 UI 层",由 UI 层(RecipeDetailPage)的 onCommandExecuted 回调来实际触发 TTS 操作。这样:
- 单一职责:声控只管"听指令 + 解析",UI 层决定"听到指令后做什么"。
- TTS 播报期间自动关闭声控 :
handleTtsPlay()中检测声控激活时立即stopListening(),彻底避免 TTS 音频被麦克风回采。 - 播完后自动重启 :
onAllStepsComplete回调中调用restartVoiceControl(),用户可免提说"重新播放"。
3.2 声控监听生命周期
initialize()
引擎就绪
创建失败
startListening()
onEnd() / stopListening()
stopListening()
destroy()
destroy()
IDLE
INITIALIZING
READY
LISTENING
onResult → parseCommand
→ executeCommand
→ 300ms 后自动重启
STOPPED
关键设计 :
onEnd后 300ms 自动重启监听,而不是持续开启长连接------这样每次识别会话独立,避免了长连接下的内存泄漏和状态累积。
四、实战:为菜谱详情页装上"耳朵"
Step 1:SpeechRecognizerHelper ------ 封装 speechRecognizer
新建 services/SpeechRecognizerHelper.ets。核心任务:将 speechRecognizer 的 Listener 回调式 API 封装为可复用的服务层,配置 VAD 静默检测参数,提供 startListening/stopListening/destroy 生命周期管理。
typescript
// services/SpeechRecognizerHelper.ets
// 所属层:服务能力层 (Service Layer)
// 职责:封装 @kit.CoreSpeechKit 的 speechRecognizer 引擎
// 版本:v1 --- 第17篇,基于 API 23
import { speechRecognizer } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
export interface AsrInitParams {
language: string; // 'zh-CN'
online: number; // 1=在线
}
export interface AsrConfig {
vadEndPointDurationMs: number; // 静默超时(默认 1500ms)
maxDurationMs: number; // 最长识别时长(默认 15000ms)
}
export interface AsrResult {
text: string;
isFinal: boolean;
confidence?: number;
}
export enum AsrEngineState {
IDLE = 'idle', INITIALIZING = 'initializing',
READY = 'ready', LISTENING = 'listening', DESTROYED = 'destroyed'
}
export interface AsrCallbacks {
onResult?: (result: AsrResult) => void;
onError?: (error: string) => void;
onEnd?: () => void;
onStateChange?: (state: AsrEngineState) => void;
onSpeechStart?: () => void;
}
export class SpeechRecognizerHelper {
private engine: speechRecognizer.SpeechRecognitionEngine | null = null;
private state: AsrEngineState = AsrEngineState.IDLE;
private callbacks: AsrCallbacks = {};
private config: AsrConfig = {
vadEndPointDurationMs: 1500, // 1.5 秒静默自动结束
maxDurationMs: 15000 // 最长 15 秒
};
// ── 初始化 ──
async init(params: AsrInitParams): Promise<void> {
if (this.state === AsrEngineState.LISTENING) await this.stopListening();
if (this.engine) {
try { await this.engine.shutdown(); } catch (e) { /* ignore */ }
this.engine = null;
}
this.state = AsrEngineState.INITIALIZING;
this.notifyStateChange();
const createParams: speechRecognizer.CreateEngineParams = {
language: params.language,
online: params.online ?? 1,
extraParams: { 'locate': 'CN', 'name': 'LingxiKitchen_ASR' }
};
return new Promise<void>((resolve, reject) => {
speechRecognizer.createEngine(createParams,
(err: BusinessError, engine: speechRecognizer.SpeechRecognitionEngine) => {
if (err) { this.state = AsrEngineState.IDLE; reject(new Error(err.message)); return; }
this.engine = engine;
this.state = AsrEngineState.READY;
this.notifyStateChange();
resolve();
});
});
}
// ── 开始监听 ──
async startListening(): Promise<void> {
if (!this.engine || this.state === AsrEngineState.LISTENING) return;
this.state = AsrEngineState.LISTENING;
this.notifyStateChange();
const recognitionParams: speechRecognizer.StartParams = {
extraParams: {
'vadEndPointDurationMs': this.config.vadEndPointDurationMs,
'maxDurationMs': this.config.maxDurationMs,
'vadFrontDurationMs': 3000
}
};
this.engine.setListener({
onStart: () => { this.callbacks.onSpeechStart?.(); },
onResult: (result) => {
if (result?.result) {
this.callbacks.onResult?.({
text: result.result,
isFinal: result.isLast ?? true,
confidence: result.confidence ?? 0.8
});
}
},
onError: (code, msg) => { this.callbacks.onError?.(`${msg} (${code})`); },
onEnd: () => {
this.state = AsrEngineState.READY;
this.notifyStateChange();
this.callbacks.onEnd?.();
}
});
this.engine.startListening(recognitionParams);
}
// ── 停止、销毁、回调注册 ──
async stopListening(): Promise<void> { /* engine.stopListening() */ }
async destroy(): Promise<void> { /* shutdown + cleanup */ }
registerCallbacks(callbacks: AsrCallbacks): void { this.callbacks = callbacks; }
private notifyStateChange(): void { this.callbacks.onStateChange?.(this.state); }
}
export const speechRecognizerHelper: SpeechRecognizerHelper = new SpeechRecognizerHelper();
核心点解读:
- VAD 三段式参数 :
vadFrontDurationMs(最前允许 3 秒静默,给用户准备时间)、vadEndPointDurationMs(1.5 秒停顿判定结束)、maxDurationMs(最长 15 秒,防止误开长录音)。onEnd后自动重启 :不在SpeechRecognizerHelper层实现,而是在 Business 层的VoiceControlManager中------因为"是否重启"是业务决策,不是服务层职责。- 全局单例导出 :
export const speechRecognizerHelper确保整个应用共用一个 ASR 引擎实例。
Step 2:VoiceControlManager ------ 指令解析 + 自动重启
新建 business/VoiceControlManager.ets。核心任务:(1)定义 6 种声控指令 + 别名映射表;(2)每次识别到最终结果后,按别名匹配指令 → 执行 → 通知 UI;(3)onEnd 后 300ms 自动重启监听,形成持续对话循环。
typescript
// business/VoiceControlManager.ets
// 所属层:业务功能层 (Business Layer)
// 职责:识别语音指令 → 映射到 TTS 播报控制
export enum VoiceCommand {
NEXT = 'next', PREV = 'prev', REPEAT = 'repeat',
PAUSE = 'pause', RESUME = 'resume', STOP = 'stop', UNKNOWN = 'unknown'
}
// ★ 指令别名映射表------声控专职免提启动播报
const COMMAND_ALIASES: Map<VoiceCommand, string[]> = new Map([
[VoiceCommand.RESUME, ['开始', '播放', '开始播', 'start', '继续播', '接着来']],
[VoiceCommand.REPEAT, ['重新播放', '重播', '从头开始', '再播一遍', '重复', '再来一遍']],
[VoiceCommand.PAUSE, ['暂停', '停一下', '等一下', '等会']],
[VoiceCommand.STOP, ['停止', '别播了', '不听了', '关掉']],
]);
export class VoiceControlManager {
private state: VoiceControlState = VoiceControlState.IDLE;
private isActive: boolean = false;
private autoRestart: boolean = true;
// ── 指令解析:别名匹配 ──
parseCommand(text: string, confidence: number = 0.8): VoiceCommandResult {
const lower = text.trim().toLowerCase();
for (const [command, aliases] of Object.entries(COMMAND_ALIASES)) {
for (const alias of aliases) {
if (lower.includes(alias.toLowerCase())) {
return {
command: command as VoiceCommand,
rawText: text, matchedKeyword: alias, confidence
};
}
}
}
return { command: VoiceCommand.UNKNOWN, rawText: text, matchedKeyword: '', confidence };
}
// ── 指令执行:通知UI层,由UI层决定具体操作 ──
async executeCommand(command: VoiceCommand): Promise<void> {
switch (command) {
case VoiceCommand.RESUME:
// "开始"/"播放" → 通知UI启动TTS + 停止自身
console.info('[VoiceControl] 🎤 识别到"开始/播放" → 通知UI启动TTS并停止声控');
this.autoRestart = false;
break;
case VoiceCommand.REPEAT:
// "重新播放" → 通知UI从头播报 + 停止自身
console.info('[VoiceControl] 🎤 识别到"重新播放" → 通知UI从头播报并停止声控');
this.autoRestart = false;
break;
case VoiceCommand.PAUSE:
case VoiceCommand.STOP:
// "暂停"/"停止" → 停止自身监听
this.autoRestart = false;
break;
default:
break;
}
// 通知UI层执行(onCommandExecuted回调)
if (command !== VoiceCommand.UNKNOWN) {
this.callbacks.onCommandExecuted?.(command);
}
}
// ── ASR 回调注册:核心逻辑 ──
private registerAsrCallbacks(): void {
speechRecognizerHelper.registerCallbacks({
onResult: (result) => {
if (result.isFinal) {
const cmdResult = this.parseCommand(result.text);
if (cmdResult.command !== VoiceCommand.UNKNOWN) {
this.callbacks.onCommand?.(cmdResult);
this.executeCommand(cmdResult.command);
}
} else {
this.callbacks.onPartialResult?.(result.text); // UI 实时展示
}
},
onEnd: () => {
// ★ 自动重启:300ms 后重新开始监听
if (this.isActive && this.autoRestart) {
setTimeout(() => {
if (this.isActive) speechRecognizerHelper.startListening();
}, 300);
}
},
});
}
}
export const voiceControlManager: VoiceControlManager = new VoiceControlManager();
核心点解读:
- 别名匹配 :使用
includes而非精确匹配,可容忍口语中的多余词。例如:说"帮我跳到下一步"匹配"下一步"。- 指令执行状态检查 :每个
case都先检查 TTS 当前状态,避免非法操作(如在未播报时执行"暂停")。- 300ms 自动重启 :在
onEnd回调中延迟重启,给系统缓冲时间清理上一次的识别资源。- 中间结果展示 :
onPartialResult将实时识别到的文本传给 UI 显示,提供"我听到你说......"的即时反馈。
Step 3:改造 RecipeDetailPage.ets,集成声控 UI
在 RecipeDetailPage.ets 中:
(1)新增状态变量
typescript
@Local voiceControlActive: boolean = false;
@Local voiceControlState: VoiceControlState = VoiceControlState.IDLE;
@Local voiceControlPartialText: string = '';
@Local lastVoiceCommand: VoiceCommand = VoiceCommand.UNKNOWN;
(2)生命周期集成
typescript
aboutToAppear(): void {
this.registerTtsCallbacks();
this.initVoiceControl(); // ★ 初始化声控
}
aboutToDisappear(): void {
ttsSpeechManager.destroy();
voiceControlManager.destroy(); // ★ 销毁声控
}
(3)回调注册------RESUME 启动 TTS,REPEAT 从头播报:
typescript
voiceControlManager.registerCallbacks({
onCommandExecuted: (command: VoiceCommand): void => {
if (command === VoiceCommand.RESUME) {
// "开始播放" → 启动TTS播报
this.handleTtsPlay();
return;
}
if (command === VoiceCommand.REPEAT) {
// "重新播放" → 回步骤1从头播报
this.currentStepIndex = 0;
this.swiperController.changeIndex(0, false);
this.handleTtsPlay();
return;
}
// PAUSE/STOP: 停止声控监听
if (command === VoiceCommand.PAUSE || command === VoiceCommand.STOP) {
ToastUtil.showToast(this.getUIContext(), '🎤 声控已关闭');
return;
}
},
onPartialResult: (text: string): void => { this.voiceControlPartialText = text; },
});
(4)handleTtsPlay 中自动关闭声控:
typescript
if (this.voiceControlActive) {
await voiceControlManager.stopListening();
this.voiceControlActive = false;
}
(5)onAllStepsComplete 中自动重启声控:
typescript
onAllStepsComplete: (): void => {
this.ttsActive = false;
this.restartVoiceControl(); // ★ 播完自动重启
},
(4)TTS 控制栏新增麦克风按钮
在 buildTtsControlBar() 控制按钮行中(停止按钮左侧),插入:
typescript
// ★ 声控麦克风按钮
Button() {
if (this.voiceControlActive && this.voiceControlState === VoiceControlState.LISTENING) {
SymbolGlyph($r('sys.symbol.mic_fill')).fontSize(22).fontColor(['#FF6B35'])
} else {
SymbolGlyph($r('sys.symbol.mic')).fontSize(22).fontColor(['#666'])
}
}
.type(ButtonType.Circle).width(36).height(36)
.backgroundColor(this.voiceControlActive ? '#FFF0E6' : Color.Transparent)
.onClick(() => this.handleVoiceControlToggle())
在状态指示行下方新增声控状态提示:
typescript
if (this.voiceControlActive) {
Row({ space: 6 }) {
SymbolGlyph($r('sys.symbol.mic_fill')).fontSize(14).fontColor(['#FF6B35'])
Text('正在听...').fontSize(12).fontColor('#FF6B35')
if (this.voiceControlPartialText) {
Text(`"${this.voiceControlPartialText}"`).fontSize(11).fontColor('#FF6B35')
}
}
}
五、代码交付清单
| 文件 | 新增/修改 | 职责 |
|---|---|---|
services/SpeechRecognizerHelper.ets |
新增 | ASR 引擎封装:初始化、开始/停止监听、VAD 配置、Listener→Promise |
business/VoiceControlManager.ets |
新增 | 声控管理:6 指令别名匹配、指令→TTS 映射执行、300ms 自动重启监听 |
business/TtsSpeechManager.ets |
修改 | ★ v2 修复:pausedStepIndex 断点续播 bug 修复(第16篇遗留) |
pages/RecipeDetailPage.ets |
修改 | 新增麦克风按钮、声控状态行、回调注册、生命周期集成 |
六、设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 声控职责范围 | 仅免提启动播报(RESUME/REPEAT) | TTS 播报时麦克风会回采喇叭声,无法可靠识别用户指令 |
| TTS 播报期间声控 | 自动关闭 | handleTtsPlay 中调用 stopListening(),彻底阻断回采 |
| 播报完成后的声控 | 自动重启 + 1分钟超时 | 用户可能想重听,但不应无限耗电 |
| 进页面声控 | 自动开启 | 免去点击麦克风按钮的步骤,真正免提 |
| 屏幕息屏策略 | 页面存续期间常亮 | 锁屏会中断 TTS 播放,setWindowKeepScreenOn(true) 保持清醒 |
| 指令执行 | UI 层回调(非 Business 层直接操作) | 声控与 TTS 解耦,UI 层统一调度 |
| 别名匹配 | includes 子串匹配 |
容忍"帮我开始播放""再重新播报一次"等变体 |
| 在线 vs 离线 | 在线优先,离线降级 | SpeechRecognizerHelper.init 中在线失败自动尝试离线 |
七、运行与结果验证
7.1 操作步骤
- 部署到真机(模拟器也可以,电脑配置不高体验会有点差)。
- 从首页进入任意菜谱详情页------声控自动开启(麦克风图标亮起)。

- 直接说"开始播放"------TTS 启动播报,声控自动关闭。


- TTS 播完所有步骤------声控自动重启(麦克风图标再次亮起)。

- 说"重新播放"------回到步骤1从头播报。


| 指令 | 预期结果 |
|---|---|
| "开始播放" / "播放" / "开始" | TTS 启动播报,声控自动关闭 |
| "重新播放" / "重播" / "从头开始" | 回到步骤1从头播报 |
| "暂停" / "停一下" | 声控停止监听 |
| "停止" / "别播了" | 声控停止监听 |
| 1 分钟无指令 | 声控自动关闭,屏幕恢复自动息屏 |
7.2 控制台日志
[RecipeDetail] === 声控自动启动 ===
[VoiceControl] 状态: ready → listening
[SpeechRecognizer] 开始语音识别监听, sessionId=asr_xxx
[SpeechRecognizer] 识别结果: "开始播放", isFinal=true, isLast=true
[VoiceControl] 匹配指令: "开始播放" → resume (别名: "开始")
[VoiceControl] 🎤 识别到"开始/播放" → 通知UI启动TTS并停止声控
[RecipeDetail] 声控"开始/播放" → 启动TTS播报并关闭声控
[RecipeDetail] TTS启动 → 自动关闭声控
[TtsSpeechManager] 状态变更: idle → initializing → ready → speaking
[RecipeDetail] TTS 状态更新: speaking
...(播报步骤1、2、3)...
[RecipeDetail] 全部步骤播报完成
[RecipeDetail] 声控已重启,说"重新播放"或点屏幕任意按钮播报, 1分钟无指令自动关闭
[VoiceControl] 状态: ready → listening
[SpeechRecognizer] 识别结果: "重新播放", isFinal=true
[VoiceControl] 匹配指令: "重新播放" → repeat (别名: "重新播放")
[RecipeDetail] 声控"重新播放" → 从头播报
[RecipeDetail] TTS启动 → 自动关闭声控
验证要点:
- 进页面声控自动开启,无需点击麦克风按钮。
"开始播放"触发 TTS,声控立即关闭。- 播完后声控自动重启,日志显示
restartVoiceControl。"重新播放"触发后 Swiper 回步骤1,TTS 重新开始。
八、本阶段总结与下篇预告
今天,我们为《灵犀厨房》装上了"耳朵"------实现了免提声控启动播报的完整链路:
- 封装
SpeechRecognizerHelper:将 CoreSpeechKit 的speechRecognizer回调 API 包装为服务层,配置 VAD 参数,支持在线/离线自动降级。 - 构建
VoiceControlManager:定义声控指令 + 口语别名,parseCommand子串匹配,executeCommand通过 UI 层回调解耦操作。 - 关键设计决策------声控不用于播报中途控制:经过真机验证,TTS 播报时麦克风会回采喇叭声导致 ASR 误识别。因此声控专职免提启动播报:进页面自动开启、说"开始播放"启动 TTS 后自动关闭、播完自动重启供重听、1 分钟无指令自动关闭。
- 集成声控 UI:麦克风按钮 + 实时识别文字展示行 + 屏幕常亮防止锁屏打断 TTS。
现在,你进入菜谱详情页,说"开始播放",菜谱自动念;播完后说"重新播放",从头再听------真正做到动口不动手。
但烹饪时还有一个高频痛点:计时。炖牛肉要 40 分钟,蒸鱼要 8 分钟,你不可能一直盯着手机。如果计时器能流转到手表上------手腕一抬就看到倒计时,到点了轻震提醒------那才是真正的"云端厨房"。
下篇预告:第 18 篇《【手表协同】烹饪计时器流转至智能手表》。我们将实现从手机设置倒计时 → 流转到 HarmonyOS 智能手表 → 震动提醒的全链路协同,让计时跟随你的手腕,而非手机屏幕。
📚 本系列持续更新中:下一篇将实现手表协同计时器,让你手腕掌控烹饪节奏。
🔗 专栏入口:[《HarmonyOS6.1全场景实战》合集]
📦 获取基线版本源码包 :包括第1-16篇所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对你有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬。你的支持,是我继续输出高质量技术内容的全部动力。
纯血鸿蒙,动口造厨。我们下一篇见!