AI 驱动的家庭场景识别:自动切换影音、休息与工作模式界面

📅 2026/7/27 1:14:26 👁️ 阅读次数 📝 编程学习
AI 驱动的家庭场景识别:自动切换影音、休息与工作模式界面

AI 驱动的家庭场景识别:自动切换影音、休息与工作模式界面

一、引子:模式切换不应该是一个按钮

在智能家居产品评审会上,产品经理指着设计稿说:"这里加一个模式切换按钮,用户点击后弹出四个选项:影音模式、阅读模式、休息模式、工作模式。"

技术上行得通,交互上是个灾难。模式切换按钮是场景自动化的反面——它把"根据上下文自动判断应该进入什么模式"的智能需求,退化成了"请你手动告诉我你想干什么"的传统交互。

真正的场景自动化应该是无感的。用户走进客厅、打开投影仪、拉上窗帘——这三个动作的组合已经告诉系统"我要看电影了"。系统的任务不是弹出确认框问"是否开启影音模式?",而是静默执行:灯光调暗到 10%、氛围灯设为暖色调、音响切到 HDMI ARC、空调设为静音模式。只在首次执行时给出一个轻微的通知:"已自动切换到影音模式,点击可调整"。

场景识别的技术本质是一个多传感器融合的意图分类问题。输入是设备状态变化、时间、位置、人体感应等多维信号,输出是一个场景标签和置信度。难点不在分类算法本身,而在分类的时机——既不能反应太慢(用户都开始看电影了灯光还没暗),也不能反应太快(用户只是路过客厅拿个东西就被判定为"要休息")。

二、底层机制:多信号融合的场景分类器

场景识别需要一组弱信号的组合推理,单个信号都不可靠,但多个信号的交叉验证可以大幅提升置信度。

关键设计决策在于置信度分级执行策略。不是所有场景识别结果都应该自动执行。将置信度分为三档:

  • 高置信度(> 0.85):投影仪开机 + 窗帘关闭 + 时间是晚上 + 人在客厅持续 10 秒以上 → 自动切换影音模式,通知:"已切换至影音模式"
  • 中置信度(0.6 - 0.85):窗帘关闭 + 时间晚上 + 但投影仪未开 → 建议切换:"检测到你可能想看视频,需要切换到影音模式吗?"
  • 低置信度(< 0.6):仅窗帘关闭 → 忽略,等待更多信号

分类器的核心输入信号权重如下:

  • 设备主动操作(用户点了什么):权重 0.4
  • 人体传感器数据:权重 0.25
  • 时间上下文:权重 0.2
  • 历史行为模式:权重 0.15

三、生产级代码:场景识别引擎

/** * 家庭场景识别引擎 * * 基于多传感器信号融合的意图分类器。 * 输入:原始传感器事件流 * 输出:场景标签 + 置信度 + 执行策略 */ // 场景标签 type SceneLabel = | 'movie_time' // 影音模式 | 'reading' // 阅读模式 | 'resting' // 休息模式 | 'working' // 工作模式 | 'dining' // 用餐模式 | 'leaving' // 离家模式 | 'sleeping' // 睡眠模式 | 'unknown'; // 无法识别 // 传感器事件 interface SensorEvent { type: 'device_state' | 'presence' | 'time' | 'audio'; deviceId?: string; property?: string; value: number | boolean | string; timestamp: number; } // 场景识别结果 interface SceneRecognition { label: SceneLabel; confidence: number; // 0-1 strategy: 'auto' | 'suggest' | 'ignore'; reasoning: string[]; // 可解释的推理步骤 suggestedActions: SceneAction[]; } // 场景动作 interface SceneAction { deviceId: string; action: string; params: Record<string, any>; priority: number; } /** * 场景识别器 * * 使用加权规则引擎而非深度学习模型,确保: * 1. 推理过程可解释(调试时能看懂为什么触发) * 2. 冷启动友好(不需要训练数据) * 3. 延迟极低(< 50ms,适合实时场景) */ class SceneRecognizer { // 滑动窗口:保留最近 30 秒的传感器事件 private eventWindow: SensorEvent[] = []; private readonly WINDOW_MS = 30000; // 场景识别规则表 // 每条规则定义了一个场景的信号组合和权重 private readonly rules: SceneRule[] = [ { label: 'movie_time', signals: [ { type: 'device_state', value: ['projector_power_on', 'tv_power_on'], weight: 0.3 }, { type: 'device_state', value: ['curtain_closed'], weight: 0.2 }, { type: 'time', value: ['evening', 'night'], weight: 0.15 }, { type: 'presence', value: ['person_in_living_room'], weight: 0.2 }, { type: 'audio', value: ['movie_sound'], weight: 0.15 }, ], }, { label: 'sleeping', signals: [ { type: 'time', value: ['late_night'], weight: 0.3 }, { type: 'presence', value: ['person_in_bedroom'], weight: 0.25 }, { type: 'device_state', value: ['all_lights_off'], weight: 0.25 }, { type: 'device_state', value: ['bedroom_curtain_closed'], weight: 0.2 }, ], }, { label: 'leaving', signals: [ { type: 'device_state', value: ['lock_locked'], weight: 0.3 }, { type: 'presence', value: ['no_one_home'], weight: 0.4 }, { type: 'time', value: ['morning'], weight: 0.15 }, { type: 'device_state', value: ['all_lights_off'], weight: 0.15 }, ], }, { label: 'reading', signals: [ { type: 'device_state', value: ['reading_lamp_on'], weight: 0.4 }, { type: 'presence', value: ['person_stationary'], weight: 0.2 }, { type: 'time', value: ['evening'], weight: 0.2 }, { type: 'audio', value: ['silence'], weight: 0.2 }, ], }, ]; // 历史场景记录(用于模式学习) private sceneHistory: { label: SceneLabel; timestamp: number }[] = []; /** * 接收传感器事件并触发场景识别 * * 使用去抖动策略:事件发生后等待 2 秒, * 收集窗口内的所有相关事件再统一分析 */ private debounceTimer: number | null = null; onSensorEvent(event: SensorEvent): void { this.eventWindow.push(event); // 清理过期事件 const cutoff = Date.now() - this.WINDOW_MS; this.eventWindow = this.eventWindow.filter( (e) => e.timestamp > cutoff ); // 去抖:2 秒内连续事件只触发一次识别 if (this.debounceTimer) clearTimeout(this.debounceTimer); this.debounceTimer = window.setTimeout(() => { this.recognize(); }, 2000); } /** * 执行场景识别 */ recognize(): SceneRecognition { let bestLabel: SceneLabel = 'unknown'; let bestConfidence = 0; let bestReasoning: string[] = []; let bestActions: SceneAction[] = []; // 遍历所有场景规则 for (const rule of this.rules) { let confidence = 0; const reasoning: string[] = []; const matchedSignals: string[] = []; // 检查每条信号 for (const signal of rule.signals) { const isMatched = this.matchSignal( signal, this.eventWindow ); if (isMatched) { confidence += signal.weight; matchedSignals.push(this.describeSignal(signal)); } else { reasoning.push(`未检测到: ${this.describeSignal(signal)}`); } } // 应用历史模式加成 const historyBoost = this.getHistoryBoost(rule.label); confidence += historyBoost; reasoning.unshift( `场景「${rule.label}」匹配度: ${(confidence * 100).toFixed(0)}%`, `匹配信号: ${matchedSignals.join(', ') || '无'}` ); if (confidence > bestConfidence) { bestConfidence = confidence; bestLabel = rule.label; bestReasoning = reasoning; bestActions = this.getActionsForScene(rule.label); } } // 置信度封顶在 1.0 bestConfidence = Math.min(bestConfidence, 1.0); return { label: bestLabel, confidence: bestConfidence, strategy: this.determineStrategy(bestConfidence, bestLabel), reasoning: bestReasoning, suggestedActions: bestActions, }; } /** * 信号匹配检查 * * 在事件窗口中查找匹配的传感器事件 */ private matchSignal( signal: SceneSignal, events: SensorEvent[] ): boolean { const matchingEvents = events.filter((e) => { if (e.type !== signal.type) return false; // 值匹配(支持数组多值匹配) if (Array.isArray(signal.value)) { return signal.value.some((v) => this.valueMatches(e.value, v)); } return this.valueMatches(e.value, signal.value); }); return matchingEvents.length > 0; } /** * 值匹配逻辑 */ private valueMatches( actual: number | boolean | string, expected: number | boolean | string ): boolean { // 字符串模糊匹配 if (typeof actual === 'string' && typeof expected === 'string') { return actual.toLowerCase().includes(expected.toLowerCase()); } return actual === expected; } /** * 信号描述(用于可解释性) */ private describeSignal(signal: SceneSignal): string { const descriptions: Record<string, string> = { projector_power_on: '投影仪已开机', curtain_closed: '窗帘已关闭', all_lights_off: '全部灯光已关闭', lock_locked: '门锁已上锁', no_one_home: '家中无人', person_in_living_room: '客厅有人', person_in_bedroom: '卧室有人', person_stationary: '人处于静止状态', evening: '当前时段:傍晚', night: '当前时段:夜间', late_night: '当前时段:深夜', morning: '当前时段:早晨', silence: '环境安静', reading_lamp_on: '阅读灯已开启', }; return descriptions[String(signal.value)] || String(signal.value); } /** * 历史模式加成 * * 最近 7 天同一时段该场景出现频次越高,加成越大 */ private getHistoryBoost(label: SceneLabel): number { const now = new Date(); const currentHour = now.getHours(); const dayOfWeek = now.getDay(); // 查找最近 7 天同一时段的历史 const recentMatches = this.sceneHistory.filter((h) => { const hDate = new Date(h.timestamp); const hourDiff = Math.abs(hDate.getHours() - currentHour); return h.label === label && hourDiff <= 1; }); // 7 天中出现了 4 天以上,加成 0.1 return recentMatches.length >= 4 ? 0.1 : 0; } /** * 确定执行策略 */ private determineStrategy( confidence: number, label: SceneLabel ): 'auto' | 'suggest' | 'ignore' { if (confidence < 0.55) return 'ignore'; // 离家模式即使中等置信度也自动执行(安全优先) if (label === 'leaving' && confidence > 0.7) return 'auto'; // 睡眠模式需要高置信度 if (label === 'sleeping' && confidence > 0.85) return 'auto'; // 通用策略 if (confidence > 0.85) return 'auto'; if (confidence > 0.55) return 'suggest'; return 'ignore'; } /** * 获取场景对应的操作序列 */ private getActionsForScene(label: SceneLabel): SceneAction[] { const actionMap: Record<string, SceneAction[]> = { movie_time: [ { deviceId: 'living-light-main', action: 'setBrightness', params: { value: 5 }, priority: 1 }, { deviceId: 'living-light-ambient', action: 'setColorTemp', params: { value: 3200 }, priority: 2 }, { deviceId: 'curtain-living', action: 'setPosition', params: { value: 0 }, priority: 1 }, { deviceId: 'speaker-main', action: 'setInput', params: { value: 'hdmi_arc' }, priority: 3 }, { deviceId: 'ac-living', action: 'setMode', params: { value: 'quiet' }, priority: 4 }, ], sleeping: [ { deviceId: 'all-lights', action: 'setPower', params: { value: false }, priority: 1 }, { deviceId: 'curtain-bedroom', action: 'setPosition', params: { value: 0 }, priority: 1 }, { deviceId: 'thermostat', action: 'setTemperature', params: { value: 26 }, priority: 2 }, ], leaving: [ { deviceId: 'all-lights', action: 'setPower', params: { value: false }, priority: 1 }, { deviceId: 'all-curtains', action: 'setPosition', params: { value: 0 }, priority: 1 }, { deviceId: 'ac-all', action: 'setPower', params: { value: false }, priority: 2 }, { deviceId: 'lock-front', action: 'lock', params: {}, priority: 0 }, ], }; return actionMap[label] || []; } /** * 记录场景切换(用于历史学习) */ recordScene(label: SceneLabel): void { this.sceneHistory.push({ label, timestamp: Date.now() }); // 保留最近 30 天数据 const cutoff = Date.now() - 30 * 24 * 3600 * 1000; this.sceneHistory = this.sceneHistory.filter( (h) => h.timestamp > cutoff ); } } // 类型补充 interface SceneSignal { type: 'device_state' | 'presence' | 'time' | 'audio'; value: string | number | boolean | (string | number | boolean)[]; weight: number; } interface SceneRule { label: SceneLabel; signals: SceneSignal[]; }

四、边界分析

误触发是最大的信任杀手:场景识别的错误分两类——误报(false positive)和漏报(false negative)。误报的代价远大于漏报。系统在用户没有看电影时自动关了灯,用户会觉得"这破系统又在自作主张"。系统在用户看电影时没关灯,用户会觉得"哦,可能没识别到",手动关一下就好。设计上应该偏保守:宁可漏报 20%,也不能误报超过 5%。

连续场景切换抖动:用户从客厅走到卧室,可能触发"影音→行走→休息"三次连续切换。UI 闪烁会让用户困惑。解决方案是引入最小场景停留时间——一旦切换到一个场景,至少停留 30 秒才能再次切换。

多成员家庭的冲突:爸爸在客厅看电影(影音模式),妈妈在卧室看书(阅读模式),系统应该按哪个场景?答案是按设备归属性——每个场景的操作限定在目标房间的设备范围内。客厅的影音模式不应该影响卧室的灯光。

五、总结

  1. 场景自动化应该是无感切换,而非弹出模式选择对话框
  2. 多传感器融合的置信度分级(auto/suggest/ignore)是场景识别的正确交互模型
  3. 加权规则引擎优于深度学习模型,可解释性在场景识别中比准确率更重要
  4. 误报(false positive)比漏报(false negative)更伤害用户信任
  5. 连续切换需要 30 秒最小停留时间防止 UI 抖动
  6. 历史模式加成(7 天同时段频次)可以提升 10% 的识别准确率
  7. 场景操作应限定在目标房间范围内,避免多成员场景冲突