Files
sentinel-home-ai/fam-edge/config/config.yaml
ericwyuan ef56ae5662 feat(fam-edge): 新增 DiskGuard 磁盘空间守护,剩余空间不足自动清理旧素材
背景:Oracle 磁盘曾经被写满(gdrive_videos 持续下载新素材、旧文件迟迟没
清理),触发 rclone 的一个安全机制——同步遇到 IO 错误就整体拒绝执行删除,
形成"越满越删不掉,越删不掉越满"的死循环,最终连新视频都下载不了。这次
排查+手动清理已经解决了当次故障,但需要一道独立于 rclone 同步之外的兜底,
防止再次悄悄写满没人发现。

- oracle_db.py 新增 get_oldest_purgeable_material():只挑最旧的、已完成
  分割阶段(status='done')的整段素材(非 motion_ 前缀),绝不碰运动片段
  (事件时间轴/人物头像依赖它)和还在处理中的素材
- disk_guard.py 新增 DiskGuard 后台线程:5 分钟检查一次,剩余空间 <10GB
  触发清理,删到 15GB 水位为止(留缓冲避免刚清完又立刻触发),复用已有的
  delete_video() 完成实际删除
- app.py 启动这个后台服务;/api/oracle/activity 新增 disk 字段(实时剩余
  空间 + 最近一次清理动作),供服务状态页展示
- 新增 9 个单元测试,全部通过(139/139)

已部署 Oracle 验证:DiskGuard 正常启动,配置生效。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 12:08:04 +08:00

190 lines
11 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FAM-Edge 配置文件 (Oracle 端) - 新架构 v2
#
# 新架构2026-08-21 重构):
# 1. 不再切片/抽帧:整视频直传云端 VLMGemini 用 Files APINVIDIA 用整视频 video_url
# 2. 视频来源rclone 从 Google 硬盘实时同步到本地 local_dir监听目录处理新视频
# 3. Oracle 自建 SQLite 库存储所有视频摘要/事件/人物,并对外提供同步接口供 NAS 拉取
# 4. 独立 person_service 汇总全量人物 -> LLM 合并为规范人物表 -> 回灌视频提示
# 5. NAS 仅作管理后台,每 30 分钟从甲骨文拉增量镜像到本地 MariaDB
# Oracle 端 HTTP 服务
server:
host: "0.0.0.0"
port: 5000
# Google 硬盘同步rclone 负责同步落地,本段仅描述监听行为)
gdrive_sync:
enabled: true
local_dir: "/opt/fam-edge/gdrive_videos" # rclone 同步落地目录video_processing 监听此目录)
watch_interval_sec: 30 # 监听新视频的轮询间隔
camera_name: "客厅" # 摄像头名称(注入视频提示)
# 文件名解析开始时间:监控文件名含时间戳时使用(如 2026-08-21_081500.mp4
parse_start_from_filename: true
# Oracle 本地库(视频摘要/事件/人物)
oracle_db:
path: "/opt/fam-edge/data/oracle.db"
# NAS 拉取同步接口鉴权 token与 NAS oracle_sync.token 一致,走 .env不明文入库
sync_api:
token: "${ORACLE_SYNC_TOKEN}"
# 人物识别服务2026-08-22 停用):已被闭集识别 person_identifier 取代
# (汤圆/媳妇走性别年龄规则免费识别,爷爷/爸爸走视觉大模型比对参考图,见
# person_identifier 配置块)。原 LLM 合并逻辑不可靠(用户原话"现在的识别全是
# 错的"),继续跑只会用不可靠的猜测持续覆盖 people 表的 canonical_name跟新
# 系统的结果打架——停用,不删除代码(关键冲突检测逻辑 _features_conflict 仍
# 保留供参考/未来复用)。
person_service:
enabled: false
schedule_interval_sec: 1800
model: "gemini"
# 视频处理
video_processing:
max_concurrent: 1 # 消费者线程数(串行处理,避免云端并发超额)
timeout: 900 # 兜底单视频分析超时
timeout_multiplier: 2 # 模型消费超时倍数:在 models[i].timeout 原值上 ×2大视频上传+分析耗时)
max_retries: 10 # 单视频失败最大重试次数(配额/过载等瞬时故障给足重试机会)
retry_interval_sec: 3600 # 失败重试最小间隔:距上次失败 ≥1h 才重新入队,等配额恢复
file_validate: true # 登记入队前用 ffprobe 校验文件可解码;失败标记 invalid 不入队
stable_window_sec: 60 # 文件 mtime 稳定窗口写入中rclone 同步未完成)的文件跳过本轮
# 降级顺序:先 gemini 整视频,失败再 nvidia 整视频;两者都失败 -> 标记 failed
vision_order: ["gemini", "nvidia"]
# 运动事件2026-08-22 运动事件驱动架构):
# - NAS 端 fam-core MotionNotifier 轮询 SS 事件后POST 推送到甲骨文
# /api/ss/motion落库 ss_motion_eventsstart_time/duration 为 Unix epoch
# 并定期用空 events 调接口当心跳,证明推送链路存活。
# - video_processor 不再整段分析:整段素材按 ss_motion_events 中【已结束】的
# 运动事件分割成运动片段motion_segment 配置),只分析运动片段。
dsm_motion_prefilter:
max_heartbeat_age_sec: 900 # 心跳超过 15 分钟没更新 -> 判定推送链路可能已死
# 运动片段分割运动事件驱动架构2026-08-22 新增):
# 整段素材视频rclone 同步落地)按 ss_motion_events 分割成运动片段再分析。
# 只分割已结束事件start_time+duration 落在当前时刻附近),进行中的事件
# 等结束后的下一轮再分割;素材保持 pending 直到窗口内事件全部结束。
motion_segment:
clips_dir: "/opt/fam-edge/motion_clips" # 分割产物目录
keep_audio: true # 保留音频pcm_alaw -> aac 64k 转码false 则 -an 去音频
min_duration_sec: 1 # 短于该时长的事件不分割
unfinished_grace_sec: 10 # start_time+duration 距当前 ≤ 该秒视为"已结束"容差
# 磁盘空间守护2026-08-28 新增Oracle 磁盘曾经被写满,触发 rclone 遇到
# IO 错误就拒绝删除的保护机制,形成"越满越删不掉"的死循环,导致新视频下载
# 和运动片段分割全部失败。这里加一道独立于 rclone 同步之外的兜底:不管上游
# 同步是否正常,剩余空间跌破 min_free_gb 就主动清理最旧的、已完成分割阶段
# 的整段素材gdrive_videos 里的原始录像,不碰 motion_clips 里的运动片段)。
disk_guard:
enabled: true
check_interval_sec: 300 # 5 分钟检查一次
min_free_gb: 10 # 剩余空间低于此值触发清理
target_free_gb: 15 # 清理到这个水位就停(留缓冲,避免刚清完又立刻再触发)
watch_path: "/opt/fam-edge" # 检查这个路径所在磁盘分区的剩余空间
max_delete_per_round: 50 # 单轮最多清理几个文件,防止候选异常多时一次删太多
# 闭集人物识别2026-08-22 新增,家里固定 4 人:爷爷/爸爸/媳妇/汤圆):
# 原来靠大模型自己编的"人物A/B/C"临时 uid + 文字特征描述跨视频合并,验证下来
# 不可靠(用户原话"现在的识别全是错的")。人脸向量方案也验证过,家庭监控这种
# 大广角/远距离画面下同人内部相似度经常比不同人还低,此路不通。
# 现在改成:汤圆(幼儿/儿童特征)/媳妇(唯一成年女性) 直接用 Gemini 已产出的
# 性别/年龄字段判断,免费且验证下来接近 100% 准;爷爷/爸爸(两个成年男性,纯
# 外观规则/人脸向量都区分不开) 改用视觉大模型"看参考图比对"——NVIDIA 实测
# 6/6 全对且配额与主分析链路完全独立设为优先Gemini flash-lite(7/8) 兜底。
# 参考图目录结构:{ref_dir}/爷爷/*.jpg、{ref_dir}/爸爸/*.jpg已用确认过身份的
# 历史截图种好,见 PROGRESS.md 记录)。
person_identifier:
enabled: true
ref_dir: "/opt/fam-edge/data/person_refs"
max_ref_per_person: 6
# 连续两次分类调用之间的最小间隔(不分 provider 统一限速):正常处理新片段时
# 调用本来就稀疏,这个主要是给历史数据批量回填用的,避免短时间内密集调用打爆配额
min_call_interval_sec: 2
nvidia:
api_key: "${NVIDIA_API_KEY}"
model_name: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
fallback_models: [] # 目前只验证过这一个能用的 NVIDIA 视觉模型,先留好扩展位
timeout: 60
max_retries: 3 # 每个模型对瞬时故障429/5xx/超时)最多重试几次(含首次)
retry_backoff_sec: 3 # 重试前基础等待秒数指数退避3s -> 6s -> 12s
gemini:
api_key: "${GEMINI_API_KEY}"
extra_api_keys: ["${GEMINI_API_KEY_2}", "${GEMINI_API_KEY_3}", "${GEMINI_API_KEY_4}"]
model_name: "gemini-flash-lite-latest"
timeout: 60
max_retries: 2
retry_backoff_sec: 3
# 智能问答2026-08-23 抽离到独立 ai-gateway 服务OpenAI 兼容协议):
# fam-edge 这边只是转发客户端,问答本体的模型降级链/key 轮换/熔断都在
# ai-gateway 自己的 config.yaml 里配置,这里只填怎么连它。
# token 走 .env AI_GATEWAY_TOKEN跟 ai-gateway 侧配置的值必须一致。
ai_gateway:
base_url: "http://127.0.0.1:5100" # 同机部署,走本地回环,不走公网
token: "${AI_GATEWAY_TOKEN}"
timeout: 60
# 视频分析模型链
models:
- provider: "gemini"
role: "vision"
enabled: true
model_name: "gemini-flash-latest" # 主模型(每日免费配额 20 请求,按模型独立)
fallback_models:
- "gemini-flash-lite-latest"
api_key: "${GEMINI_API_KEY}"
# 多 Key 轮换(各自独立 Google Cloud 项目,配额互不影响):主 key 配额用尽时
# 依次尝试这些 key每个 key 都会重新走一遍模型 fallback 链。key 本身放 .env
# 这里只放环境变量名,不直接写密钥。
extra_api_keys:
- "${GEMINI_API_KEY_2}"
- "${GEMINI_API_KEY_3}"
- "${GEMINI_API_KEY_4}"
# 每个 key 对应的 Google Cloud 项目名(顺序对应上面 api_key + extra_api_keys
# 用于模型统计页展示是哪个项目在跑,不填则退回 "key1/key2/..."
key_labels:
- "智能摄像头-1"
- "智能摄像头-2"
- "智能摄像头-3"
- "智能摄像头-4"
timeout: 600
# 模型级独立超时(最终值,不参与编排层 ×2 放大)
# gemini-flash-lite 实测 ~22-34s按用户要求放宽至 8 分钟480s避免大视频/排队时过早切断
model_timeouts:
"gemini-flash-lite-latest": 480
circuit_breaker:
enabled: true
threshold: 5
cooldown: 300
- provider: "nvidia"
role: "vision"
enabled: true
# 模型可用性实测记录2026-08-21用真实短视频逐个探测 chat.completions 接口):
# omni本行 model_namevideo_url 只认 base64 data URINVCF asset_id 引用
# 方式对它直接 500"Only base64 data URLs are supported for now")——本适配器
# 已改为 base64 内嵌整段视频,见 max_base64_mb。确认可用真实返回结构化 JSON
# nemotron-nano-12b-v2-vl走 asset_id 引用需要 NVCF-ASSET-DIR/
# NVCF-FUNCTION-ASSET-IDS 请求头,这两个头的值由 NVCF 服务端按内部路径生成,
# 客户端传什么都 400 "Invalid NVCF-ASSET-DIR",标准 OpenAI 兼容调用打不通,已移除。
# meta/llama-3.2-11b-vision-instruct明确不支持视频输入
# "At most 0 video(s) may be provided"),只能单图,已移除。
# base64 方案受请求体大小限制(实测约 25MB 上限),真实监控视频压缩后通常在
# 20MB 上下,超过 max_base64_mb 直接跳过(不做注定失败的慢速编码),不是本地
# 故意限制过窄——这是当前唯一能打通的 NVIDIA 视频理解路径。
model_name: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning"
fallback_models: []
base_url: "https://integrate.api.nvidia.com/v1"
api_key: "${NVIDIA_API_KEY}"
timeout: 600
max_base64_mb: 20 # 超过此大小直接跳过 NVIDIA不做注定失败的编码+上传
switch_interval_sec: 5 # 模型切换间隔:一个失败后等待再试下一个(未来加模型时用)
model_timeouts: # 模型级独立超时(最终值,不参与 ×2
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": 300
circuit_breaker:
enabled: true
threshold: 5
cooldown: 300