【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十七):【语音识别】免提声控启动播报——动口不动手

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 操作。这样:

  1. 单一职责:声控只管"听指令 + 解析",UI 层决定"听到指令后做什么"。
  2. TTS 播报期间自动关闭声控handleTtsPlay() 中检测声控激活时立即 stopListening(),彻底避免 TTS 音频被麦克风回采。
  3. 播完后自动重启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 操作步骤

  1. 部署到真机(模拟器也可以,电脑配置不高体验会有点差)。
  2. 从首页进入任意菜谱详情页------声控自动开启(麦克风图标亮起)。
  3. 直接说"开始播放"------TTS 启动播报,声控自动关闭。

  4. TTS 播完所有步骤------声控自动重启(麦克风图标再次亮起)。
  5. 说"重新播放"------回到步骤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 后端

如果你觉得这篇文章对你有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬。你的支持,是我继续输出高质量技术内容的全部动力。
纯血鸿蒙,动口造厨。我们下一篇见!

相关推荐
花间相见4 小时前
【语音识别】— FunASR 项目详解与 Fun-ASR-Nano 实战
人工智能·语音识别
花间相见4 小时前
【语音识别部署】— sherpa-onnx:让 ASR 模型跑得更快、跑在任何地方
人工智能·语音识别
天上路人18 小时前
A-59F所有应用模式说明
人工智能·硬件架构·音视频·语音识别·实时音视频
俊基科技19 小时前
A-29P深度解析:100dB回音消除与AI降噪的硬件设计实战
语音识别·ai降噪·回声消除·语音模组
曦月合一1 天前
语音识别网页版转化成APP版
app·语音识别·谷歌浏览器
byzh_rc1 天前
[自然语言处理-入门] 语音识别
人工智能·自然语言处理·语音识别
若兰幽竹2 天前
【HarmonyOS6.1全场景实战】基线版本:我用了15篇文章,造出了一个能登录、能推荐、带后台的鸿蒙全栈App
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
做萤石二次开发的哈哈2 天前
如何调用接口向指定设备下发语音播放?
人工智能·语音识别
05大叔2 天前
生成式任务
人工智能·语音识别