From ffcc292ccec2b96dad0687da27aa0d68064cf133 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Sat, 22 Aug 2026 12:46:33 +0800 Subject: [PATCH] =?UTF-8?q?[=E6=96=87=E6=A1=A3]=20=E5=90=8C=E6=AD=A5=20v3?= =?UTF-8?q?=20=E8=BF=90=E5=8A=A8=E4=BA=8B=E4=BB=B6=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=20-=20=E6=95=B4=E6=AE=B5=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E5=88=86=E5=89=B2=E8=BF=90=E5=8A=A8=E7=89=87=E6=AE=B5=E5=86=8D?= =?UTF-8?q?=E5=88=86=E6=9E=90(=E4=B8=8D=E5=86=8D=E6=95=B4=E6=AE=B5?= =?UTF-8?q?=E9=80=81=E4=BA=91=E7=AB=AF):=20README=201.1/2.2/2.2.1/2.3/?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E8=A1=A8,=20PROGRESS=20=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E7=8A=B6=E6=80=81+=E5=8F=98=E6=9B=B4=E8=AE=B0=E5=BD=95;=20?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20systemd=20=E5=AE=88=E6=8A=A4=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROGRESS.md | 18 ++++++++++-- README.md | 82 +++++++++++++++++++++++++++-------------------------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index f72ecf2..143371d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,15 +1,27 @@ # 项目进度追踪 -> 最后更新: 2026-08-22 11:08 +> 最后更新: 2026-08-22 12:45 ## 服务运行状态 | 服务 | 节点 | 地址 | 状态 | 验证结果 | |------|------|------|------|---------| -| FAM-Core | NAS | 0.0.0.0:8000 | ✅ 运行中 | health=ok, gunicorn 单 worker;Oracle-Sync + 运动监测 Webhook(轮询已关闭,SS→NAS→Oracle 推送) | -| FAM-Edge | Oracle | 0.0.0.0:5000 | ✅ 运行中 | v2.0, SQLite 异步队列 + 消费者线程 + TokenBucket 速率限制 | +| FAM-Core | NAS | 0.0.0.0:8000 | ✅ 运行中 | health=ok, gunicorn 单 worker;Oracle-Sync + MotionNotifier 轮询 SS 事件推送(游标 DB 续用/失败重试) | +| FAM-Edge | Oracle | 0.0.0.0:5000 | ✅ 运行中 | systemd 守护(fam-edge.service);素材→运动片段分割→只分析片段;队列消费正常 | | MariaDB | NAS | 127.0.0.1:3306 | ✅ 运行中 | 10.11.11, 6 张表, utf8mb4 | | Ollama | Oracle | 127.0.0.1:11434 | ✅ 运行中 | qwen2.5:7b,仅智能问答兜底(不参与视觉/融合) | + +## 2026-08-22 运动事件驱动架构(v3) + +- **决策**:不再分析整段视频。rclone 整段素材保留在 Oracle,按 NAS 推送的 `ss_motion_events`(start_time/duration)**ffmpeg 分割成运动片段**,只分析片段。 +- **改动**(commit `a1523b4` + `8d6cfad`,已部署 Oracle systemd 重启): + - `oracle_db.py`:videos 表兼容加 `motion_event_id`/`camera_id` 列;新增 `get_motion_events_in_range`(窗口内已结束事件,grace 容差)/`has_unfinished_motion_in_range`/`get_video_by_motion_event_id` + - `video_processor.py`:素材→分割(只分割已结束事件,`-c:v copy -c:a aac` 保留音频,motion_event_id 幂等)/片段→只分析 双分支;修复 ffmpeg args 缺可执行文件 bug + - `video_queue.py`:素材分割出的片段入队 + - `config.yaml`:新增 `motion_segment` 块(clips_dir/keep_audio/min_duration/unfinished_grace_sec) +- **E2E 验证(真机)**:素材 `Generic_ONVIF-001-20260822-110859-...mp4` → 分割 35 段(真实 event_id 25504-25538、duration 1-132s)→ 片段只分析(summary 真实:人物/动作)→ events(绝对时间 ts)→ NAS 同步 → `/api/ui/videos` 显示 motion_ 片段(字段契约不变:filename/event_start_time/camera_name/event_count/summary_json/compute_provider)→ `/api/ui/videos/` 详情 events + people 正常 → `/api/proxy/frame` 帧图 200 OK +- **关键认知**:SS 事件 duration=0 仅在动作**进行中**(结束时必为正数,实测 id=25538 从 0→2s),故只分割已结束事件(start+duration ≤ now+grace),进行中的等下一轮。 +- **部署注意**:Oracle fam-edge 由 **systemd `fam-edge.service` 守护**(Restart=always),代码部署后必须 `sudo systemctl restart fam-edge`(手动 setsid 会与守护打架导致端口冲突)。 | Tailscale | Oracle ↔ NAS | 100.74 ↔ 100.70 | ⚠️ 待修复 | Tailscale 运行但端口不通,当前用公网IP | | FAM-UI | NAS | 0.0.0.0:8501 | ✅ 运行中 | Streamlit 1.61.1, HTTP 200, health=ok | diff --git a/README.md b/README.md index b16a7e8..2c4b3cd 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > 多模型容灾降级 + 交互式命名 + AI 对话的家庭监控系统。 > 本文档为项目需求文档与 README 的整合版,按当前代码实际状态(v1.0 E2E 已打通)编写。 -> 最后更新:2026-08-20(云端多模型方案整合) +> 最后更新:2026-08-22(运动事件驱动架构:整段素材分割运动片段再分析) --- @@ -13,13 +13,13 @@ ### 1.1 首期范围(已基本完成) -> **新架构 v2(2026-08-21 重构)**:NAS 不再处理视频,仅作管理后台;视频分析全部上云(Oracle)。 +> **新架构 v3(2026-08-22 重构:运动事件驱动)**:NAS 不再处理视频,仅作管理后台 + 运动事件推送;视频分析全部上云(Oracle),且**不再分析整段视频**——整段素材按 SS 运动事件**分割成运动片段**后只分析片段。 -- **FAM-Core**(NAS 端单进程):仅 **Oracle-Sync**(每 30 分钟拉增量镜像)+ **Chat-Handler** + **Member-Manager** 三个子模块,CPU 占用极低 -- **FAM-Edge**(Oracle 端单进程):rclone 实时同步 Google 硬盘视频 → 监听目录 → **整视频直传云端 VLM**(Gemini 用 Files API / NVIDIA 用整视频 `video_url`,不切片不抽帧)→ 结构化 JSON 落本地 SQLite → 对外提供 `/api/oracle/sync` 增量拉取接口(**本地模型不参与视频分析**) -- **FAM-UI**(NAS 端):Streamlit 读本地同步镜像(sync_videos / sync_events / sync_people),事件时间轴 + 人物管理 + AI 对话 + 对话历史 + 统计 -- **数据库**:Oracle 侧 SQLite(videos/events/people/sync_cursor);NAS 侧 MariaDB 镜像(sync_videos / sync_events / sync_people / sync_cursor)+ chat_history -- **数据流向**:Google 硬盘 ──rclone──► 甲骨文本地 ──整视频分析──► Oracle SQLite ──每 30 分钟 NAS 拉取──► NAS MariaDB 镜像 ──► FAM-UI +- **FAM-Core**(NAS 端单进程):**Oracle-Sync**(每 30 分钟拉增量镜像)+ **MotionNotifier**(轮询 SS 运动事件推送 Oracle)+ **Chat-Handler** + **Member-Manager**,CPU 占用极低 +- **FAM-Edge**(Oracle 端单进程):rclone 实时同步 Google 硬盘视频(**整段素材**)→ 按 `ss_motion_events` 运动事件(start_time/duration)**ffmpeg 分割运动片段** → **只把运动片段送云端 VLM**(Gemini 用 Files API / NVIDIA 整视频 `video_url`)→ 结构化 JSON 落本地 SQLite → 对外提供 `/api/oracle/sync` 增量拉取接口(**本地模型不参与视频分析**) +- **FAM-UI**(NAS 端):Vue3 SPA 读本地同步镜像(sync_videos / sync_events / sync_people),事件时间轴 + 人物管理 + AI 对话 + 对话历史 + 统计 +- **数据库**:Oracle 侧 SQLite(videos/events/people/ss_motion_events/sync_cursor);NAS 侧 MariaDB 镜像(sync_videos / sync_events / sync_people / sync_cursor)+ chat_history +- **数据流向**:Google 硬盘 ──rclone──► 甲骨文整段素材 ──按运动事件分割片段──► 片段云端分析 ──► Oracle SQLite ──每 30 分钟 NAS 拉取──► NAS MariaDB 镜像 ──► FAM-UI;运动事件由 NAS 轮询 SS 推送 Oracle(NAS → Oracle 单向) - **AI 对话**:查 sync_events 拼上下文 → 经 FAM-Edge 问答编排(Gemini → NVIDIA → 本地 Ollama 兜底)生成回答 → 返回并写 chat_history - **人物命名**:用户命名/合并某 label → 回推 Oracle `/api/oracle/people/correct`(manual 优先)→ 下一周期同步回 NAS;Oracle 独立 person_service 汇总全量人物 → LLM 合并为规范名 → 回灌视频提示 @@ -84,13 +84,13 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA N ▼ ┌──────────────────────── Oracle Cloud (129.146.203.203) ────────────────────────┐ │ FAM-Edge (Flask :5000) │ -│ ├─ Watch-Processor: 30s 轮询 /opt/fam-edge/gdrive_videos,新视频串行处理 │ -│ ├─ Video-Processor: 整视频直传云端 VLM(不切片不抽帧) │ -│ │ Gemini(Files API) → 失败 NVIDIA(整视频 video_url) → 再失败 FAILED │ +│ ├─ Video-Queue: 30s 轮询 /opt/fam-edge/gdrive_videos 登记整段素材 │ +│ ├─ Video-Processor: 素材按 ss_motion_events 分割运动片段 → 只分析片段 │ +│ │ (ffmpeg -c:v copy -c:a aac) → Gemini → 失败 NVIDIA → 再失败 FAILED │ │ ├─ Person-Service: 汇总人物 → LLM 合并规范名 → 回灌视频提示 │ -│ ├─ OracleDB (SQLite): videos / events / people / sync_cursor │ -│ └─ API: /api/oracle/sync (增量拉取) · /api/oracle/people/correct (命名校正) · │ -│ /api/edge/chat/ask (问答编排 Gemini→NVIDIA→Ollama) │ +│ ├─ OracleDB (SQLite): videos / events / people / ss_motion_events │ +│ └─ API: /api/oracle/sync (增量拉取) · /api/ss/motion (运动事件) · │ +│ /api/oracle/people/correct (命名校正) · /api/edge/chat/ask (问答) │ └───────────────────────────────┬───────────────────────────────────────────────┘ │ HTTP GET /api/oracle/sync?since=&token= (每 30 分钟) ▼ @@ -106,49 +106,51 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA N └─────────────────────────────────────────────────────────────────────────────────┘ ``` -#### 2.2.1 运动监测链路(v3,Webhook 驱动 · 去轮询) +#### 2.2.1 运动监测链路(v3,NAS 轮询驱动) -摄像头动作事件由 **Surveillance Station 主动推送**,不经过任何轮询: +摄像头动作事件由 **NAS 端 MotionNotifier 轮询 Surveillance Station 事件列表**获取(简单稳定,事件不遗漏;Webhook 端点在代码中保留为可选低延迟补充): Surveillance Station (NAS 本机) - │ 事件=偵測到動作 → 行動規則「Webhook」动作:event_time=%EVENT_TIME% 等 - │ HTTP POST(同机 127.0.0.1:8000,无需外网) + │ SYNO.SurveillanceStation.EventCenter.Event method=List + │ camera_ids=2, event_types=10, start_time/end_time(下划线风格) ▼ - NAS FAM-Core :8000 /api/ss/webhook (motion_bp.py) - │ ├─ 解析 JSON / 表单(兼容单条与数组) - │ ├─ 字段映射:%DEVICE_NAME% → camera_id(查 SS 名→id 映射)、%EVENT_TIME% → epoch - │ └─ 合成稳定 event_id = hash(device|time|thumb)(供甲骨文 UNIQUE 幂等去重) - │ HTTP POST /api/ss/motion?token=ORACLE_SYNC_TOKEN(事件触发即推) + NAS FAM-Core MotionNotifier(motion_notifier.py,每 60s) + │ ├─ 增量游标(MariaDB sync_cursor.motion_last_event_id,DB 续用/重启补推) + │ ├─ 推送失败批次不前进游标(下轮重试,不丢事件) + │ └─ 定期空 events 心跳(证明推送链路存活) + │ HTTP POST /api/ss/motion?token=ORACLE_SYNC_TOKEN(真实 event_id/start_time/duration) ▼ Oracle FAM-Edge :5000 /api/ss/motion (api_gateway.py) - │ → OracleDB.ss_motion_events(event_id UNIQUE,自动去重) - │ → video_processor.has_motion_in_range_local() 查本地事件做运动预过滤 + │ → OracleDB.ss_motion_events(event_id UNIQUE,自动去重;start_time/duration 为 Unix epoch) + ▼ + Oracle video_processor:整段素材按运动事件【分割运动片段】→ 只分析片段(见 2.3) -> 设计要点:甲骨文**不反向访问** NAS;SS 推送是 NAS 入站(同机 127.0.0.1)。 -> 失败-open:`ss_motion_events` 为空时预过滤返回 None(不误杀视频)。 +> 设计要点:甲骨文**不反向访问** NAS;只分割**已结束**事件(SS 事件 duration 在动作进行中为 0、结束才回填真实时长)。 +> 失败-open:心跳超过 `max_heartbeat_age_sec`(900s)未更新时预过滤/分割 fail-open,不误判"无运动"。 **网络要点(新架构)**: - NAS → Oracle 出站共两条:① `GET /api/oracle/sync`(拉取增量)+ `POST /api/oracle/people/correct`(命名回推);② `POST /api/ss/motion`(运动侦测事件推送)。均走 Oracle 公网 IP:5000,token 鉴权 - Oracle Ollama :11434 不对外暴露,问答经 FAM-Edge `/api/edge/chat/ask` 代理 - Tailscale 两节点在线但 NAS 无法反向访问 Oracle,故全部走 NAS 主动出站拉取模式 -### 2.3 主链路时序(新架构 v2) +### 2.3 主链路时序(新架构 v3:运动事件驱动) -1. Google 硬盘新视频 → rclone 定时同步到 Oracle `/opt/fam-edge/gdrive_videos` -2. Watch-Processor 轮询发现新文件 → 登记到 Oracle `videos`(pending) -3. Video-Processor 串行处理:整视频上传 Gemini Files API(或 NVIDIA 整视频 `video_url`)→ 模型直出 `{global_summary, events[], people_mentioned[]}` → 写 Oracle `videos` + `events` + `people` -4. Person-Service 每 30 分钟汇总全量人物 → LLM 合并为规范名 → 更新 `people.canonical_name` → 生成 `known_members_context` 回灌后续视频提示 -5. NAS Oracle-Sync 每 30 分钟 `GET /api/oracle/sync?since=` → upsert 到本地 `sync_*` 镜像表 → 推进 `sync_cursor` -6. FAM-UI 读本地镜像展示;用户命名 → `POST /api/oracle/people/correct` 回推 Oracle,下一周期同步生效 +1. Google 硬盘新视频(整段素材)→ rclone 定时同步到 Oracle `/opt/fam-edge/gdrive_videos` +2. Video-Queue 轮询发现新文件 → 登记到 Oracle `videos`(pending) +3. Video-Processor 处理素材:按文件名解析开始时间 → 查窗口内 `ss_motion_events` **已结束**运动事件 → ffmpeg 分割运动片段(`-c:v copy -c:a aac` 保留音频)→ 片段登记 `videos`(pending,`motion_event_id` 关联)并入队;素材标记"已分割 N 段"(仍有未结束事件则保持 pending 下轮再分割) +4. Video-Processor 处理运动片段:**只分析片段**(不分析整段)→ Gemini(或 NVIDIA 兜底)直出 `{global_summary, events[], people_mentioned[]}` → 写 Oracle `videos` + `events` + `people` +5. Person-Service 每 30 分钟汇总全量人物 → LLM 合并为规范名 → 更新 `people.canonical_name` → 生成 `known_members_context` 回灌后续片段提示 +6. NAS Oracle-Sync 每 30 分钟 `GET /api/oracle/sync?since=` → upsert 到本地 `sync_*` 镜像表 → 推进 `sync_cursor` +7. FAM-UI 读本地镜像展示(时间轴/人物/统计字段契约不变);用户命名 → `POST /api/oracle/people/correct` 回推 Oracle,下一周期同步生效 -**运动监测支流(v3,Webhook 驱动 · 去轮询)**: -- Surveillance Station 侦测到动作 → 行動規則「Webhook」动作 `POST :8000/api/ss/webhook`(参数 `event_time=%EVENT_TIME%`、`device_name=%DEVICE_NAME%`、`event_name`/`server_name`/`thumbnail_url`) -- FAM-Core `motion_bp` 解析并映射(`%DEVICE_NAME%`→camera_id、`%EVENT_TIME%`→epoch、合成稳定 event_id)→ `POST /api/ss/motion` 推送 Oracle -- Oracle `api_gateway` 落库 `ss_motion_events`(UNIQUE event_id 幂等),`video_processor.has_motion_in_range_local()` 据此做运动预过滤(空表 fail-open 返回 None) +**运动监测支流(NAS 轮询驱动)**: +- MotionNotifier 每 60s 轮询 SS `EventCenter.Event.List`(camera_ids=2, event_types=10)→ 增量(游标)推送 `POST /api/ss/motion` 落库 `ss_motion_events`(真实 event_id/start_time/duration) +- 整段素材处理时按 `ss_motion_events` 分割运动片段(只分割已结束事件;duration=0 的进行中事件等结束后的下轮);片段分析结果的时间点(`events.ts`)为绝对时间,前端时间轴与帧图(`/api/proxy/frame`,绝对 ts − event_start_time 偏移取帧)天然兼容 **容错设计**: - Oracle 单视频串行(`max_concurrent=1`)避免多视频抢占云端配额 -- 视频分析失败(两云端均不可用)标记 `failed`,下一周期 cursor 仍包含它会被重试 +- 运动片段分析失败(两云端均不可用)标记 `failed`,下一周期 cursor 仍包含它会被重试 +- 素材分割幂等:按 `motion_event_id` 去重,片段文件已存在则跳过分割 - NAS 同步失败仅记日志,下一周期(30 分钟)自动重试,不阻塞 UI - fam-core 日志双写:stdout + `fam-core/logs/fam-core.log` @@ -178,8 +180,8 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA N | 模块 | 文件 | 职责 | |------|------|------| | API-Gateway | `api_gateway/api_gateway.py` | `GET /api/oracle/sync`(增量拉取,since+token 校验);`POST /api/oracle/people/correct`(命名校正);`POST /api/edge/chat/ask`(问答编排);`POST /api/ss/motion`(运动事件接收,token 校验,落库 `ss_motion_events`);`GET /health` | -| Video-Queue | `video_queue.py` | 生产-消费队列:生产者 30s 轮询 rclone 同步落地目录登记新视频入队(含重启恢复);消费者(`max_concurrent` 个线程)取队列调 Video-Processor;模型超时 = 原配置 ×`timeout_multiplier`;失败重试上限 `max_retries` | -| Video-Processor | `video_processor.py` | 按 `vision_order` 调适配器 `analyze_video`(整视频);首个成功即落库 Oracle `videos`+`events`+`people`;全失败标 `failed`;运动预过滤改查本地 `ss_motion_events`(`has_motion_in_range_local`),不再反向访问 NAS | +| Video-Queue | `video_queue.py` | 生产-消费队列:生产者 30s 轮询 rclone 同步落地目录登记整段素材入队(含重启恢复);消费者(`max_concurrent` 个线程)取队列调 Video-Processor;模型超时 = 原配置 ×`timeout_multiplier`;失败重试上限 `max_retries` | +| Video-Processor | `video_processor.py` | **素材→分割/片段→分析**双分支:素材按 `ss_motion_events` 已结束运动事件 ffmpeg 分割运动片段(`-c:v copy -c:a aac`,幂等去重),片段只送云端 VLM 分析;按 `vision_order` 调适配器;首个成功即落库 Oracle `videos`+`events`+`people`;全失败标 `failed` | | Person-Service | `person_service.py` | 汇总全量人物 → LLM 合并为规范名 → `set_canonical`;生成 `known_members_context` 回灌视频提示;manual 命名优先不被覆盖 | | OracleDB | `oracle_db.py` | SQLite:videos / events / people / sync_cursor / ss_motion_events;`get_sync_delta(since)` 增量导出;`record_motion_events` / `has_motion_in_range_local`(运动事件幂等落库与窗口查询) | | Model-Adapters | `model_adapters/` | `BaseModelAdapter.analyze_video(video_path, known_members_context, event_start_time)`;Gemini(Files API 整视频)/ NVIDIA(整视频 `video_url`,`num_frames=128`)/ Ollama(纯文本,不参与视频) |