Files
sentinel-home-ai/README.md

797 lines
50 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-UIVue3 构建产物,由 FAM-Core 托管), MariaDB :3306, Surveillance Station :5000 |
| Oracle Cloud | FAM-Edge | Ampere A1 2C12G ARM64无 GPU, Ubuntu 20.04 | 公网 129.146.203.203 / Tailscale 100.74.137.126 | FAM-Edge :5000systemd 守护), Ollama :11434仅本地 |
| 家庭网络 | 用户入口 | 普通终端 | 192.168.50.0/24 | 浏览器访问 `http://192.168.50.64:8000` |
**网络要点(推送模式)**
- 服务间通信只有两条出站:**NAS → Oracle 公网 IP:5000**(① `GET /api/oracle/sync` 拉增量 + `POST /api/oracle/people/correct` 命名回推 ② `POST /api/ss/motion` 推送运动侦测事件。Edge 不需要反向访问 NAS
- **运动事件获取NAS 轮询驱动)**FAM-Core MotionNotifier 每 60s 轮询本机 Surveillance Station 事件列表 API`SYNO.SurveillanceStation.EventCenter.Event`),增量推送 Oracle。`/api/ss/webhook` 端点在代码中保留为可选低延迟补充SS 行動規則 Webhook 未配置则不触发)
- Oracle 端 Ollama 端口 11434 不对外暴露,聊天请求经 FAM-Edge `/api/edge/chat/ask` 代理转发
- 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增量镜像+ MotionNotifier轮询 SS 运动事件)。
| 模块 | 文件 | 职责 |
|------|------|------|
| 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=true`):每 `poll_interval_sec`(60s) 查 SS `EventCenter.Event.List`camera_ids/event_types=10→ 增量推送 Oracle `/api/ss/motion`;游标存 MariaDB重启续用/补推停机期间事件,失败批次不前进);心跳线程定期空 POST 证明链路存活;启动时拉取 SS 摄像头「名→id」映射并配置兜底 |
| Motion-BP | `motion_bp.py` | `POST /api/ss/webhook` 可选低延迟补充SS 行動規則 Webhook 未配置则不触发,兼容 JSON/表单/单条/数组)→ 映射 → 推送 Oracle`GET /api/ss/status` 查询状态 |
| UI-API | `ui_api.py` | `/api/ui/*` 只读接口videos/stats/people/people-clips/attention-events/named-members/model-stats/service-status供 Vue 前端渲染 |
| 公共层 | `db_layer.py` / `config_loader.py` / `logger.py` | PyMySQL 连接unix_socket同步镜像 CRUD`get_sync_people_clips()` 人物→运动片段查询;文件日志 |
> 已删除Task-Scheduler / Dispatcher / Poller / Event-Receiver / Video-Server视频上传、切片、抽帧、关键帧落盘等职责全部迁移至 Oracle 端NAS CPU 占用大幅降低)。
### 3.2 FAM-EdgeOracle 端)
> 整段素材按运动事件分割运动片段,**只分析运动片段**(不再整段送云端)。
| 模块 | 文件 | 职责 |
|------|------|------|
| 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 /api/oracle/frame|avatar`(帧/头像);`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` 保留音频,`motion_event_id` 幂等),片段只送云端 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`motion_event_id`/`camera_id` 列)/ events / people / sync_cursor / ss_motion_events`get_sync_delta(since)` 增量导出;`record_motion_events` / `get_motion_events_in_range`(已结束运动事件窗口查询,分割用)/ `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`/ Ollama纯文本不参与视频——输入均为运动片段 |
| QA-Orchestrator | `qa.py` | 遍历所有适配器 `chat()`Gemini→NVIDIA→Ollama 三级降级(仅问答) |
### 3.3 FAM-UINAS 端)
Vue3 + Vite + Tailwind SPA`fam-ui/src/views/*.vue`,构建产物 `fam-ui/dist/` 由 FAM-Core `static_app.py` 托管Vue Router history 模式),页面均读本地同步镜像:
| 页面 | 功能 |
|------|------|
| 🕒 事件时间轴 | 运动片段会话列表(`motion_*.mp4`,只显示有内容的会话)+ 选中会话的事件时间线(时间点 + 描述 + 人物/关注徽章 + 事件帧图);支持 `?video=<id>` 定位跳转 |
| 👤 人物管理 | 按规范名/标签聚合,命名/合并(回推 Oracle**人物卡含「运动片段」区块**(该人物出现过的运动片段:缩略图/时间/摘要/事件数,点击跳时间轴) |
| 💬 AI 对话 | 输入框 + 调 `/api/chat/ask`;按 queried_person 预设快捷提问 |
| 📝 对话历史 | chat_history 倒序展示 |
| 📈 统计图表 | 模型来源占比 / 关注事件 / 同步状态 |
---
## 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` | 视频会话(整段素材 + 运动片段均在此表) | id, filename(UNIQUE), camera_name, status, summary_json, events_json, people_json, compute_provider, event_start_time, duration_sec, **motion_event_id**(运动片段关联的 SS 事件 id, **camera_id**, updated_at |
| `events` | 视频内时间点事件 | id, video_id, ts, description, person_list_json, person_appearances_json, is_attention_event |
| `people` | 规范人物Oracle 维护) | id, label(UNIQUE), canonical_name, appearances, source(llm/manual), features_json, display_uid |
| `ss_motion_events` | NAS 推送的运动事件(分割素材的依据) | id, event_id(UNIQUE), camera_id, event_type(10=运动), start_time(epoch), duration, thumbnail_url, received_at |
| `sync_cursor` | 同步游标 + 心跳 | key, valuesync_cursor=上次 server_timemotion_heartbeat_at=推送链路心跳) |
**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, features_json, display_uid |
| `sync_cursor` | 同步游标 + 运动游标 | key='last_since'key='motion_last_event_id'MotionNotifier 增量游标) |
| `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 顺序,仅两云端都失败才用本地兜底
**POST /api/ss/motion** — 运动事件接收NAS MotionNotifier 推送):`{"token", "events":[{event_id, camera_id, event_type, start_time, duration, thumbnail_url}]}` → 幂等落库 `ss_motion_events` + 刷新心跳;`events: []` 空数组即心跳
**GET /api/oracle/frame** — 事件帧图:`?video_id=&ts=&w=` → jpeg`ts` 为绝对时间,`frame_service``ts event_start_time` 偏移从视频文件取帧)
**GET /api/oracle/avatar** — 人物头像:`?label=&w=` → jpeg从该人物候选事件 `person_appearances` bbox 裁剪)
**GET /health** — 服务状态(含已处理视频数)
### 5.2 FAM-CoreNAS :8000
| 端点 | 方法 | 说明 |
|------|------|------|
| `/health` | GET | 服务健康 |
| `/api/status` | GET | Oracle-Sync + MotionNotifier 状态sync running/cursormotion poll_enabled/pushed_total/heartbeat |
| `/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`SS 行動規則未配置则不触发) |
| `/api/ss/status` | GET | 运动监测状态:`poll_enabled` / `camera_loaded` / `camera_map` / `pushed_total` / `last_error` / 心跳 |
| `/api/ui/videos` | GET | 运动片段会话列表(`?date=&page=`**只返回 `motion_` 前缀或有事件的会话**0 段素材不展示) |
| `/api/ui/videos/<id>` | GET | 会话详情:`{video, events[]}`(事件含 person_list_json / person_appearances_json / is_attention_event |
| `/api/ui/stats` | GET | 统计卡videos / events / people / attention`?date=` 过滤,口径与列表一致) |
| `/api/ui/people` | GET | 人物列表(按 canonical_name 聚合display / labels / appearances / first_seen / features_json / display_uid |
| `/api/ui/people/clips` | GET | 人物运动片段:`?label=&limit=` → 该人物出现过的运动片段video_id / event_start_time / duration / summary / first_ts 缩略图定位 / clip_events |
| `/api/ui/attention-events` | GET | 需关注事件(日期 + 涉及人物,已去重清洗) |
| `/api/ui/named-members` | GET | 已命名成员真名列表AI 对话快捷选择) |
| `/api/ui/model-stats` | GET | 云端模型调用统计(按模型聚合 + 最近明细) |
| `/api/ui/service-status` | GET | NAS 同步状态 + Oracle 实时活动代理token 不下发浏览器) |
| `/api/proxy/frame` | GET | 事件帧图代理:`?video_id=&ts=&w=` → Oracle `/api/oracle/frame`(浏览器不直连 Oracle |
| `/api/proxy/avatar` | GET | 人物头像代理:`?label=&w=` → Oracle `/api/oracle/avatar` |
> 已删除:`/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 视频预处理历史记录关键帧抽帧方案v3 已废弃)
> ⚠️ 本小节为旧架构v1/v2 本地抽帧分析)记录。**v3 运动事件驱动架构不再抽帧**
> 整段素材按运动事件 ffmpeg 分割运动片段(`-c:v copy`),片段直传云端 VLM 分析。
> 保留此处仅作历史参考。
- **粗抽候选帧**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/` | `bash start_core.sh`gunicorn -w 1 :8000source .env 注入 DSM_*/ORACLE_SYNC_TOKEN |
| FAM-UI | NAS | `/volume1/web/sentinel-home-ai/fam-ui/dist/` | Vue3 构建产物,由 FAM-Core `static_app.py` 托管(无需独立进程);本地改代码后 `npm run build` 并 tar 部署 dist |
| FAM-Edge | Oracle | `/opt/fam-edge/` | **systemd `fam-edge.service` 守护**Restart=always部署代码后 `sudo systemctl restart fam-edge`(勿手动 setsid会端口冲突 |
| Ollama | Oracle | systemd 托管 | 环境变量 `OLLAMA_KEEP_ALIVE=-1` |
> Oracle 部署方式:本地 git 提交 push Gitea → tar 管道到 `/opt/fam-edge``--strip-components=1` 解临时目录再 cp避免动 data/venv/gdrive_videos
### 8.2 依赖
- **FAM-CoreNAS, Python 3.10 venv**Flask, Gunicorn, **PyMySQL**45KB 纯 Python 替代 19MB mysql-connector, PyYAML, requests
- **FAM-UINAS, Node**Vue3 + Vite + Tailwind`fam-ui/`,构建产物 dist 不入库);**不再依赖 Streamlit**
- **FAM-EdgeOracle, Python venv**Flask, Gunicorn, requests, PyYAML, opencv-python, numpy, **openai**NVIDIA NIM 兼容 OpenAI API 规范Gemini 用 requests 直调 REST
- **系统级**FFmpeg两端Oracle 端用于运动片段分割 + 帧图/头像、Ollama + qwen2.5:7bOracle、MariaDB 10.11NAS、rcloneOracle同步 Google Drive 素材)
### 8.3 配置文件要点
**fam-core/config/config.yaml**NAS —— 同步 + 问答 + 运动监测):
```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 分钟拉一次增量
chat_handler:
qa_url: "http://129.146.203.203:5000/api/edge/chat/ask"
timeout: 120
motion_notifier: # 运动监测(轮询主路径)
enabled: true
poll_enabled: true
dsm_host: "192.168.50.64" # Surveillance Station
dsm_port: 5000
dsm_account: "${DSM_ACCOUNT}"
dsm_password: "${DSM_PASSWORD}"
camera_ids: [2]
camera_name_to_id: {"Generic_ONVIF-001": 2}
oracle_base_url: "http://129.146.203.203:5000"
oracle_token: "${ORACLE_SYNC_TOKEN}"
poll_interval_sec: 60
poll_window_hours: 2
batch_size: 100
heartbeat_interval_sec: 300 # 心跳(证明推送链路存活,< Oracle 侧 900s 阈值)
```
**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}"
person_service:
schedule_interval_sec: 1800 # 每 30 分钟重新汇总人物
model: "gemini"
video_processing:
max_concurrent: 1 # 单视频串行,避免抢占云端配额
timeout: 900
vision_order: ["gemini", "nvidia"]
motion_segment: # 运动片段分割(运动事件驱动架构)
clips_dir: "/opt/fam-edge/motion_clips"
keep_audio: true # 保留音频pcm_alaw -> aac 64k 转码)
min_duration_sec: 1
unfinished_grace_sec: 10 # start+duration 距当前 ≤10s 视为已结束
models:
- provider: "gemini"
role: "vision"
model_name: "gemini-flash-latest"
api_key: "${GEMINI_API_KEY}"
extra_api_keys: ["${GEMINI_API_KEY_2}", "${GEMINI_API_KEY_3}", "${GEMINI_API_KEY_4}"]
timeout: 600
- provider: "nvidia"
role: "vision"
model_name: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning" # 整视频 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 管道:`tar czf - <子目录> | ssh ... 'tar xzf - -C <目标>'`
- NAS scp 子系统被禁用,同样用 stdin 管道传文件
- **Oracle fam-edge 由 systemd `fam-edge.service` 守护Restart=always**:部署代码后必须 `sudo systemctl restart fam-edge`;手动 `setsid` 启动会和守护打架(端口 `Connection in use`
- NAS 远端 kill gunicorn 用 `ps aux | grep "[f]am-core/venv/bin/gunicorn"` 字符类技巧pkill/pgrep 会匹配 SSH 自身命令行导致断连)
- fam-core 启动模块路径是 `src.fam_core.app:app`(不是 `fam_core.app:app``start_core.sh` 会 source 仓库根 `.env` 注入 `DSM_*/ORACLE_SYNC_TOKEN`
- Edge 单 worker 处理任务期间 `/health` 可能不响应,属正常
- **运动数据清理**:切换架构/重新提取时清 Oracle `videos/events/people` + `motion_clips/`(保留 `ss_motion_events` 与素材)与 NAS `sync_*` 镜像,重启两端自动重新分割分析
- **前端构建产物 `fam-ui/dist` 不入库**.gitignore改前端后需 `npm run build` 并单独 tar 部署 dist
---
## 9. 快速开始
```bash
# 1. 初始化数据库NAS
python scripts/init_db.py
# 2. 启动 FAM-CoreNAS含运动监测轮询 + Oracle-Sync
cd /volume1/web/sentinel-home-ai && bash start_core.sh
# 3. 启动 FAM-EdgeOraclesystemd 守护)
sudo systemctl restart fam-edge
# 4. 前端Vue3构建后由 FAM-Core 托管,无需独立进程)
cd fam-ui && npm run build # 产物 fam-ui/dist/
# 或使用脚本
./scripts/start_core.sh && ./scripts/start_edge.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-22v3 运动事件驱动架构)
- **运动事件驱动**v32026-08-22不再分析整段视频。NAS MotionNotifier 轮询 SS 事件列表60s游标续用/失败重试/心跳)推送 `ss_motion_events`Oracle 整段素材按运动事件 ffmpeg 分割运动片段(`-c:v copy -c:a aac`,只分割已结束事件,`motion_event_id` 幂等),**只分析运动片段**;前端契约不变
- **数据迁移**2026-08-22整段提取的旧数据Oracle videos/events/people + NAS 镜像已清空并按运动视频重新提取历史素材NAS 轮询启动前)无运动事件,时间轴已过滤其空会话
- **人物管理重设计**2026-08-22人物卡新增「运动片段」区块缩略图/时间/摘要/事件数,点击跳时间轴定位);新增 `GET /api/ui/people/clips`Timeline 支持 `?video=` 定位
- 前端从 Streamlit 迁移为 **Vue3 + Vite + Tailwind SPA**(由 FAM-Core 托管 dist
- 运动事件链路NAS→Oracle 单向推送)+ 心跳 fail-openGemini 多 Key 轮换 + 模型调用统计
- 双端部署NAS `start_core.sh`source .envOracle **systemd `fam-edge.service` 守护**
详细进度见 `PROGRESS.md`
### v1.1 待办
| # | 任务 | 优先级 |
|---|------|--------|
| 1 | 单元测试JSON parser / circuit breaker / schema 校验) | 中 |
| 2 | Tailscale 修复NAS userspace 模式升级,流量不走公网) | 低 |
| 3 | daily_summaries 每日摘要 | 低 |
| 4 | 运动片段音频策略验证(当前保留 aac 转码) | 低 |
---
文档结束