Files
sentinel-home-ai/README.md

758 lines
44 KiB
Markdown
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.
# Sentinel Home AI — 家庭多模态智能监控系统
> 多模型容灾降级 + 交互式命名 + AI 对话的家庭监控系统。
> 本文档为项目需求文档与 README 的整合版按当前代码实际状态v1.0 E2E 已打通)编写。
> 最后更新2026-08-22运动事件驱动架构整段素材分割运动片段再分析
---
## 1. 项目简介与设计目标
系统持续分析家庭监控摄像头Synology Surveillance Station录制的视频抽取关键帧后调用视觉语言模型VLM分析画面中的人物、动作、衣着落库为结构化事件用户通过 Web UI 浏览事件、给"人物A/B/C"命名(批量回溯历史记录)、以及用自然语言向 AI 询问"汤圆今天干嘛了"。
### 1.1 首期范围(已基本完成)
> **新架构 v32026-08-22 重构:运动事件驱动)**NAS 不再处理视频,仅作管理后台 + 运动事件推送视频分析全部上云Oracle且**不再分析整段视频**——整段素材按 SS 运动事件**分割成运动片段**后只分析片段。
- **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 合并为规范名 → 回灌视频提示
### 1.2 不在首期范围(推迟 v1.1+
- Nginx 静态服务Video-Server 用 Flask `send_from_directory` 替代,且推送模式下已不再必需)
- 资源保护策略 BCPU/内存过载返回 503
- 心跳监控(用整体超时 + 僵尸任务回收兜底)
- API 业务接口鉴权(`/api/edge/*``/api/core/*` 不加鉴权)
- Cron 兜底清理(`finally` 清理足够)
- daily_summaries 每日摘要(表已建,逻辑未实现)
### 1.3 已知遗留问题v1.1 优先解决)
| 问题 | 现状 | 影响 |
|------|------|------|
| **llava-phi3 多图单请求失效(已替换)** | 原本地模型 llava-phi3 `analyze_frames` 多图单请求输出长度仅 3~4**已替换为 qwen2.5:7b**(纯文本模型,专职问答兜底) | 历史遗留描述,新方案下视觉走云端,本地不参与视觉分析,该问题随之消除 |
| **吞吐不足** | 历史本地模型全流程约 19 分钟(关键帧筛选 10min + VLM 5.5min + 融合 3min30min/360MB 视频实测 929s | 已切换到云端 VLM 直出结构化 JSON无本地融合步骤吞吐瓶颈转移至云端调用延迟整体明显改善 |
| **Tailscale 端口不通** | NAS tailscaled 以 userspace 模式运行(无 TUN 网卡Oracle 无法反向访问 NAS当前 NAS→Oracle 走公网 IP | 推送模式已规避反向访问,但流量走公网 |
| **历史视频积压(已处理)** | 正式目录 `/volume1/surveillance/Generic_ONVIF-001` 原有约 285 个历史视频(~100GB已切回生产目录并 forward-only 处理:历史任务占位 FAILEDscheduler dedup 自动跳过,仅处理新增视频 | 已解决;生产目录已生效,新视频正常入链 |
**规划方向(新框架)****云端大模型负责视觉识别与结构化输出、本地大模型仅做智能问答兜底**。按任务类型分工:
| 任务 | 执行方 | 模型 | 说明 |
|------|--------|------|------|
| **视觉分析 + 结构化输出**(看图识人/动作/衣着 → 直出 JSON | 云端 | Gemini → NVIDIA NIMfallback 降级) | 云端 VLM 直接产出 `global_summary` / `entities_json` / `frame_details`Edge 仅做**格式化校验**后直存 NAS**本地模型不介入** |
| **结果格式化**(云端 JSON → 入库 schema | Edge 进程 | 无模型调用 | `format_cloud_result`:字段归一化、补 `source_providers`/`compute_provider`、推导 `entities`、缺失 `global_summary` 时事实拼接;纯数据转换,非 LLM 二次汇总 |
| **AI 对话**"汤圆今天干嘛了" | 云端优先 + 本地兜底 | Gemini → NVIDIA NIM → 本地 Ollama | 两云端任一成功即用;**仅当 Gemini 与 NVIDIA 都失败**才回退本地 Ollama qwen2.5:7b |
- **Google Gemini**`gemini-flash-latest`API Key 已验证,支持多图单请求,视觉 + 问答均参与)
- **NVIDIA NIM**`meta/llama-3.2-11b-vision-instruct`OpenAI 兼容 API云端 GPU 推理,单请求限 1 图故逐帧调用;视觉 + 问答均参与)
- **本地 Ollama**qwen2.5:7b**仅参与智能问答,且仅作兜底**:视觉链路两云端全失败 → 任务 FAILED 走重试,**绝不回退本地模型做视觉/融合**
Orchestrator 视觉阶段按 `fallback` 模式顺序降级Gemini → NVIDIA NIM云端模型直出结构化 JSON 后由 `format_cloud_result` 格式化。问答阶段按 `gemini → nvidia → ollama` 顺序,仅末位本地模型作兜底。
---
## 2. 系统架构
### 2.1 物理节点与部署
| 节点 | 角色 | 硬件 | IP | 服务与端口 |
|------|------|------|-----|-----------|
| Synology NAS | FAM-Core + FAM-UI + 数据库 | DS220+ (Geminilake), DSM 7 | 家庭局域网 192.168.50.64 / Tailscale 100.70.234.39 | FAM-Core :8000, FAM-UI :8501, MariaDB :3306, Surveillance Station |
| Oracle Cloud | FAM-Edge | Ampere A1 2C12G ARM64无 GPU, Ubuntu 20.04 | 公网 129.146.203.203 / Tailscale 100.74.137.126 | FAM-Edge :5000, Ollama :11434仅本地 |
| 家庭网络 | 用户入口 | 普通终端 | 192.168.50.0/24 | 浏览器访问 `http://192.168.50.64:8501` |
**网络要点(推送模式)**
- 服务间通信只有两条出站:**NAS → Oracle 公网 IP:5000**(① HTTP 上传视频 ② `POST /api/ss/motion` 推送运动侦测事件。Edge 不需要反向访问 NAS无视频拉取
- **Surveillance Station → NAS 本机 Webhook**:摄像头动作经 SS「行動規則/Webhook」实时 `POST :8000/api/ss/webhook`(同机 127.0.0.1,无需外网),由 FAM-Core 映射后推送 Oracle。这是 NAS 入站,非 Edge 反向访问
- Oracle 端 Ollama 端口 11434 不对外暴露,聊天请求经 FAM-Edge `/api/edge/chat` 代理转发
- Tailscale 两节点已安装在线,但 NAS tailscaled 为 userspace 模式且防火墙端口不通,暂走公网 IP
### 2.2 部署拓扑与数据流(新架构 v2Oracle 分析 + NAS 镜像)
```
┌──────────── Google 硬盘 ────────────┐
│ oraclenas@...gserviceaccount.com │
│ (Cloud Sync 落盘目录) │
└──────────────┬───────────────────────┘
│ rclone 定时同步systemd timer
┌──────────────────────── Oracle Cloud (129.146.203.203) ────────────────────────┐
│ FAM-Edge (Flask :5000) │
│ ├─ 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 / 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 分钟)
┌─────────────────────────────── NAS (192.168.50.64) ────────────────────────────┐
│ FAM-Core (Flask :8000) │
│ ├─ Oracle-Sync: 唯一后台线程,拉增量写 MariaDB 镜像 + 维护 sync_cursor │
│ ├─ Chat-Handler: /api/chat/ask查 sync_events 拼上下文 → 走 Oracle 编排) │
│ ├─ Member-Manager: /api/member/name|merge回推 Oracle + 即时拉回) │
│ ▼ │
│ MariaDB (sentinel_home_ai): sync_videos / sync_events / sync_people / │
│ sync_cursor / chat_history │
│ FAM-UI (Streamlit :8501) 读同步镜像 │
└─────────────────────────────────────────────────────────────────────────────────┘
```
#### 2.2.1 运动监测链路v3NAS 轮询驱动)
摄像头动作事件由 **NAS 端 MotionNotifier 轮询 Surveillance Station 事件列表**获取简单稳定事件不遗漏Webhook 端点在代码中保留为可选低延迟补充):
Surveillance Station (NAS 本机)
│ SYNO.SurveillanceStation.EventCenter.Event method=List
│ camera_ids=2, event_types=10, start_time/end_time下划线风格
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自动去重start_time/duration 为 Unix epoch
Oracle video_processor整段素材按运动事件【分割运动片段】→ 只分析片段(见 2.3
> 设计要点:甲骨文**不反向访问** 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 主链路时序(新架构 v3运动事件驱动
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下一周期同步生效
**运动监测支流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 仍包含它会被重试
- 素材分割幂等:按 `motion_event_id` 去重,片段文件已存在则跳过分割
- NAS 同步失败仅记日志下一周期30 分钟)自动重试,不阻塞 UI
- fam-core 日志双写stdout + `fam-core/logs/fam-core.log`
---
## 3. 模块设计
### 3.1 FAM-CoreNAS 端)
> NAS 不再处理视频,仅作管理后台。唯一常驻后台线程是 Oracle-Sync。
| 模块 | 文件 | 职责 |
|------|------|------|
| Oracle-Sync | `oracle_sync/oracle_sync.py` | 唯一后台线程:每 30 分钟 `GET /api/oracle/sync?since=<cursor>&token=` 拉增量 → upsert 到 `sync_videos`/`sync_events`/`sync_people` → 推进 `sync_cursor``push_name_correct()` 回推命名校正;`trigger_now()` 立即同步 |
| Chat-Handler | `chat_handler/chat_handler.py` | `/api/chat/ask``sync_events` 拼上下文 → 经 Oracle `/api/edge/chat/ask` 问答编排Gemini→NVIDIA→本地 Ollama 兜底)→ 写 chat_history |
| Member-Manager | `member_manager/member_manager.py` | `/api/member/unnamed` / `/api/member/list` / `/api/member/name` / `/api/member/merge`;命名/合并回推 Oracle 并即时拉回本地镜像 |
| MotionNotifier | `motion_notifier/motion_notifier.py` | 运动事件映射/发送服务:**轮询已关闭**`poll_enabled=false`);启动时一次性拉取 SS 摄像头「名→id」映射并以 config 兜底;提供 `build_event_from_webhook()` 将 SS Webhook 字段归一化为事件 |
| Motion-Webhook | `motion_bp.py` | `POST /api/ss/webhook` 接收 SS Webhook兼容 JSON/表单/单条/数组)→ 映射 → 推送 Oracle`GET /api/ss/status` 查询状态 |
| 公共层 | `db_layer.py` / `config_loader.py` / `logger.py` | PyMySQL 连接unix_socket同步镜像 CRUD文件日志 |
> 已删除Task-Scheduler / Dispatcher / Poller / Event-Receiver / Video-Server视频上传、切片、抽帧、关键帧落盘等职责全部迁移至 Oracle 端NAS CPU 占用大幅降低)。
### 3.2 FAM-EdgeOracle 端)
> 整视频分析,不切片、不抽帧、不依赖 OpenCV 人脸。
| 模块 | 文件 | 职责 |
|------|------|------|
| 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` | **素材→分割/片段→分析**双分支:素材按 `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纯文本不参与视频 |
| QA-Orchestrator | `qa.py` | 遍历所有适配器 `chat()`Gemini→NVIDIA→Ollama 三级降级(仅问答) |
### 3.3 FAM-UINAS 端)
Streamlit 应用(`fam-ui/src/app.py`),侧边栏切换页面(均读本地同步镜像):
| 页面 | 功能 |
|------|------|
| 🕒 事件时间轴 | 视频会话列表(按处理后时间倒序)+ 选中会话的事件时间线(时间点 + 描述 + 人物/关注徽章,无帧图) |
| 💬 AI 对话 | 输入框 + 调 `/api/chat/ask`;按 queried_person 预设快捷提问 |
| 📝 对话历史 | chat_history 倒序展示 |
| 👤 人物管理 | 按规范名/标签聚合,命名/合并(回推 Oracle不再展示帧照片 |
| 📈 统计图表 | 模型来源占比 / 关注事件 / 同步状态 |
---
## 4. 数据库设计
库名 `sentinel_home_ai`MariaDB 10.11.11utf8mb4。完整 DDL 见 `scripts/ddl.sql`
### 4.1 表清单
> 新架构 v2Oracle 侧用 SQLite`videos`/`events`/`people`/`sync_cursor`NAS 侧 MariaDB 仅保留 **同步镜像表 + 问答历史**。`process_tasks`/`monitor_events`/`event_details`/`family_members` 等旧表已不再写入(保留历史数据,未删除)。
**OracleSQLite`oracle_db.py`**
| 表 | 用途 | 关键字段 |
|----|------|---------|
| `videos` | 视频会话(每视频 1 行) | id, filename(UNIQUE), camera_name, status, summary_json, events_json, people_json, compute_provider, event_start_time, updated_at |
| `events` | 视频内时间点事件 | id, video_id, ts, description, person_list_json, is_attention_event |
| `people` | 规范人物Oracle 维护) | id, label(UNIQUE), canonical_name, appearances, source(llm/manual) |
| `sync_cursor` | 同步游标 | key, value上次 server_time |
**NASMariaDB同步镜像`scripts/ddl.sql`**
| 表 | 用途 | 关键字段 |
|----|------|---------|
| `sync_videos` | 视频会话镜像(对齐 Oracle videos | id, filename, camera_name, status, summary_json, events_json, people_json, compute_provider, processed_at |
| `sync_events` | 事件镜像(对齐 Oracle events | id, video_id, ts, description, person_list_json, is_attention_event |
| `sync_people` | 人物镜像(对齐 Oracle people | id, label, canonical_name, appearances, source |
| `sync_cursor` | 同步游标 | key='last_since', value |
| `chat_history` | AI 问答记录 | chat_id, user_question, ai_answer, context_summary, queried_date, queried_person |
### 4.2 表关系
```
Oracle: videos (1) ─── (N) events people 独立label/canonical_name
NAS 镜像: sync_videos (1) ─── (N) sync_events sync_people 独立
chat_history 独立表(问答上下文摘要留存)
```
命名回溯:用户命名某 `label``POST /api/oracle/people/correct``canonical_name`manual 优先)→ 下一周期同步回 NAS `sync_people`Oracle `person_service` 用规范名回灌视频提示,后续事件 `person_list_json` 直接带真名。
### 4.3 compute_provider
- `sync_videos.compute_provider`:字符串,记录该视频实际成功调用的视觉模型(`gemini` / `nvidia`);本地 Ollama 不参与视频分析,不会出现在该字段
- 问答链路Gemini→NVIDIA→Ollama 兜底)的 provider 体现在 `/api/edge/chat/ask` 响应的 `provider` 字段
### 4.4 兼容性注意
- MariaDB 10.11 严格模式:**空字符串不能插 DATETIME 列**1292 错误)。同步表时间字段统一用 `VARCHAR(32)` 文本存储 Oracle 的 ISO 字符串,规避类型转换问题
- MariaDB 不支持 MySQL 的 `$[*]` JSON 通配路径,人物统计按 `person_list_json LIKE '%name%'` 字符串匹配在 Python 层完成
---
## 5. API 规范(实际实现)
### 5.1 FAM-EdgeOracle :5000
**GET /api/oracle/sync**NAS 每 30 分钟拉增量,新架构主接口)
- 请求:`?since=<ISO 文本>&token=<ORACLE_SYNC_TOKEN>``since` 为空拉全量)
- 响应200
```json
{
"videos": [
{"id": 1, "filename": "2026-08-21_081500.mp4", "camera_name": "客厅",
"status": "done", "summary_json": "...", "events_json": "[...]",
"people_json": "[...]", "compute_provider": "gemini",
"event_start_time": "2026-08-21 08:15:00", "updated_at": "2026-08-21 08:40:12"}
],
"events": [
{"id": 10, "video_id": 1, "ts": "00:01:23", "description": "汤圆在客厅玩耍",
"person_list_json": "[\"汤圆\"]", "is_attention_event": 0, "updated_at": "2026-08-21 08:40:12"}
],
"people": [
{"id": 1, "label": "人物A", "canonical_name": "汤圆", "source": "manual",
"appearances": 12, "updated_at": "2026-08-21 08:41:00"}
],
"server_time": "2026-08-21 08:41:30"
}
```
- 401token 校验失败
**POST /api/oracle/people/correct** — 命名校正回推:`{"label":"人物A","canonical_name":"汤圆","token":...}`manual 优先,不被 LLM 覆盖)→ `{"status":"ok"}`
**POST /api/edge/chat/ask** — 智能问答编排FAM-Core Chat-Handler 调用):请求 `{"prompt","max_tokens"}` → 响应 `{"answer","provider"}`;内部按 Gemini → NVIDIA → 本地 Ollama 顺序,仅两云端都失败才用本地兜底
**GET /health** — 服务状态(含已处理视频数)
### 5.2 FAM-CoreNAS :8000
| 端点 | 方法 | 说明 |
|------|------|------|
| `/health` | GET | 服务健康 |
| `/api/status` | GET | Oracle-Sync 同步状态running / last_sync_at / last_error / cursor / last_count |
| `/api/chat/ask` | POST | 用户问答:`{"question","queried_person","queried_date"}``{"answer","context_summary","chat_id"}`(上下文来自 sync_events |
| `/api/chat/history` | GET | 对话历史(`?date=``?person=&limit=` |
| `/api/member/unnamed` | GET | 未命名人物列表label / 出现次数 / 首见时间) |
| `/api/member/list` | GET | 全部人物label + canonical_name + 是否命名) |
| `/api/member/name` | POST | 命名:`{"label","canonical_name"}` → 回推 Oracle 并即时拉回本地镜像 |
| `/api/member/merge` | POST | 合并:`{"source","target"}` → 将 source 并入 target 身份(统一 canonical_name |
| `/api/ss/webhook` | POST | 接收 Surveillance Station Webhook参数 `event_time`/`device_name`/`event_name`/`server_name`/`thumbnail_url`);映射后推送 Oracle `/api/ss/motion` |
| `/api/ss/status` | GET | 运动监测状态:`poll_enabled` / `camera_loaded` / `camera_map` / `pushed_total` / `last_error` |
> 已删除:`/api/core/callback/event`、`/media/<path>`(视频处理职责已迁移至 Oracle
### 5.3 云端结构化输出 JSON Schema
整视频直传云端 VLM模型直接产出结构化 JSON`analyze_video` 返回):
```json
{
"global_summary": "客厅监控摘要……",
"events": [
{"timestamp": "00:01:23", "description": "汤圆在客厅玩耍",
"people": ["汤圆"], "is_attention_event": false}
],
"people_mentioned": ["汤圆"]
}
```
- Gemini 用 Files API 上传整视频后 `generateContent`NVIDIA 用整视频 `video_url` + `num_frames=128`(模型内部自行采样帧),均不切片、不抽帧、不依赖 OpenCV
- `person_service` 汇总全量 `people_mentioned` → LLM 合并为规范名 → 生成 `known_members_context` 回灌后续视频提示,使模型用真名指代
- `action` / 描述由 AI 自由生成无枚举过滤,`is_attention_event` 由 AI 自行判断
---
## 6. 关键技术
### 6.1 视频预处理(自适应关键帧)
- **粗抽候选帧**FFmpeg 快速 seek逐帧 `ffmpeg -ss <ts> -frames:v 1`),替代 fps 滤镜全解码30min 视频从 180s+ 降到 33s6-8x 提速)
- **帧数自适应**:候选帧 `clamp(时长分钟×2, 30, 120)`;关键帧上限 `clamp(时长/150s, 8, 30)`30min→12 帧60min→24 帧,封顶 30
- **帧差筛选**OpenCV MSE首末帧必选MSE > 阈值 500 的保留,不足 min_key_frames=5 补足
- **压缩**:长边 > 1024px 才缩放JPEG 质量 80
- **异常兜底**ffprobe 失败退化为 60s 间隔抽帧;帧差异常退化为等距 5 帧
### 6.2 本地 Ollama仅智能问答兜底ARM CPU
本地 Ollamaqwen2.5:7b纯文本模型**不参与视觉分析、不参与云端结果融合**。它只在**智能问答**场景下、且 Gemini 与 NVIDIA 两云端模型都失败时才被启用作为兜底。视觉分析与结构化输出全部由云端模型承担。以下参数作为问答任务的调优依据保留。
| 参数 | 值 | 依据 |
|------|-----|------|
| `OLLAMA_KEEP_ALIVE=-1` | 模型常驻内存 | 消除 55s 冷启动(常驻约 4.3GB12GB 内存够用) |
| `num_predict=512` | 限制生成 token | ARM 约 5 tok/s过长生成会拖慢问答响应 |
| 视觉/模型 timeout | 600s | 实测 1024px 帧视觉编码 ~36s + 生成 ~12s/60token问答链路改用 config 中各模型 timeout |
| gunicornEdge | `--timeout 1800` | 同步分析模式,默认 30s 会杀 worker |
| push_timeoutNAS | 1800s | 覆盖最坏情况30min 视频实测 929s |
**已知问题**:原 llava-phi3 多图单请求基本失效N 张图一次调用输出长度仅 3~4。**已替换为 qwen2.5:7b**(纯文本模型,专职问答兜底,不涉及视觉多图问题;视频分析已全部由云端 VLM 承担)。
### 6.3 模型适配器架构
**抽象基类 `BaseModelAdapter`**:定义统一接口,每个模型实现自己的适配器。
| 方法 | 职责 |
|------|------|
| `__init__(provider_name, config)` | 读取 api_key / base_url / model_name / timeout / 熔断器配置 |
| `health_check() -> bool` | 轻量请求验证连通性与 Key 有效性 |
| `analyze_frames(frame_paths, frame_timestamps, known_members_context) -> Optional[str]` | 接收多张关键帧路径 + 时间戳 + 已知成员特征,返回 VLM 文本描述(失败返回 None |
| `get_timeout() -> int` | 返回该模型单次调用超时阈值 |
| 熔断器实例 | 每个云端模型独立熔断器 |
**模型清单由 `config.yaml` 的 `models` 数组动态决定**,新增模型 = 实现适配器 + 配置加一项,主流程不动。
**已实现 / 规划的适配器**(按 `role` 区分职责):
| provider | 适配器类 | role | SDK / 协议 | 状态 |
|----------|---------|------|------------|------|
| `ollama` | `OllamaAdapter` | **text** | requests 直调 REST `/api/chat` | 已实现 |
| `gemini` | `GeminiAdapter` | **vision** | requests 直调 REST `:generateContent` | 待实现 |
| `nvidia` | `NvidiaVisionAdapter` | **vision** | **openai SDK**NIM 兼容 OpenAI API 规范) | 待实现 |
**role 语义**
- `vision`:参与视觉分析阶段,按 fallback 顺序降级,直出结构化 JSON
- `text`:仅参与智能问答(`usage: qa_fallback`),且为 Gemini/NVIDIA 都失败时的兜底,不参与视觉分析、不参与云端结果融合
**NvidiaVisionAdapter 关键实现**`fam_edge/adapters/nvidia_adapter.py`
- 基于 `openai` Python SDK`base_url=https://integrate.api.nvidia.com/v1``api_key``${NVIDIA_API_KEY}` 展开
- `health_check`:调 `client.models.list()` 轻量验证 Key
- `analyze_frames`:构建 OpenAI 标准 `content` 列表1 个 text item + N 个 `image_url` item图片以 `data:image/jpeg;base64,...` 格式内联),调 `chat.completions.create``temperature=0.2``max_tokens=1024`
- prompt 中文模板:要求按时间顺序分析人物数量/衣着/微动作/互动,重点标注异常高危行为
**Orchestrator 调度模式**`config.yaml``orchestrator.mode`
| 模式 | 行为 |
|------|------|
| `fallback`(默认) | 顺序降级:按 models 数组顺序依次尝试,首个健康且返回非 None 的模型结果即采用;失败则降级到下一个;全部失败返回 503 |
| `ensemble` | 并行交叉验证:所有 enabled 模型并行调用,结果一致 → 可信度高;多数派为准;仅单一模型描述 → `source_providers` 仅含该模型 |
**视觉分析降级链路fallback 模式,仅云端 role=vision 模型)**
```
Gemini (gemini-flash-latest, role=vision) —— 多图单请求直出结构化 JSON
│ 失败 / 熔断 OPEN / 超时
NVIDIA NIM (llama-3.2-11b-vision-instruct, role=vision) —— 逐帧结构化聚合
│ 失败 / 熔断 OPEN / 超时
任务 FAILED走重试绝不回退本地 Ollama —— 本地仅负责问答兜底)
```
**格式化阶段(无模型调用,仅数据转换)**
```
云端成功 provider 直出的结构化 JSONframe_details / 可选 global_summary / entities_json
format_cloud_result字段归一化 → 补 source_providers=[provider] / compute_provider=[provider]
→ 缺失 entities_json 由 frame_details 推导 → 缺失 global_summary 则事实拼接 → schema 校验
合法入库结构,随响应返回 NAS 直接落库(本地模型不介入)
```
**智能问答降级链路chat 场景)**
```
Gemini (role=vision, 也参与问答)
│ 失败 / 熔断 OPEN / 超时
NVIDIA NIM (role=vision, 也参与问答)
│ 失败 / 熔断 OPEN / 超时
本地 Ollama (qwen2.5:7b, role=text, usage=qa_fallback) —— 仅当两云端都失败才启用
│ 失败
返回"所有模型均不可用"
```
**熔断器策略**(按 provider 独立,仅云端模型启用):
| provider | role | threshold | cooldown | enabled |
|----------|------|-----------|----------|---------|
| Gemini | vision | 3 次连续失败 | 600s | true |
| NVIDIA NIM | vision | 3 次连续失败 | 600s | true |
| Ollama | text | — | — | false本地不熔断 |
**健康探测**Ollama `GET /api/tags`Gemini `GET /v1/models?key=...`NVIDIA `client.models.list()`。视觉模型全部不健康返回 503。
**多模型标签兼容**`compute_provider``event_details.source_providers` 的 JSON 数组值新增 `"nvidia"` 标签(与 `"ollama"` / `"gemini"` 并列);`validate_schema` 校验非空数组。
**执行可观测性**Orchestrator 在每次调用记录各模型的 `provider / 耗时 / success` 三元组到日志,并落库到 `monitor_events.compute_provider` 数组。
### 6.4 任务可靠性
- 状态机 PENDING → PROCESSING → SUCCESS/FAILED退避重试 `min(60×(retry+1)×2, 600)`smax_retries=3
- **僵尸任务回收**PROCESSING 超过 `push_timeout + 120s` 自动重置 PENDING应对 fam-core 重启 / Edge 重启 / in-flight 请求丢失)
- `failure_stage` ENUMdownload / extract / vlm_visual / vlm_fusion / callback
---
## 7. 性能基准(实测)
### 7.1 单图推理(原 llava-phi3, Oracle ARM 2C12G — 已替换为 qwen2.5:7b
| 场景 | 总耗时 |
|------|--------|
| 冷启动(首次加载 2.9GB 模型) | 109s已由 keep-alive 消除) |
| 预热 + num_predict=30 | **4.3s** |
| 1024px 真实帧 + num_predict=60 | ~48.6s(视觉编码 36s 固定成本 + ~5 tok/s |
### 7.2 真实视频全流程30min / 360MB / 1080p H.264
| 步骤 | 耗时 |
|------|------|
| 视频上传 NAS→Oracle公网5.1 MB/s | 70s |
| FFmpeg 快速 seek 抽 60 候选帧 | 33s |
| OpenCV 关键帧筛选→12 帧) | 5s |
| 压缩 | 0.6s |
| Ollama 视觉分析12 帧) | 820savg 68s/帧0.9 tok/s |
| **总计** | **929s15.5min** |
### 7.3 E2E 验证2026-08-20
task_id=28930s 测试片段)全链路打通:推送 5.7MB → Edge 分析 1129s → 同步返回 → `monitor_events` event_id=1 + `event_details` 2 条落库,`event_end_time` 推算正确start+29s
---
## 8. 部署说明
### 8.1 部署位置与启动命令
| 组件 | 节点 | 路径 | 启动 |
|------|------|------|------|
| FAM-Core | NAS | `/volume1/web/sentinel-home-ai/fam-core/` | `./venv/bin/gunicorn --chdir <路径> -w 1 -b 0.0.0.0:8000 --timeout 120 --daemon --pid /tmp/fam-core-gunicorn.pid src.fam_core.app:app` |
| FAM-UI | NAS | `/volume1/web/sentinel-home-ai/fam-ui/` | `./venv/bin/streamlit run src/app.py`headless, :8501 |
| FAM-Edge | Oracle | `/opt/fam-edge/` | `venv/bin/gunicorn -w 1 -b 0.0.0.0:5000 --timeout 1800 src.fam_edge.app:app`(日志 `/tmp/fam-edge.log` |
| Ollama | Oracle | systemd 托管 | 环境变量 `OLLAMA_KEEP_ALIVE=-1` |
### 8.2 依赖
- **FAM-Core/UINAS, Python 3.10 venv**Flask, Gunicorn, **PyMySQL**45KB 纯 Python 替代 19MB mysql-connector, PyYAML, requestsFAM-UI 另需 Streamlit + pandas
- **FAM-EdgeOracle, Python 3.8+ venv**Flask, Gunicorn, requests, PyYAML, opencv-python, numpy, **openai**NVIDIA NIM 兼容 OpenAI API 规范,复用同一 SDKGemini 用 requests 直调 REST不依赖 google-generativeai SDK
- **系统级**FFmpeg两端、Ollama + qwen2.5:7bOracle、MariaDB 10.11NAS
### 8.3 配置文件要点
**fam-core/config/config.yaml**NAS新架构 v2 —— 仅同步 + 问答):
```yaml
server:
port: 8000
database: # MariaDBunix_socket 优先)
unix_socket: "/run/mysqld/mysqld10.sock"
oracle_sync: # 唯一后台线程配置
base_url: "http://129.146.203.203:5000"
token: "${ORACLE_SYNC_TOKEN}" # 与 Oracle 端 sync_api.token 一致
interval_sec: 1800 # 每 30 分钟拉一次增量
timeout: 120
chat_handler:
qa_url: "http://129.146.203.203:5000/api/edge/chat/ask" # 问答统一走 Oracle 编排
timeout: 120
```
**fam-edge/config/config.yaml**Oracle整视频分析 + 同步 + 人物服务):
```yaml
server:
port: 5000
gdrive_sync: # rclone 同步落地目录监听
enabled: true
local_dir: "/opt/fam-edge/gdrive_videos"
watch_interval_sec: 30
camera_name: "客厅"
parse_start_from_filename: true
oracle_db:
path: "/opt/fam-edge/data/oracle.db"
sync_api:
token: "${ORACLE_SYNC_TOKEN}" # NAS 拉取鉴权(与 NAS oracle_sync.token 一致)
person_service:
schedule_interval_sec: 1800 # 每 30 分钟重新汇总人物
model: "gemini"
video_processing:
max_concurrent: 1 # 单视频串行,避免抢占云端配额
timeout: 900
vision_order: ["gemini", "nvidia"]
models:
- provider: "gemini"
role: "vision"
model_name: "gemini-flash-latest"
api_key: "${GEMINI_API_KEY}"
timeout: 600
- provider: "nvidia"
role: "vision"
model_name: "nvidia/nemotron-nano-12b-v2-vl" # 整视频 video_url 输入(内部采样帧)
base_url: "https://integrate.api.nvidia.com/v1"
api_key: "${NVIDIA_API_KEY}"
timeout: 600
- provider: "ollama"
role: "text"
usage: "qa_fallback" # 仅智能问答兜底,不参与视频
model_name: "qwen2.5:7b"
``` api_key: "${NVIDIA_API_KEY}"
timeout: 20
circuit_breaker:
enabled: true
threshold: 3
cooldown: 600
# 3. 本地 Ollama纯文本仅 Q&A 兜底,不参与视觉分析、不参与云端结果融合)
- provider: "ollama"
role: "text" # 仅问答兜底
usage: "qa_fallback" # Gemini/NVIDIA 都失败时才启用
enabled: true
model_name: "qwen2.5:7b"
base_url: "http://localhost:11434"
timeout: 120
num_predict: 512
circuit_breaker:
enabled: false
```
**环境变量**Oracle 节点,写入 `~/.bashrc` 或 systemd 环境变量文件):
```bash
export GEMINI_API_KEY="AQ.Ab8RN6I0l8hC7hLnNHRY6qOXdch5CTWczDNlS4c1XrneGHipUQ"
export NVIDIA_API_KEY="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"
```
**NVIDIA NIM 单图连通性验证**
```bash
pip3 install openai
python3 -c "
import os
from openai import OpenAI
client = OpenAI(
base_url='https://integrate.api.nvidia.com/v1',
api_key=os.environ['NVIDIA_API_KEY']
)
response = client.chat.completions.create(
model='meta/llama-3.2-11b-vision-instruct',
messages=[{'role': 'user', 'content': 'Hello, are you ready?'}],
max_tokens=30
)
print('NVIDIA NIM 连接成功:', response.choices[0].message.content)
"
```
### 8.4 运维注意事项
- NAS 部署目录**不是 git 仓库**(文件拷贝部署),同步代码用 stdin 管道:`ssh ... "cat > 远端路径" < 本地文件`
- NAS scp 子系统被禁用,同样用 stdin 管道传文件
- 远端 kill gunicorn 时 pkill/pgrep 会匹配 SSH 自身命令行导致断连,用 `pgrep -f 'gunicorn -w [1]'` 字符类技巧或 PID 文件
- fam-core 启动模块路径是 `src.fam_core.app:app`(不是 `fam_core.app:app`
- Edge 单 worker 处理任务期间 `/health` 可能不响应,属正常
---
## 9. 快速开始
```bash
# 1. 初始化数据库NAS
python scripts/init_db.py
# 2. 启动 FAM-CoreNAS
cd fam-core && gunicorn -w 1 -b 0.0.0.0:8000 --timeout 120 src.fam_core.app:app
# 3. 启动 FAM-EdgeOracle
cd fam-edge && gunicorn -w 1 -b 0.0.0.0:5000 --timeout 1800 src.fam_edge.app:app
# 4. 启动 FAM-UINAS
cd fam-ui && streamlit run src/app.py
# 或使用脚本
./scripts/start_core.sh && ./scripts/start_edge.sh && ./scripts/start_ui.sh
```
---
## 10. 服务器访问信息
### 10.1 Synology NASFAM-Core + FAM-UI + MariaDB
| 项目 | 值 |
|------|-----|
| IP | 192.168.50.64 |
| SSH 端口 | 2222scp 禁用,用 stdin 管道传文件) |
| SSH 用户 / 密码 | ericwyuan / iLoveJava5 |
| 系统 | Synology DS220+ (Geminilake), DSM 7 |
| Tailscale IP | 100.70.234.39userspace 模式,端口不通待修) |
| 登录命令 | `ssh -p 2222 ericwyuan@192.168.50.64` |
### 10.2 MariaDBNAS
| 项目 | 值 |
|------|-----|
| 版本 | MariaDB 10.11.11 |
| Socket | /run/mysqld/mysqld10.sock |
| Root 密码 | iLoveJava5! |
| 数据库名 | sentinel_home_ai |
| 连接 | `/usr/local/mariadb10/bin/mysql -S /run/mysqld/mysqld10.sock -u root -p`(非交互 PATH 下用 fam-core venv 的 PyMySQL + unix_socket 查询) |
### 10.3 Oracle CloudFAM-Edge + Ollama + FFmpeg
| 项目 | 值 |
|------|-----|
| 公网 IP | 129.146.203.203 |
| SSH 用户 | ubuntu密钥 `~/.ssh/oracle_sentinel` |
| 系统 | aarch64 (Ampere A1 2C12G), Ubuntu 20.04 LTS |
| Tailscale | 100.74.137.126(已安装在线,与 NAS 端口不通) |
| 登录命令 | `ssh -i ~/.ssh/oracle_sentinel ubuntu@129.146.203.203` |
### 10.4 Gitea 代码仓库
| 项目 | 值 |
|------|-----|
| URL | http://192.168.50.64:3000/ericwyuan/sentinel-home-ai |
| 账号 / 密码 | ericwyuan / iLoveJava5 |
### 10.5 Gemini APIGoogle AI Studio
| 项目 | 值 |
|------|-----|
| API Key | AQ.Ab8RN6I0l8hC7hLnNHRY6qOXdch5CTWczDNlS4c1XrneGHipUQ |
| 模型 | `gemini-flash-latest`生产配置v1beta 下 `gemini-1.5-flash` 会 404故用别名 |
| 端点 | https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent |
| 验证状态 | 2026-08-20 测试可用 |
```bash
curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent" \
-H 'Content-Type: application/json' \
-H 'X-goog-api-key: AQ.Ab8RN6I0l8hC7hLnNHRY6qOXdch5CTWczDNlS4c1XrneGHipUQ' \
-X POST \
-d '{"contents":[{"parts":[{"text":"Explain how AI works in a few words"}]}]}'
```
### 10.6 NVIDIA NIM 托管 API
| 项目 | 值 |
|------|-----|
| API Key | `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` |
| Base URL | `https://integrate.api.nvidia.com/v1` |
| 默认模型 | `meta/llama-3.2-11b-vision-instruct`(可替换 `qwen/qwen2-vl-72b-instruct` |
| SDK | `openai` Python SDKNIM 兼容 OpenAI API 规范,直接复用) |
| 验证状态 | 已验证2026-08-20 部署验证:`/api/edge/chat/ask` 实测 provider=nvidiaGemini 超时后 NVIDIA 兜底成功;单图连通性验证脚本见 8.3 |
**环境变量**
```bash
export NVIDIA_API_KEY="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"
```
---
## 11. 提交规范AI Agent 必读)
**工作流程(强制)**
1. **开工前**:仓库根目录 `git pull --rebase`
2. **每完成一项验收子任务**:立即 `git add` + `git commit` + `git push`,一任务一 commit不批量合并
3. **遇到阻塞**:先 commit 可工作部分message 加 `[WIP]` 前缀
4. **修 Bug**:单独 commit格式 `fix(模块): 问题简述`
**commit message 格式**
- 任务:`[阶段X.Y子任务号] 子任务名称 - 完成内容简述`
- Bug`fix(模块): 问题简述`
- 性能/文档:`perf(...)` / `docs(...)`
**禁止**:不 commit 直接继续;一次 commit 多个子任务;`git push --force`;跳过 hooks`--no-verify`)。
**自检清单**:任务对应哪一行验收标准?是否已 commit + pushmessage 是否合规?开工前是否 pull --rebase
---
## 12. 当前进度与 v1.1 计划
### 已完成(截至 2026-08-20
- 全部模块代码 + DDL + 部署脚本NAS/Oracle 双端部署运行
- 模型基准测试(原 llava-phi3 预热 4.3s PASS**已替换为 qwen2.5:7b**Ollama 常驻内存
- 架构改为推送模式NAS 上传整段视频 → Edge 同步分析 → 结果随响应返回)
- 关键帧自适应帧数 + FFmpeg 快速 seek6-8x 提速)
- FAM-Core API 全端点测试通过
- FAM-UI 部署Streamlit 1.61.1
- 真实视频性能基准30min/360MB → 929s
- **E2E 全链路打通**task 289 → SUCCESSmonitor_events/event_details 落库正确
- 可靠性加固僵尸任务回收、文件日志、datetime 空值兜底、超时按实测调整
- **架构重构(移除本地融合,云端直出直存)**
- 视频分析链路:云端 VLMGemini `gemini-flash-latest` / NVIDIA NIM `llama-3.2-11b-vision-instruct`)直出结构化 JSON → Edge `format_cloud_result` 格式化/校验(无模型调用)→ 直存 NAS DB本地 Ollama 不再参与视频摘要与融合(`run_text_fusion` 已移除)
- 智能问答链路:新增 `chat()` 方法,`run_qa` 按 Gemini → NVIDIA → 本地 Ollama 降级编排;端点 `/api/edge/chat/ask`NAS Chat-Handler 经 Edge 编排(`qa_url`),不再直连 Ollama
- 生产目录已切回 `/volume1/surveillance/Generic_ONVIF-001`285 历史视频 forward-only 占位跳过
- **双端部署验证通过**2026-08-20Oracle Edge 7 文件部署 + 重启,`/api/edge/chat/ask` 实测 provider=nvidiaGemini 超时→NVIDIA 兜底成功NAS chat_handler + config 手术式更新 + HUP 重载,插入临时上下文实测 NAS→Edge 编排链路 43s 返回并落库测试数据已清理Edge 日志确认 task 294360MB以新架构处理中、三模型健康检查全通过
详细进度见 `PROGRESS.md`
### v1.1 待办
| # | 任务 | 优先级 |
|---|------|--------|
| 1 | 单元测试JSON parser / circuit breaker / schema 校验) | 中 |
| 2 | Tailscale 修复NAS userspace 模式升级,流量不走公网) | 低 |
| 3 | daily_summaries 每日摘要 | 低 |
---
文档结束