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>
This commit is contained in:
16
PROGRESS.md
16
PROGRESS.md
@@ -1,15 +1,25 @@
|
|||||||
# 项目进度追踪
|
# 项目进度追踪
|
||||||
|
|
||||||
> 最后更新: 2026-08-22 12:45
|
> 最后更新: 2026-08-23
|
||||||
|
|
||||||
## 服务运行状态
|
## 服务运行状态
|
||||||
|
|
||||||
| 服务 | 节点 | 地址 | 状态 | 验证结果 |
|
| 服务 | 节点 | 地址 | 状态 | 验证结果 |
|
||||||
|------|------|------|------|---------|
|
|------|------|------|------|---------|
|
||||||
| FAM-Core | NAS | 0.0.0.0:8000 | ✅ 运行中 | health=ok, gunicorn 单 worker;Oracle-Sync + MotionNotifier 轮询 SS 事件推送(游标 DB 续用/失败重试) |
|
| FAM-Core | NAS | 0.0.0.0:8000 | ✅ 运行中 | health=ok, gunicorn 单 worker;Oracle-Sync + MotionNotifier 轮询 SS 事件推送(游标 DB 续用/失败重试) |
|
||||||
| FAM-Edge | Oracle | 0.0.0.0:5000 | ✅ 运行中 | systemd 守护(fam-edge.service);素材→运动片段分割→只分析片段;队列消费正常 |
|
| FAM-Edge | Oracle | 0.0.0.0:5000 | ✅ 运行中 | systemd 守护(fam-edge.service);素材→运动片段分割→只分析片段;问答改为转发 AI-Gateway;队列消费正常 |
|
||||||
|
| **AI-Gateway** | Oracle | 0.0.0.0:5100 | ✅ 运行中 | systemd 守护(ai-gateway.service),2026-08-23 新增;独立仓库/独立部署;`/health` 正常,端到端问答实测成功(provider=nvidia) |
|
||||||
| MariaDB | NAS | 127.0.0.1:3306 | ✅ 运行中 | 10.11.11, 6 张表, utf8mb4 |
|
| MariaDB | NAS | 127.0.0.1:3306 | ✅ 运行中 | 10.11.11, 6 张表, utf8mb4 |
|
||||||
| Ollama | Oracle | 127.0.0.1:11434 | ✅ 运行中 | qwen2.5:7b,仅智能问答兜底(不参与视觉/融合) |
|
| Ollama | Oracle | 127.0.0.1:11434 | ✅ 运行中 | qwen2.5:7b,仅智能问答兜底(不参与视觉/融合);**被 AI-Gateway 调用,不再被 FAM-Edge 直接调用** |
|
||||||
|
|
||||||
|
## 2026-08-23 问答链路抽离为独立 ai-gateway 服务
|
||||||
|
|
||||||
|
- **决策**:原本嵌在 FAM-Edge 里的问答模型降级链(NVIDIA 文字模型链 → Gemini 非 flash 文字模型链 → 本地 Ollama 兜底,含 key 轮换/熔断)跟视频分析业务无关,是通用能力,抽成独立服务(OpenAI 兼容协议 `/v1/chat/completions`),除了 FAM-Edge 自己(改为转发调用),别的项目也能直接接入
|
||||||
|
- **新增独立仓库/项目 `ai-gateway`**(http://192.168.50.64:3000/ericwyuan/ai-gateway):`app.py`(Flask,`/v1/chat/completions` 流式+非流式、`/v1/models` 占位、`/health` 免鉴权)/ `auth.py`(Bearer token 鉴权,fail-closed)/ `orchestrator.py`(`ChatOrchestrator`,按 provider 顺序降级,保留"未吐字才切换、已吐字后中途失败直接结束"语义)/ `adapters/`(NVIDIA/Gemini/Ollama,纯文本,从 fam-edge 对应适配器裁剪 chat 相关代码而来);测试 37/37 通过
|
||||||
|
- **FAM-Edge 侧改动**(commit `5caeb29`):`qa.py` 重写为 HTTP 转发客户端(调 ai-gateway `/v1/chat/completions`,翻译回原有 `run_qa`/`run_qa_stream` 契约,`api_gateway.py` 和 FAM-Core 调用方零改动);删除 `model_adapters/ollama_adapter.py` 及其测试;`gemini_adapter.py`/`nvidia_adapter.py` 移除 `chat()`/`chat_stream()`/问答专用超时(只保留 `analyze_video`);`app.py` 移除 Ollama 预热逻辑;`config.yaml` 移除 3 个问答专用 model 条目,新增 `ai_gateway` 客户端配置块;测试 125/125 通过
|
||||||
|
- **部署**:Oracle 新建 `/opt/ai-gateway/`(独立 venv,Python 3.8),systemd `ai-gateway.service` 守护,gunicorn 绑定 `0.0.0.0:5100`(对外直接开放,Bearer token 鉴权,用户明确选择"别的项目也能从这台机器之外访问"而不是仅本机);`AI_GATEWAY_TOKEN`/`NVIDIA_API_KEY`/`GEMINI_API_KEY*` 存在独立的 `/opt/ai-gateway/.env`(用户选择两份 `.env` 各自独立维护,而非复用 `/opt/fam-edge/.env`,代价是以后轮换 key 需要改两处)
|
||||||
|
- **验证**:`/health` 通过;`curl` 直接测 `/v1/chat/completions`(带 token)端到端成功,provider=nvidia;FAM-Edge `/api/edge/chat/ask` 非流式 + `/api/edge/chat/ask/stream` 流式均验证通过,事件格式(`provider_trying`/`chunk`/`done`)不变,中文无乱码;队列/pending/failed 数量正常,无积压
|
||||||
|
- **已知代价**:NVIDIA/Gemini key 现在两处各存一份(`/opt/fam-edge/.env` + `/opt/ai-gateway/.env`),非最初设计的"复用同一份",用户已知情并选择保留现状
|
||||||
|
|
||||||
## 2026-08-22 运动事件驱动架构(v3)
|
## 2026-08-22 运动事件驱动架构(v3)
|
||||||
|
|
||||||
|
|||||||
181
README.md
181
README.md
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
> 多模型容灾降级 + 交互式命名 + AI 对话的家庭监控系统。
|
> 多模型容灾降级 + 交互式命名 + AI 对话的家庭监控系统。
|
||||||
> 本文档为项目需求文档与 README 的整合版,按当前代码实际状态(v1.0 E2E 已打通)编写。
|
> 本文档为项目需求文档与 README 的整合版,按当前代码实际状态(v1.0 E2E 已打通)编写。
|
||||||
> 最后更新:2026-08-22(运动事件驱动架构:整段素材分割运动片段再分析)
|
> 最后更新:2026-08-23(问答链路抽离为独立 ai-gateway 服务:FAM-Edge 不再自己维护问答模型降级链,改为转发调用)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
- **FAM-UI**(NAS 端):Vue3 SPA 读本地同步镜像(sync_videos / sync_events / sync_people),事件时间轴 + 人物管理 + AI 对话 + 对话历史 + 统计
|
- **FAM-UI**(NAS 端):Vue3 SPA 读本地同步镜像(sync_videos / sync_events / sync_people),事件时间轴 + 人物管理 + AI 对话 + 对话历史 + 统计
|
||||||
- **数据库**:Oracle 侧 SQLite(videos/events/people/ss_motion_events/sync_cursor);NAS 侧 MariaDB 镜像(sync_videos / sync_events / sync_people / sync_cursor)+ chat_history
|
- **数据库**:Oracle 侧 SQLite(videos/events/people/ss_motion_events/sync_cursor);NAS 侧 MariaDB 镜像(sync_videos / sync_events / sync_people / sync_cursor)+ chat_history
|
||||||
- **数据流向**:Google 硬盘 ──rclone──► 甲骨文整段素材 ──按运动事件分割片段──► 片段云端分析 ──► Oracle SQLite ──每 30 分钟 NAS 拉取──► NAS MariaDB 镜像 ──► FAM-UI;运动事件由 NAS 轮询 SS 推送 Oracle(NAS → Oracle 单向)
|
- **数据流向**:Google 硬盘 ──rclone──► 甲骨文整段素材 ──按运动事件分割片段──► 片段云端分析 ──► Oracle SQLite ──每 30 分钟 NAS 拉取──► NAS MariaDB 镜像 ──► FAM-UI;运动事件由 NAS 轮询 SS 推送 Oracle(NAS → Oracle 单向)
|
||||||
- **AI 对话**:查 sync_events 拼上下文 → 经 FAM-Edge 问答编排(Gemini → NVIDIA → 本地 Ollama 兜底)生成回答 → 返回并写 chat_history
|
- **AI 对话**:查 sync_events 拼上下文 → FAM-Edge 转发到独立 **ai-gateway** 服务(OpenAI 兼容协议,NVIDIA → Gemini → 本地 Ollama 降级链在 ai-gateway 内部完成,FAM-Edge 不再自己维护模型链)→ 返回并写 chat_history
|
||||||
- **人物命名**:用户命名/合并某 label → 回推 Oracle `/api/oracle/people/correct`(manual 优先)→ 下一周期同步回 NAS;Oracle 独立 person_service 汇总全量人物 → LLM 合并为规范名 → 回灌视频提示
|
- **人物命名**:用户命名/合并某 label → 回推 Oracle `/api/oracle/people/correct`(manual 优先)→ 下一周期同步回 NAS;Oracle 独立 person_service 汇总全量人物 → LLM 合并为规范名 → 回灌视频提示
|
||||||
|
|
||||||
### 1.2 不在首期范围(推迟 v1.1+)
|
### 1.2 不在首期范围(推迟 v1.1+)
|
||||||
@@ -47,13 +47,13 @@
|
|||||||
|------|--------|------|------|
|
|------|--------|------|------|
|
||||||
| **视觉分析 + 结构化输出**(看图识人/动作/衣着 → 直出 JSON) | 云端 | Gemini → NVIDIA NIM(fallback 降级) | 云端 VLM 直接产出 `global_summary` / `entities_json` / `frame_details`,Edge 仅做**格式化校验**后直存 NAS,**本地模型不介入** |
|
| **视觉分析 + 结构化输出**(看图识人/动作/衣着 → 直出 JSON) | 云端 | Gemini → NVIDIA NIM(fallback 降级) | 云端 VLM 直接产出 `global_summary` / `entities_json` / `frame_details`,Edge 仅做**格式化校验**后直存 NAS,**本地模型不介入** |
|
||||||
| **结果格式化**(云端 JSON → 入库 schema) | Edge 进程 | 无模型调用 | `format_cloud_result`:字段归一化、补 `source_providers`/`compute_provider`、推导 `entities`、缺失 `global_summary` 时事实拼接;纯数据转换,非 LLM 二次汇总 |
|
| **结果格式化**(云端 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 |
|
| **AI 对话**("汤圆今天干嘛了") | 独立 **ai-gateway** 服务(OpenAI 兼容协议) | NVIDIA → Gemini → 本地 Ollama | FAM-Edge 不参与问答模型调用,只转发;降级顺序、key 轮换、熔断全部由 ai-gateway 自己管理 |
|
||||||
|
|
||||||
- **Google Gemini**(`gemini-flash-latest`,API Key 已验证,支持多图单请求,视觉 + 问答均参与)
|
- **Google Gemini**(`gemini-flash-latest`,API Key 已验证,支持多图单请求;**问答场景改用 ai-gateway 里独立的非 flash 文字模型链**,不复用视觉分析这个 flash 实例)
|
||||||
- **NVIDIA NIM**(`meta/llama-3.2-11b-vision-instruct`,OpenAI 兼容 API,云端 GPU 推理,单请求限 1 图故逐帧调用;视觉 + 问答均参与)
|
- **NVIDIA NIM**(视觉分析用 `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning` 整视频输入;**问答场景改用 ai-gateway 里独立的文字模型链**,不复用视觉分析实例)
|
||||||
- **本地 Ollama**(qwen2.5:7b)**仅参与智能问答,且仅作兜底**:视觉链路两云端全失败 → 任务 FAILED 走重试,**绝不回退本地模型做视觉/融合**
|
- **本地 Ollama**(qwen2.5:7b)**已从 FAM-Edge 移除,2026-08-23 起归属独立的 ai-gateway 服务**:仅参与智能问答,且仅在 ai-gateway 内部 NVIDIA/Gemini 都失败时作兜底;视觉分析链路完全不涉及本地模型
|
||||||
|
|
||||||
Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA NIM;云端模型直出结构化 JSON 后由 `format_cloud_result` 格式化。问答阶段按 `gemini → nvidia → ollama` 顺序,仅末位本地模型作兜底。
|
Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA NIM;云端模型直出结构化 JSON 后由 `format_cloud_result` 格式化,均在 FAM-Edge 内完成。问答阶段完全在 **ai-gateway**(独立服务,见 §3.4)内部按 `nvidia → gemini → ollama` 顺序降级,FAM-Edge 只是转发客户端。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,13 +64,13 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA N
|
|||||||
| 节点 | 角色 | 硬件 | IP | 服务与端口 |
|
| 节点 | 角色 | 硬件 | 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(Vue3 构建产物,由 FAM-Core 托管), MariaDB :3306, Surveillance Station :5000 |
|
| Synology NAS | FAM-Core + FAM-UI + 数据库 | DS220+ (Geminilake), DSM 7 | 家庭局域网 192.168.50.64 / Tailscale 100.70.234.39 | FAM-Core :8000, FAM-UI(Vue3 构建产物,由 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 :5000(systemd 守护), Ollama :11434(仅本地) |
|
| 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 :5000(systemd 守护,视频分析 + 转发问答), **AI-Gateway :5100(systemd 守护,对外监听,问答模型降级链)**, Ollama :11434(仅本地,被 AI-Gateway 调用) |
|
||||||
| 家庭网络 | 用户入口 | 普通终端 | 192.168.50.0/24 | 浏览器访问 `http://192.168.50.64:8000` |
|
| 家庭网络 | 用户入口 | 普通终端 | 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 → 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 未配置则不触发)
|
- **运动事件获取(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` 代理转发
|
- 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:5100(Bearer token 鉴权 fail-closed),供其他项目直接接入,不止服务本系统
|
||||||
- Tailscale 两节点已安装在线,但 NAS tailscaled 为 userspace 模式且防火墙端口不通,暂走公网 IP
|
- Tailscale 两节点已安装在线,但 NAS tailscaled 为 userspace 模式且防火墙端口不通,暂走公网 IP
|
||||||
|
|
||||||
### 2.2 部署拓扑与数据流(新架构 v2:Oracle 分析 + NAS 镜像)
|
### 2.2 部署拓扑与数据流(新架构 v2:Oracle 分析 + NAS 镜像)
|
||||||
@@ -88,9 +88,16 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA N
|
|||||||
│ ├─ Video-Processor: 素材按 ss_motion_events 分割运动片段 → 只分析片段 │
|
│ ├─ Video-Processor: 素材按 ss_motion_events 分割运动片段 → 只分析片段 │
|
||||||
│ │ (ffmpeg -c:v copy -c:a aac) → Gemini → 失败 NVIDIA → 再失败 FAILED │
|
│ │ (ffmpeg -c:v copy -c:a aac) → Gemini → 失败 NVIDIA → 再失败 FAILED │
|
||||||
│ ├─ Person-Service: 汇总人物 → LLM 合并规范名 → 回灌视频提示 │
|
│ ├─ Person-Service: 汇总人物 → LLM 合并规范名 → 回灌视频提示 │
|
||||||
|
│ ├─ QA-Proxy (qa.py): /api/edge/chat/ask(/stream) 原样转发到 AI-Gateway │
|
||||||
│ ├─ OracleDB (SQLite): videos / events / people / ss_motion_events │
|
│ ├─ OracleDB (SQLite): videos / events / people / ss_motion_events │
|
||||||
│ └─ API: /api/oracle/sync (增量拉取) · /api/ss/motion (运动事件) · │
|
│ └─ API: /api/oracle/sync (增量拉取) · /api/ss/motion (运动事件) · │
|
||||||
│ /api/oracle/people/correct (命名校正) · /api/edge/chat/ask (问答) │
|
│ /api/oracle/people/correct (命名校正) · /api/edge/chat/ask (问答代理) │
|
||||||
|
│ │ HTTP (本机回环 + 公网均可达) │
|
||||||
|
│ ▼ │
|
||||||
|
│ AI-Gateway (Flask :5100,独立项目/服务/git 仓库,OpenAI 兼容协议) │
|
||||||
|
│ ├─ /v1/chat/completions:NVIDIA → Gemini(多 Key 轮换)→ 本地 Ollama 降级链 │
|
||||||
|
│ ├─ Bearer token 鉴权(fail-closed),对外 0.0.0.0:5100,非本系统专属 │
|
||||||
|
│ └─ 独立 .env(/opt/ai-gateway/.env,NVIDIA/Gemini key 与 FAM-Edge 各自一份) │
|
||||||
└───────────────────────────────┬───────────────────────────────────────────────┘
|
└───────────────────────────────┬───────────────────────────────────────────────┘
|
||||||
│ HTTP GET /api/oracle/sync?since=&token= (每 30 分钟)
|
│ HTTP GET /api/oracle/sync?since=&token= (每 30 分钟)
|
||||||
▼
|
▼
|
||||||
@@ -130,7 +137,8 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA N
|
|||||||
|
|
||||||
**网络要点(新架构)**:
|
**网络要点(新架构)**:
|
||||||
- NAS → Oracle 出站共两条:① `GET /api/oracle/sync`(拉取增量)+ `POST /api/oracle/people/correct`(命名回推);② `POST /api/ss/motion`(运动侦测事件推送)。均走 Oracle 公网 IP:5000,token 鉴权
|
- NAS → Oracle 出站共两条:① `GET /api/oracle/sync`(拉取增量)+ `POST /api/oracle/people/correct`(命名回推);② `POST /api/ss/motion`(运动侦测事件推送)。均走 Oracle 公网 IP:5000,token 鉴权
|
||||||
- Oracle Ollama :11434 不对外暴露,问答经 FAM-Edge `/api/edge/chat/ask` 代理
|
- 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 主动出站拉取模式
|
- Tailscale 两节点在线但 NAS 无法反向访问 Oracle,故全部走 NAS 主动出站拉取模式
|
||||||
|
|
||||||
### 2.3 主链路时序(新架构 v3:运动事件驱动)
|
### 2.3 主链路时序(新架构 v3:运动事件驱动)
|
||||||
@@ -165,7 +173,7 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA N
|
|||||||
| 模块 | 文件 | 职责 |
|
| 模块 | 文件 | 职责 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 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()` 立即同步 |
|
| 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 |
|
| 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 并即时拉回本地镜像 |
|
| 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」映射并配置兜底 |
|
| 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` 查询状态 |
|
| Motion-BP | `motion_bp.py` | `POST /api/ss/webhook` 可选低延迟补充(SS 行動規則 Webhook 未配置则不触发,兼容 JSON/表单/单条/数组)→ 映射 → 推送 Oracle;`GET /api/ss/status` 查询状态 |
|
||||||
@@ -180,13 +188,13 @@ Orchestrator 视觉阶段按 `fallback` 模式顺序降级:Gemini → NVIDIA N
|
|||||||
|
|
||||||
| 模块 | 文件 | 职责 |
|
| 模块 | 文件 | 职责 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| API-Gateway | `api_gateway/api_gateway.py` | `GET /api/oracle/sync`(增量拉取,since+token 校验);`POST /api/oracle/people/correct`(命名校正);`POST /api/edge/chat/ask`(问答编排);`POST /api/ss/motion`(运动事件接收,token 校验,落库 `ss_motion_events` + 刷新心跳);`GET /api/oracle/frame|avatar`(帧/头像);`GET /health` |
|
| 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-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` |
|
| 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 命名优先不被覆盖 |
|
| Person-Service | `person_service.py` | 汇总全量人物 → LLM 合并为规范名 → `set_canonical`;生成 `known_members_context` 回灌片段提示;manual 命名优先不被覆盖 |
|
||||||
| OracleDB | `oracle_db.py` | SQLite:videos(含 `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` / 心跳 |
|
| OracleDB | `oracle_db.py` | SQLite:videos(含 `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)`;Gemini(Files API)/ NVIDIA(整视频 `video_url`)/ Ollama(纯文本,不参与视频)——输入均为运动片段 |
|
| Model-Adapters | `model_adapters/` | `BaseModelAdapter.analyze_video(video_path, known_members_context, event_start_time)`;Gemini(Files API)/ NVIDIA(整视频 `video_url`)——**只有视觉分析用,2026-08-23 起不再含 Ollama/文字模型**,问答模型完全移交 AI-Gateway |
|
||||||
| QA-Orchestrator | `qa.py` | 遍历所有适配器 `chat()`,Gemini→NVIDIA→Ollama 三级降级(仅问答) |
|
| 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-UI(NAS 端)
|
### 3.3 FAM-UI(NAS 端)
|
||||||
|
|
||||||
@@ -200,6 +208,20 @@ Vue3 + Vite + Tailwind SPA(`fam-ui/src/views/*.vue`,构建产物 `fam-ui/dis
|
|||||||
| 📝 对话历史 | chat_history 倒序展示 |
|
| 📝 对话历史 | chat_history 倒序展示 |
|
||||||
| 📈 统计图表 | 模型来源占比 / 关注事件 / 同步状态 |
|
| 📈 统计图表 | 模型来源占比 / 关注事件 / 同步状态 |
|
||||||
|
|
||||||
|
### 3.4 AI-Gateway(Oracle 端,独立项目/服务,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. 数据库设计
|
## 4. 数据库设计
|
||||||
@@ -243,7 +265,7 @@ chat_history 独立表(问答上下文摘要留存)
|
|||||||
### 4.3 compute_provider
|
### 4.3 compute_provider
|
||||||
|
|
||||||
- `sync_videos.compute_provider`:字符串,记录该视频实际成功调用的视觉模型(`gemini` / `nvidia`);本地 Ollama 不参与视频分析,不会出现在该字段
|
- `sync_videos.compute_provider`:字符串,记录该视频实际成功调用的视觉模型(`gemini` / `nvidia`);本地 Ollama 不参与视频分析,不会出现在该字段
|
||||||
- 问答链路(Gemini→NVIDIA→Ollama 兜底)的 provider 体现在 `/api/edge/chat/ask` 响应的 `provider` 字段
|
- 问答链路(AI-Gateway 内部 NVIDIA→Gemini→Ollama 降级链)的 provider 体现在 `/api/edge/chat/ask` 响应的 `provider` 字段(由 AI-Gateway 原样透传回来)
|
||||||
|
|
||||||
### 4.4 兼容性注意
|
### 4.4 兼容性注意
|
||||||
|
|
||||||
@@ -284,7 +306,7 @@ chat_history 独立表(问答上下文摘要留存)
|
|||||||
- 401:token 校验失败
|
- 401:token 校验失败
|
||||||
|
|
||||||
**POST /api/oracle/people/correct** — 命名校正回推:`{"label":"人物A","canonical_name":"汤圆","token":...}`(manual 优先,不被 LLM 覆盖)→ `{"status":"ok"}`
|
**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/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: []` 空数组即心跳
|
**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/frame** — 事件帧图:`?video_id=&ts=&w=` → jpeg(`ts` 为绝对时间,`frame_service` 按 `ts − event_start_time` 偏移从视频文件取帧)
|
||||||
**GET /api/oracle/avatar** — 人物头像:`?label=&w=` → jpeg(从该人物候选事件 `person_appearances` bbox 裁剪)
|
**GET /api/oracle/avatar** — 人物头像:`?label=&w=` → jpeg(从该人物候选事件 `person_appearances` bbox 裁剪)
|
||||||
@@ -356,19 +378,19 @@ chat_history 独立表(问答上下文摘要留存)
|
|||||||
- **压缩**:长边 > 1024px 才缩放,JPEG 质量 80
|
- **压缩**:长边 > 1024px 才缩放,JPEG 质量 80
|
||||||
- **异常兜底**:ffprobe 失败退化为 60s 间隔抽帧;帧差异常退化为等距 5 帧
|
- **异常兜底**:ffprobe 失败退化为 60s 间隔抽帧;帧差异常退化为等距 5 帧
|
||||||
|
|
||||||
### 6.2 本地 Ollama(仅智能问答兜底,ARM CPU)
|
### 6.2 本地 Ollama(仅智能问答兜底,归属 AI-Gateway)
|
||||||
|
|
||||||
本地 Ollama(qwen2.5:7b,纯文本模型)**不参与视觉分析、不参与云端结果融合**。它只在**智能问答**场景下、且 Gemini 与 NVIDIA 两云端模型都失败时才被启用作为兜底。视觉分析与结构化输出全部由云端模型承担。以下参数作为问答任务的调优依据保留。
|
本地 Ollama(qwen2.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.3GB,12GB 内存够用) |
|
| `OLLAMA_KEEP_ALIVE=-1` | 模型常驻内存 | 消除 55s 冷启动(常驻约 4.3GB,12GB 内存够用) |
|
||||||
| `num_predict=512` | 限制生成 token | ARM 约 5 tok/s,过长生成会拖慢问答响应 |
|
| `num_predict=512` | 限制生成 token | ARM 约 5 tok/s,过长生成会拖慢问答响应 |
|
||||||
| 视觉/模型 timeout | 600s | 实测 1024px 帧视觉编码 ~36s + 生成 ~12s/60token(问答链路改用 config 中各模型 timeout) |
|
| AI-Gateway 启动预热 | 后台线程 `warm_up()` | `OLLAMA_KEEP_ALIVE=-1` 只保证加载后不换出,不负责主动预加载;NVIDIA/Gemini 一直成功时 Ollama 永远不会被自然触发,加了启动时预热避免真正兜底时才发现要等 1-2 分钟冷启动 |
|
||||||
| gunicorn(Edge) | `--timeout 1800` | 同步分析模式,默认 30s 会杀 worker |
|
| gunicorn(Edge) | `--timeout 1800` | 同步分析模式,默认 30s 会杀 worker |
|
||||||
| push_timeout(NAS) | 1800s | 覆盖最坏情况(30min 视频实测 929s) |
|
| push_timeout(NAS) | 1800s | 覆盖最坏情况(30min 视频实测 929s) |
|
||||||
|
|
||||||
**已知问题**:原 llava-phi3 多图单请求基本失效(N 张图一次调用输出长度仅 3~4)。**已替换为 qwen2.5:7b**(纯文本模型,专职问答兜底,不涉及视觉多图问题;视频分析已全部由云端 VLM 承担)。
|
**历史问题**:原 llava-phi3 多图单请求基本失效(N 张图一次调用输出长度仅 3~4)。已替换为 qwen2.5:7b(纯文本模型,专职问答兜底,不涉及视觉多图问题;视频分析已全部由云端 VLM 承担)。
|
||||||
|
|
||||||
### 6.3 模型适配器架构
|
### 6.3 模型适配器架构
|
||||||
|
|
||||||
@@ -388,13 +410,14 @@ chat_history 独立表(问答上下文摘要留存)
|
|||||||
|
|
||||||
| provider | 适配器类 | role | SDK / 协议 | 状态 |
|
| provider | 适配器类 | role | SDK / 协议 | 状态 |
|
||||||
|----------|---------|------|------------|------|
|
|----------|---------|------|------------|------|
|
||||||
| `ollama` | `OllamaAdapter` | **text** | requests 直调 REST `/api/chat` | 已实现 |
|
| `gemini` | `GeminiAdapter` | **vision** | requests 直调 REST `:generateContent` | 已实现 |
|
||||||
| `gemini` | `GeminiAdapter` | **vision** | requests 直调 REST `:generateContent` | 待实现 |
|
| `nvidia` | `NvidiaVisionAdapter` | **vision** | **openai SDK**(NIM 兼容 OpenAI API 规范) | 已实现 |
|
||||||
| `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 语义**:
|
**role 语义**:
|
||||||
- `vision`:参与视觉分析阶段,按 fallback 顺序降级,直出结构化 JSON
|
- `vision`:参与视觉分析阶段,按 fallback 顺序降级,直出结构化 JSON
|
||||||
- `text`:仅参与智能问答(`usage: qa_fallback`),且为 Gemini/NVIDIA 都失败时的兜底,不参与视觉分析、不参与云端结果融合
|
- `text`:**已不在 FAM-Edge 出现**——原来"仅参与智能问答、Gemini/NVIDIA 都失败时兜底"的语义现在完全由独立的 AI-Gateway 服务内部实现(见 §3.4)
|
||||||
|
|
||||||
**NvidiaVisionAdapter 关键实现**(`fam_edge/adapters/nvidia_adapter.py`):
|
**NvidiaVisionAdapter 关键实现**(`fam_edge/adapters/nvidia_adapter.py`):
|
||||||
|
|
||||||
@@ -433,30 +456,40 @@ format_cloud_result:字段归一化 → 补 source_providers=[provider] / comp
|
|||||||
合法入库结构,随响应返回 NAS 直接落库(本地模型不介入)
|
合法入库结构,随响应返回 NAS 直接落库(本地模型不介入)
|
||||||
```
|
```
|
||||||
|
|
||||||
**智能问答降级链路(chat 场景)**:
|
**智能问答降级链路(chat 场景,2026-08-23 起完全在独立的 AI-Gateway 服务内部,FAM-Edge 只转发)**:
|
||||||
|
|
||||||
```
|
```
|
||||||
Gemini (role=vision, 也参与问答)
|
FAM-Edge /api/edge/chat/ask(/stream)
|
||||||
│ 失败 / 熔断 OPEN / 超时
|
│ HTTP 转发(qa.py,本机回环)
|
||||||
▼
|
▼
|
||||||
NVIDIA NIM (role=vision, 也参与问答)
|
AI-Gateway /v1/chat/completions
|
||||||
│ 失败 / 熔断 OPEN / 超时
|
│
|
||||||
▼
|
▼
|
||||||
本地 Ollama (qwen2.5:7b, role=text, usage=qa_fallback) —— 仅当两云端都失败才启用
|
NVIDIA(问答专用文字模型链,跟视觉分析的 omni 模型完全独立)
|
||||||
|
│ 完全没有输出 / 熔断 OPEN / 超时
|
||||||
|
▼
|
||||||
|
Gemini(问答专用非 flash 文字模型链,多 Key 随机轮换,跟视觉分析的 flash 模型完全独立)
|
||||||
|
│ 完全没有输出 / 熔断 OPEN / 超时
|
||||||
|
▼
|
||||||
|
本地 Ollama (qwen2.5:7b) —— 仅当前两者都失败才启用
|
||||||
│ 失败
|
│ 失败
|
||||||
▼
|
▼
|
||||||
返回"所有模型均不可用"
|
返回"所有模型均不可用"(HTTP 503)
|
||||||
```
|
```
|
||||||
|
|
||||||
**熔断器策略**(按 provider 独立,仅云端模型启用):
|
> 已经开始吐字之后中途失败:不换下一个 provider 接着写(避免答案风格前后不连贯),直接结束这次生成——这个语义在 AI-Gateway 的 `orchestrator.py` 里实现,FAM-Edge 的 `qa.py` 只是原样转发这个行为,不重复实现。
|
||||||
|
|
||||||
| provider | role | threshold | cooldown | enabled |
|
**熔断器策略**(按 provider 独立):
|
||||||
|----------|------|-----------|----------|---------|
|
|
||||||
| 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。
|
| 服务 | 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` 校验非空数组。
|
**多模型标签兼容**:`compute_provider` 与 `event_details.source_providers` 的 JSON 数组值新增 `"nvidia"` 标签(与 `"ollama"` / `"gemini"` 并列);`validate_schema` 校验非空数组。
|
||||||
|
|
||||||
@@ -506,7 +539,8 @@ task_id=289(30s 测试片段)全链路打通:推送 5.7MB → Edge 分析
|
|||||||
| FAM-Core | NAS | `/volume1/web/sentinel-home-ai/fam-core/` | `bash start_core.sh`(gunicorn -w 1 :8000,source .env 注入 DSM_*/ORACLE_SYNC_TOKEN/FAM_AUTH_*) |
|
| FAM-Core | NAS | `/volume1/web/sentinel-home-ai/fam-core/` | `bash start_core.sh`(gunicorn -w 1 :8000,source .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-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,会端口冲突) |
|
| 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` |
|
| **AI-Gateway** | Oracle | `/opt/ai-gateway/` | **systemd `ai-gateway.service` 守护**(Restart=always),独立 venv(Python 3.8);gunicorn 绑定 `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):
|
> **外网访问(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(本系统)**
|
> - `3000` → NAS Gitea、`8500` → NAS WordPress(8088)、**`8000` → NAS FAM-Core(本系统)**
|
||||||
@@ -514,14 +548,15 @@ task_id=289(30s 测试片段)全链路打通:推送 5.7MB → Edge 分析
|
|||||||
>
|
>
|
||||||
> **登录校验(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 需重新登录。
|
> **登录校验(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)。
|
> 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 依赖
|
### 8.2 依赖
|
||||||
|
|
||||||
- **FAM-Core(NAS, Python 3.10 venv)**:Flask, Gunicorn, **PyMySQL**(45KB 纯 Python 替代 19MB mysql-connector), PyYAML, requests
|
- **FAM-Core(NAS, Python 3.10 venv)**:Flask, Gunicorn, **PyMySQL**(45KB 纯 Python 替代 19MB mysql-connector), PyYAML, requests
|
||||||
- **FAM-UI(NAS, Node)**:Vue3 + Vite + Tailwind(`fam-ui/`,构建产物 dist 不入库);**不再依赖 Streamlit**
|
- **FAM-UI(NAS, Node)**:Vue3 + Vite + Tailwind(`fam-ui/`,构建产物 dist 不入库);**不再依赖 Streamlit**
|
||||||
- **FAM-Edge(Oracle, Python venv)**:Flask, Gunicorn, requests, PyYAML, opencv-python, numpy, **openai**(NVIDIA NIM 兼容 OpenAI API 规范);Gemini 用 requests 直调 REST
|
- **FAM-Edge(Oracle, Python venv)**:Flask, Gunicorn, requests, PyYAML, opencv-python, numpy, **openai**(NVIDIA NIM 兼容 OpenAI API 规范,仅视觉分析用);Gemini 用 requests 直调 REST;**问答不再直接调模型,只用 requests 转发到 AI-Gateway**
|
||||||
- **系统级**:FFmpeg(两端;Oracle 端用于运动片段分割 + 帧图/头像)、Ollama + qwen2.5:7b(Oracle)、MariaDB 10.11(NAS)、rclone(Oracle,同步 Google Drive 素材)
|
- **AI-Gateway(Oracle, Python 3.8 venv,独立部署单元)**:Flask, Gunicorn, requests, PyYAML, **openai**(NVIDIA 问答模型用)
|
||||||
|
- **系统级**:FFmpeg(两端;Oracle 端用于运动片段分割 + 帧图/头像)、Ollama + qwen2.5:7b(Oracle,**被 AI-Gateway 调用**)、MariaDB 10.11(NAS)、rclone(Oracle,同步 Google Drive 素材)
|
||||||
|
|
||||||
### 8.3 配置文件要点
|
### 8.3 配置文件要点
|
||||||
|
|
||||||
@@ -556,7 +591,7 @@ motion_notifier: # 运动监测(轮询主路径)
|
|||||||
heartbeat_interval_sec: 300 # 心跳(证明推送链路存活,< Oracle 侧 900s 阈值)
|
heartbeat_interval_sec: 300 # 心跳(证明推送链路存活,< Oracle 侧 900s 阈值)
|
||||||
```
|
```
|
||||||
|
|
||||||
**fam-edge/config/config.yaml**(Oracle —— 素材分割 + 片段分析 + 同步 + 人物服务):
|
**fam-edge/config/config.yaml**(Oracle —— 素材分割 + 片段分析 + 同步 + 人物服务;**2026-08-23 起不再含任何问答模型配置**):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
server:
|
server:
|
||||||
@@ -583,10 +618,15 @@ motion_segment: # 运动片段分割(运动事件驱动架构
|
|||||||
keep_audio: true # 保留音频(pcm_alaw -> aac 64k 转码)
|
keep_audio: true # 保留音频(pcm_alaw -> aac 64k 转码)
|
||||||
min_duration_sec: 1
|
min_duration_sec: 1
|
||||||
unfinished_grace_sec: 10 # start+duration 距当前 ≤10s 视为已结束
|
unfinished_grace_sec: 10 # start+duration 距当前 ≤10s 视为已结束
|
||||||
models:
|
ai_gateway: # 问答转发客户端配置(2026-08-23 新增,取代原来的问答专用 models 条目)
|
||||||
|
base_url: "http://127.0.0.1:5100"
|
||||||
|
token: "${AI_GATEWAY_TOKEN}"
|
||||||
|
timeout: 60
|
||||||
|
models: # 只剩视觉分析用的两个 provider,role 全是 vision
|
||||||
- provider: "gemini"
|
- provider: "gemini"
|
||||||
role: "vision"
|
role: "vision"
|
||||||
model_name: "gemini-flash-latest"
|
model_name: "gemini-flash-latest"
|
||||||
|
fallback_models: ["gemini-flash-lite-latest"]
|
||||||
api_key: "${GEMINI_API_KEY}"
|
api_key: "${GEMINI_API_KEY}"
|
||||||
extra_api_keys: ["${GEMINI_API_KEY_2}", "${GEMINI_API_KEY_3}", "${GEMINI_API_KEY_4}"]
|
extra_api_keys: ["${GEMINI_API_KEY_2}", "${GEMINI_API_KEY_3}", "${GEMINI_API_KEY_4}"]
|
||||||
timeout: 600
|
timeout: 600
|
||||||
@@ -596,30 +636,36 @@ models:
|
|||||||
base_url: "https://integrate.api.nvidia.com/v1"
|
base_url: "https://integrate.api.nvidia.com/v1"
|
||||||
api_key: "${NVIDIA_API_KEY}"
|
api_key: "${NVIDIA_API_KEY}"
|
||||||
timeout: 600
|
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 兜底,不参与视觉分析、不参与云端结果融合)
|
**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"
|
- provider: "ollama"
|
||||||
role: "text" # 仅问答兜底
|
|
||||||
usage: "qa_fallback" # Gemini/NVIDIA 都失败时才启用
|
|
||||||
enabled: true
|
enabled: true
|
||||||
model_name: "qwen2.5:7b"
|
model_name: "qwen2.5:7b"
|
||||||
base_url: "http://localhost:11434"
|
base_url: "http://localhost:11434"
|
||||||
timeout: 120
|
timeout: 120
|
||||||
num_predict: 512
|
|
||||||
circuit_breaker:
|
|
||||||
enabled: false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 部署时 `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 环境变量文件):
|
**环境变量**(Oracle 节点,写入 `~/.bashrc` 或 systemd 环境变量文件):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -716,10 +762,12 @@ cd fam-ui && npm run build # 产物 fam-ui/dist/
|
|||||||
|
|
||||||
### 10.4 Gitea 代码仓库
|
### 10.4 Gitea 代码仓库
|
||||||
|
|
||||||
| 项目 | 值 |
|
| 仓库 | URL | 说明 |
|
||||||
|------|-----|
|
|------|-----|------|
|
||||||
| URL | http://192.168.50.64:3000/ericwyuan/sentinel-home-ai |
|
| sentinel-home-ai(本仓库) | http://192.168.50.64:3000/ericwyuan/sentinel-home-ai | monorepo:fam-core + fam-edge + fam-ui |
|
||||||
| 账号 / 密码 | ericwyuan / iLoveJava5 |
|
| ai-gateway(2026-08-23 新增) | http://192.168.50.64:3000/ericwyuan/ai-gateway | 独立仓库/独立部署单元,问答网关服务 |
|
||||||
|
|
||||||
|
账号 / 密码:ericwyuan / iLoveJava5(两个仓库共用)
|
||||||
|
|
||||||
### 10.5 Gemini API(Google AI Studio)
|
### 10.5 Gemini API(Google AI Studio)
|
||||||
|
|
||||||
@@ -779,8 +827,9 @@ export NVIDIA_API_KEY="nvapi-9cFAdO5xdbwPuxS8KGRTnlVimn1gJzbbbzWNhPwHa_Yl3pTe-Pf
|
|||||||
|
|
||||||
## 12. 当前进度与 v1.1 计划
|
## 12. 当前进度与 v1.1 计划
|
||||||
|
|
||||||
### 已完成(截至 2026-08-22,v3 运动事件驱动架构)
|
### 已完成(截至 2026-08-23)
|
||||||
|
|
||||||
|
- **问答链路抽离为独立 ai-gateway 服务**(2026-08-23):FAM-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 删除
|
||||||
- **运动事件驱动**(v3,2026-08-22):不再分析整段视频。NAS MotionNotifier 轮询 SS 事件列表(60s,游标续用/失败重试/心跳)推送 `ss_motion_events`;Oracle 整段素材按运动事件 ffmpeg 分割运动片段(`-c:v copy -c:a aac`,只分割已结束事件,`motion_event_id` 幂等),**只分析运动片段**;前端契约不变
|
- **运动事件驱动**(v3,2026-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):整段提取的旧数据(Oracle videos/events/people + NAS 镜像)已清空并按运动视频重新提取;历史素材(NAS 轮询启动前)无运动事件,时间轴已过滤其空会话
|
||||||
- **人物管理重设计**(2026-08-22):人物卡新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳时间轴定位);新增 `GET /api/ui/people/clips`;Timeline 支持 `?video=` 定位
|
- **人物管理重设计**(2026-08-22):人物卡新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳时间轴定位);新增 `GET /api/ui/people/clips`;Timeline 支持 `?video=` 定位
|
||||||
|
|||||||
Reference in New Issue
Block a user