[文档] 同步 v3 运动事件驱动架构 - 整段素材分割运动片段再分析(不再整段送云端): README 1.1/2.2/2.2.1/2.3/模块表, PROGRESS 运行状态+变更记录; 记录 systemd 守护部署方式

This commit is contained in:
ericwyuan
2026-08-22 12:46:33 +08:00
parent 8d6cfad6c0
commit ffcc292cce
2 changed files with 57 additions and 43 deletions

View File

@@ -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 单 workerOracle-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 单 workerOracle-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/<id>` 详情 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 |

View File

@@ -3,7 +3,7 @@
> 多模型容灾降级 + 交互式命名 + AI 对话的家庭监控系统。
> 本文档为项目需求文档与 README 的整合版按当前代码实际状态v1.0 E2E 已打通)编写。
> 最后更新2026-08-20云端多模型方案整合
> 最后更新2026-08-22运动事件驱动架构整段素材分割运动片段再分析
---
@@ -13,13 +13,13 @@
### 1.1 首期范围(已基本完成)
> **新架构 v22026-08-21 重构)**NAS 不再处理视频仅作管理后台视频分析全部上云Oracle
> **新架构 v32026-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 侧 SQLitevideos/events/people/sync_cursorNAS 侧 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 侧 SQLitevideos/events/people/ss_motion_events/sync_cursorNAS 侧 MariaDB 镜像sync_videos / sync_events / sync_people / sync_cursor+ chat_history
- **数据流向**Google 硬盘 ──rclone──► 甲骨文整段素材 ──按运动事件分割片段──► 片段云端分析 ──► Oracle SQLite ──每 30 分钟 NAS 拉取──► NAS MariaDB 镜像 ──► FAM-UI;运动事件由 NAS 轮询 SS 推送 OracleNAS → Oracle 单向)
- **AI 对话**:查 sync_events 拼上下文 → 经 FAM-Edge 问答编排Gemini → NVIDIA → 本地 Ollama 兜底)生成回答 → 返回并写 chat_history
- **人物命名**:用户命名/合并某 label → 回推 Oracle `/api/oracle/people/correct`manual 优先)→ 下一周期同步回 NASOracle 独立 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 运动监测链路v3Webhook 驱动 · 去轮询
#### 2.2.1 运动监测链路v3NAS 轮询驱动
摄像头动作事件由 **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 MotionNotifiermotion_notifier.py每 60s
│ ├─ 增量游标MariaDB sync_cursor.motion_last_event_idDB 续用/重启补推
│ ├─ 推送失败批次不前进游标(下轮重试,不丢事件)
│ └─ 定期空 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_eventsevent_id UNIQUE自动去重
│ → video_processor.has_motion_in_range_local() 查本地事件做运动预过滤
│ → OracleDB.ss_motion_eventsevent_id UNIQUE自动去重start_time/duration 为 Unix epoch
Oracle video_processor整段素材按运动事件【分割运动片段】→ 只分析片段(见 2.3
> 设计要点:甲骨文**不反向访问** NASSS 推送是 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:5000token 鉴权
- 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=<cursor>` → 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=<cursor>` → upsert 到本地 `sync_*` 镜像表 → 推进 `sync_cursor`
7. FAM-UI 读本地镜像展示(时间轴/人物/统计字段契约不变);用户命名 → `POST /api/oracle/people/correct` 回推 Oracle下一周期同步生效
**运动监测支流(v3Webhook 驱动 · 去轮询**
- 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` | SQLitevideos / 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)`GeminiFiles API 整视频)/ NVIDIA整视频 `video_url``num_frames=128`/ Ollama纯文本不参与视频 |