Files
sentinel-home-ai/README.md
ericwyuan 2e370ab668 docs: 同步问答链路抽离到 ai-gateway 后的架构文档
README.md/PROGRESS.md 里大量描述还停留在"FAM-Edge 自己维护 Gemini→NVIDIA→
Ollama 问答降级链"的旧架构,跟实际代码(FAM-Edge 已改为转发客户端,模型链
整个搬到独立的 ai-gateway 服务)不一致,逐处订正:

- 新增 §3.4 AI-Gateway 模块章节、部署拓扑图、Gitea 仓库表新增 ai-gateway 条目
- 模块表(FAM-Core/FAM-Edge)、API 文档、数据库 compute_provider 说明更新
- §6.2/6.3 本地 Ollama 与模型适配器章节:去掉已删除的 OllamaAdapter/role=text,
  问答降级链路图重画为"FAM-Edge 转发 -> AI-Gateway 内部降级"
- 修复已经损坏(合并冲突残留)且过时的 fam-edge/config.yaml 示例,新增
  ai-gateway/config.yaml 示例;如实记录两份 .env 各自独立维护的实际部署状态
- PROGRESS.md 补一条 2026-08-23 变更记录 + 服务运行状态表新增 AI-Gateway 行

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 17:40:55 +08:00

855 lines
60 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-23问答链路抽离为独立 ai-gateway 服务FAM-Edge 不再自己维护问答模型降级链,改为转发调用)
---
## 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 转发到独立 **ai-gateway** 服务OpenAI 兼容协议NVIDIA → Gemini → 本地 Ollama 降级链在 ai-gateway 内部完成FAM-Edge 不再自己维护模型链)→ 返回并写 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 对话**"汤圆今天干嘛了" | 独立 **ai-gateway** 服务OpenAI 兼容协议) | NVIDIA → Gemini → 本地 Ollama | FAM-Edge 不参与问答模型调用只转发降级顺序、key 轮换、熔断全部由 ai-gateway 自己管理 |
- **Google Gemini**`gemini-flash-latest`API Key 已验证,支持多图单请求;**问答场景改用 ai-gateway 里独立的非 flash 文字模型链**,不复用视觉分析这个 flash 实例)
- **NVIDIA NIM**(视觉分析用 `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` 整视频输入;**问答场景改用 ai-gateway 里独立的文字模型链**,不复用视觉分析实例)
- **本地 Ollama**qwen2.5:7b**已从 FAM-Edge 移除2026-08-23 起归属独立的 ai-gateway 服务**:仅参与智能问答,且仅在 ai-gateway 内部 NVIDIA/Gemini 都失败时作兜底;视觉分析链路完全不涉及本地模型
Orchestrator 视觉阶段按 `fallback` 模式顺序降级Gemini → NVIDIA NIM云端模型直出结构化 JSON 后由 `format_cloud_result` 格式化,均在 FAM-Edge 内完成。问答阶段完全在 **ai-gateway**(独立服务,见 §3.4)内部按 `nvidia → gemini → ollama` 顺序降级FAM-Edge 只是转发客户端。
---
## 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 + AI-Gateway | Ampere A1 2C12G ARM64无 GPU, Ubuntu 20.04 | 公网 129.146.203.203 / Tailscale 100.74.137.126 | FAM-Edge :5000systemd 守护,视频分析 + 转发问答), **AI-Gateway :5100systemd 守护,对外监听,问答模型降级链)**, Ollama :11434仅本地被 AI-Gateway 调用) |
| 家庭网络 | 用户入口 | 普通终端 | 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 不对外暴露;聊天请求 NAS → FAM-Edge `/api/edge/chat/ask`(保持不变的对外契约)→ **FAM-Edge 转发到同机的 AI-Gateway :5100**`/v1/chat/completions`OpenAI 兼容协议Bearer token 鉴权)→ NVIDIA/Gemini/Ollama 降级链。AI-Gateway 自己对外监听 0.0.0.0:5100Bearer token 鉴权 fail-closed供其他项目直接接入不止服务本系统
- 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 合并规范名 → 回灌视频提示 │
│ ├─ QA-Proxy (qa.py): /api/edge/chat/ask(/stream) 原样转发到 AI-Gateway │
│ ├─ OracleDB (SQLite): videos / events / people / ss_motion_events │
│ └─ API: /api/oracle/sync (增量拉取) · /api/ss/motion (运动事件) · │
│ /api/oracle/people/correct (命名校正) · /api/edge/chat/ask (问答代理) │
│ │ HTTP (本机回环 + 公网均可达) │
│ ▼ │
│ AI-Gateway (Flask :5100独立项目/服务/git 仓库OpenAI 兼容协议) │
│ ├─ /v1/chat/completionsNVIDIA → Gemini多 Key 轮换)→ 本地 Ollama 降级链 │
│ ├─ Bearer token 鉴权fail-closed对外 0.0.0.0:5100非本系统专属 │
│ └─ 独立 .env/opt/ai-gateway/.envNVIDIA/Gemini key 与 FAM-Edge 各自一份) │
└───────────────────────────────┬───────────────────────────────────────────────┘
│ 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 (Vue3 SPAFAM-Core 托管 dist) 读同步镜像 │
└─────────────────────────────────────────────────────────────────────────────────┘
```
#### 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` → 同机 AI-Gateway `:5100` 两跳代理
- AI-Gateway `:5100` 本身对公网直接开放Bearer token 鉴权),跟 FAM-Edge `:5000` 是两个独立监听端口,非本系统的其他项目可以跳过 FAM-Edge 直接接入
- 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` 问答FAM-Edge 转发到独立 AI-Gateway 服务,接口契约不变)→ 写 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(/stream)`(问答,转发到 AI-Gateway见 §3.4`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`)——**只有视觉分析用2026-08-23 起不再含 Ollama/文字模型**,问答模型完全移交 AI-Gateway |
| QA-Proxy | `qa.py` | **2026-08-23 重写为 HTTP 转发客户端**(原来自己遍历适配器 `chat()` 做 NVIDIA→Gemini→Ollama 三级降级的逻辑已整个搬到 AI-Gateway调 AI-Gateway `/v1/chat/completions`,把 OpenAI 兼容响应翻译回原有 `run_qa`/`run_qa_stream` 契约,`api_gateway.py` 和 FAM-Core 调用方零改动 |
### 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 倒序展示 |
| 📈 统计图表 | 模型来源占比 / 关注事件 / 同步状态 |
### 3.4 AI-GatewayOracle 端,独立项目/服务2026-08-23 新增)
> 独立的 git 仓库/部署单元(`ai-gateway/`Gitea 见 §10.4),跟 FAM-Edge/FAM-Core 不是同一个代码库。原本嵌在 FAM-Edge 里的问答模型降级链(跟视频分析业务无关,是通用能力)整个抽出来,做成 OpenAI 兼容协议的独立服务——除了 FAM-Edge 自己(改为转发调用),任何支持自定义 `base_url` 的 OpenAI SDK/工具都能直接接入,不限于本系统。
| 模块 | 文件 | 职责 |
|------|------|------|
| App | `app.py` | `POST /v1/chat/completions`核心端点OpenAI 兼容请求/响应结构,支持 `stream: true` 流式与非流式);`GET /v1/models`(占位实现);`GET /health`(免鉴权) |
| Auth | `auth.py` | Bearer token 鉴权(`Authorization: Bearer <AI_GATEWAY_TOKEN>`**fail-closed**:未配置 token 时全部需鉴权接口直接拒绝503不会退回任何默认值 |
| Orchestrator | `orchestrator.py` | `ChatOrchestrator`:按 `config.yaml``models` 数组顺序依次尝试适配器 `chat()`/`chat_stream()`,一个 provider 完全没有输出才换下一个;已开始吐字后中途失败直接结束,不悄悄换源接着写 |
| Adapters | `adapters/` | `NvidiaAdapter`(多模型链)/ `GeminiAdapter`(多 Key 随机轮换,`_messages_to_gemini()` 转换为原生 `contents`/`systemInstruction`/ `OllamaAdapter``/api/chat` 原生多轮,含 `warm_up()` 启动预热)——**纯文本,均为 chat-only不含视觉分析** |
| Config | `config_loader.py` | 部署时用 `FAM_ENV_FILE` 指向共享的 `.env` 复用密钥当前实际部署未启用这个复用AI-Gateway 用自己独立的 `/opt/ai-gateway/.env`,见 §8.3 说明) |
**降级链**`config.yaml``models` 数组顺序NVIDIA多模型链自动降级→ Gemini多 Key 随机轮换)→ 本地 Ollama兜底`OLLAMA_KEEP_ALIVE=-1` + 启动预热避免冷启动)。
---
## 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 不参与视频分析,不会出现在该字段
- 问答链路AI-Gateway 内部 NVIDIA→Gemini→Ollama 降级链)的 provider 体现在 `/api/edge/chat/ask` 响应的 `provider` 字段(由 AI-Gateway 原样透传回来)
### 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(/stream)** — 智能问答FAM-Core Chat-Handler 调用):请求 `{"prompt","max_tokens"}` → 响应 `{"answer","provider"}`(流式版为 SSE事件 `provider_trying`/`chunk`/`done`/`all_failed`**2026-08-23 起 FAM-Edge 自己不跑模型,转发到同机 AI-Gateway `/v1/chat/completions`**,内部按 NVIDIA → Gemini → 本地 Ollama 顺序降级(对 FAM-Core 不可见,接口契约不变)
**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/login` | POST | 登录:`{"username","password"}` → 校验 `FAM_AUTH_USER`/`FAM_AUTH_PASS`NAS `.env` 配置,无硬编码默认值,未配置则拒绝所有登录)→ 种 HttpOnly cookie `fam_session`2 小时) |
| `/api/logout` | POST | 退出登录(清 cookie |
| `/api/auth/check` | GET | 登录态检查:`{"authed": true\|false}`**免登录** |
| `/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 行動規則未配置则不触发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仅智能问答兜底归属 AI-Gateway
本地 Ollamaqwen2.5:7b纯文本模型**不参与视觉分析、不参与云端结果融合**。**2026-08-23 起 Ollama 相关代码(`OllamaAdapter`、启动预热逻辑)已从 FAM-Edge 整个移除,归属独立的 AI-Gateway 服务**——它只在 AI-Gateway 内部 NVIDIA 与 Gemini 都失败时才被启用作为兜底,进程仍然跑在 Oracle 同一台机器上,只是调用方从 FAM-Edge 换成了 AI-Gateway。视觉分析与结构化输出全部由云端模型承担跟本地模型无关。以下参数作为问答任务的调优依据保留。
| 参数 | 值 | 依据 |
|------|-----|------|
| `OLLAMA_KEEP_ALIVE=-1` | 模型常驻内存 | 消除 55s 冷启动(常驻约 4.3GB12GB 内存够用) |
| `num_predict=512` | 限制生成 token | ARM 约 5 tok/s过长生成会拖慢问答响应 |
| AI-Gateway 启动预热 | 后台线程 `warm_up()` | `OLLAMA_KEEP_ALIVE=-1` 只保证加载后不换出不负责主动预加载NVIDIA/Gemini 一直成功时 Ollama 永远不会被自然触发,加了启动时预热避免真正兜底时才发现要等 1-2 分钟冷启动 |
| 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 / 协议 | 状态 |
|----------|---------|------|------------|------|
| `gemini` | `GeminiAdapter` | **vision** | requests 直调 REST `:generateContent` | 已实现 |
| `nvidia` | `NvidiaVisionAdapter` | **vision** | **openai SDK**NIM 兼容 OpenAI API 规范) | 已实现 |
> `OllamaAdapter` 已于 2026-08-23 从 FAM-Edge 移除(连同 `role='text'` 的问答适配器整体搬到独立的 AI-Gateway 服务,见 §3.4。FAM-Edge 现在的 `model_adapters/` 只剩 `vision` 角色,`role` 字段本身仍保留(`get_role()` 仍被 `video_processor.py` 用于筛选视觉适配器),只是不会再出现 `text` 取值。
**role 语义**
- `vision`:参与视觉分析阶段,按 fallback 顺序降级,直出结构化 JSON
- `text`**已不在 FAM-Edge 出现**——原来"仅参与智能问答、Gemini/NVIDIA 都失败时兜底"的语义现在完全由独立的 AI-Gateway 服务内部实现(见 §3.4
**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 场景2026-08-23 起完全在独立的 AI-Gateway 服务内部FAM-Edge 只转发)**
```
FAM-Edge /api/edge/chat/ask(/stream)
│ HTTP 转发qa.py本机回环
AI-Gateway /v1/chat/completions
NVIDIA问答专用文字模型链跟视觉分析的 omni 模型完全独立)
│ 完全没有输出 / 熔断 OPEN / 超时
Gemini问答专用非 flash 文字模型链,多 Key 随机轮换,跟视觉分析的 flash 模型完全独立)
│ 完全没有输出 / 熔断 OPEN / 超时
本地 Ollama (qwen2.5:7b) —— 仅当前两者都失败才启用
│ 失败
返回"所有模型均不可用"HTTP 503
```
> 已经开始吐字之后中途失败:不换下一个 provider 接着写(避免答案风格前后不连贯),直接结束这次生成——这个语义在 AI-Gateway 的 `orchestrator.py` 里实现FAM-Edge 的 `qa.py` 只是原样转发这个行为,不重复实现。
**熔断器策略**(按 provider 独立):
| 服务 | provider | role | threshold | cooldown | enabled |
|------|----------|------|-----------|----------|---------|
| FAM-Edge | Gemini | vision | 3 次连续失败 | 600s | true |
| FAM-Edge | NVIDIA NIM | vision | 3 次连续失败 | 600s | true |
| AI-Gateway | NVIDIA问答 | — | 见 ai-gateway 配置 | — | true |
| AI-Gateway | Gemini问答 | — | 见 ai-gateway 配置 | — | true |
| AI-Gateway | Ollama | — | — | — | false本地不熔断 |
**健康探测**FAM-Edge 侧 Gemini `GET /v1/models?key=...`NVIDIA `client.models.list()`,视觉模型全部不健康返回 503。AI-Gateway 侧 Ollama `GET /api/tags`Gemini/NVIDIA 同上,各自独立。
**多模型标签兼容**`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_AUTH_* |
| 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会端口冲突 |
| **AI-Gateway** | Oracle | `/opt/ai-gateway/` | **systemd `ai-gateway.service` 守护**Restart=always独立 venvPython 3.8gunicorn 绑定 `0.0.0.0:5100`(对外直接开放,非仅本机);部署代码后 `sudo systemctl restart ai-gateway` |
| Ollama | Oracle | systemd 托管 | 环境变量 `OLLAMA_KEEP_ALIVE=-1`**被 AI-Gateway 调用,不再被 FAM-Edge 调用** |
> **外网访问frp 内网穿透)**NAS 跑 `frpc``/etc/frp/frpc.toml`S99frpc.sh 守护),映射到 Oracle `129.146.203.203`frps :7000
> - `3000` → NAS Gitea、`8500` → NAS WordPress(8088)、**`8000` → NAS FAM-Core本系统**
> - 外网入口 `http://129.146.203.203:8000`**需登录**(见 §5.2 `/api/login`
>
> **登录校验2026-08-22 新增2026-08-23 改为 fail-closed**FAM-Core 全站拦截(`auth.py`)——页面未登录 302 `/login`内置深色登录页SPA 零改动),`/api/*` 未登录 401凭据 `FAM_AUTH_USER`/`FAM_AUTH_PASS`NAS `.env` 配置,**无硬编码默认值**——这两个变量跟 NAS SSH 密码是同一个值,公网入口不能有"没配置就退回已知密码"的兜底,`.env` 没配好这两项时直接拒绝所有登录);白名单免登录:`/login` `/api/login` `/api/logout` `/api/auth/check` `/health` `/api/ss/webhook` `/assets/*`。登录态为进程内 token + HttpOnly cookie**2 小时**),过期或重启 fam-core 需重新登录。
> Oracle 部署方式:本地 git 提交 push Gitea → tar 管道到 `/opt/fam-edge``--strip-components=1` 解临时目录再 cp避免动 data/venv/gdrive_videos。**AI-Gateway 是独立 git 仓库**http://192.168.50.64:3000/ericwyuan/ai-gateway见 §10.4),同样 tar 管道部署到 `/opt/ai-gateway`,互不影响。
### 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**问答不再直接调模型,只用 requests 转发到 AI-Gateway**
- **AI-GatewayOracle, Python 3.8 venv独立部署单元**Flask, Gunicorn, requests, PyYAML, **openai**NVIDIA 问答模型用)
- **系统级**FFmpeg两端Oracle 端用于运动片段分割 + 帧图/头像、Ollama + qwen2.5:7bOracle**被 AI-Gateway 调用**、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 —— 素材分割 + 片段分析 + 同步 + 人物服务;**2026-08-23 起不再含任何问答模型配置**
```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 视为已结束
ai_gateway: # 问答转发客户端配置2026-08-23 新增,取代原来的问答专用 models 条目)
base_url: "http://127.0.0.1:5100"
token: "${AI_GATEWAY_TOKEN}"
timeout: 60
models: # 只剩视觉分析用的两个 providerrole 全是 vision
- provider: "gemini"
role: "vision"
model_name: "gemini-flash-latest"
fallback_models: ["gemini-flash-lite-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
```
**ai-gateway/config/config.yaml**Oracle —— 独立服务,问答模型降级链,`server.port: 5100`
```yaml
server:
port: 5100
models: # 顺序即降级优先级chat-only不含视觉
- provider: "nvidia"
enabled: true
model_name: "nvidia/nemotron-3-ultra-550b-a55b" # 问答专用文字模型链,跟视觉分析的 omni 模型不同实例
fallback_models: ["nvidia/nemotron-3-super-120b-a12b", "openai/gpt-oss-120b"]
api_key: "${NVIDIA_API_KEY}"
timeout: 600
- provider: "gemini"
enabled: true
model_name: "gemini-pro-latest" # 问答专用非 flash 模型链,跟视觉分析的 flash 模型不同实例
fallback_models: ["gemini-2.5-pro"]
api_key: "${GEMINI_API_KEY}"
extra_api_keys: ["${GEMINI_API_KEY_2}", "${GEMINI_API_KEY_3}", "${GEMINI_API_KEY_4}"]
timeout: 60
- provider: "ollama"
enabled: true
model_name: "qwen2.5:7b"
base_url: "http://localhost:11434"
timeout: 120
```
> 部署时 `config_loader.py` 支持 `FAM_ENV_FILE` 环境变量指向另一个 `.env` 复用密钥(设计初衷是复用 FAM-Edge 已配置的 `NVIDIA_API_KEY`/`GEMINI_API_KEY*`,避免同一份密钥维护两份);**当前实际部署选择了各自独立**`/opt/ai-gateway/.env` 自己存了一份 `NVIDIA_API_KEY`/`GEMINI_API_KEY*`/`AI_GATEWAY_TOKEN`,跟 `/opt/fam-edge/.env` 没有关联,以后轮换 key 需要两处都改。
**环境变量**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 | 说明 |
|------|-----|------|
| sentinel-home-ai本仓库 | http://192.168.50.64:3000/ericwyuan/sentinel-home-ai | monorepofam-core + fam-edge + fam-ui |
| ai-gateway2026-08-23 新增) | http://192.168.50.64:3000/ericwyuan/ai-gateway | 独立仓库/独立部署单元,问答网关服务 |
账号 / 密码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-23
- **问答链路抽离为独立 ai-gateway 服务**2026-08-23FAM-Edge 原本自己维护的问答模型降级链NVIDIA→Gemini→Ollama含 key 轮换/熔断)整个搬到独立仓库/独立部署单元 `ai-gateway`OpenAI 兼容协议 `/v1/chat/completions`Bearer token 鉴权,对外 `:5100`FAM-Edge `qa.py` 重写为转发客户端,`/api/edge/chat/ask(/stream)` 对 FAM-Core 的契约不变;`OllamaAdapter` 从 FAM-Edge 删除
- **运动事件驱动**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 转码) | 低 |
---
文档结束