1. fam-edge 新增 frame_marker.py: Edge 端人脸检测画红框+统计人脸数(NAS ARM 太弱只存图零计算),orchestrator 分析后回传前标记,新增 /api/edge/mark_frames 批量补标端点 2. fam-core 新增 tools/backfill_mark_frames.py: 存量关键帧批量补红框(幂等+备份 frames_orig) 3. db_layer.name_member 增强: 支持自动注册新标签/重命名回溯(旧真名一并替换)/多人组合字符串 REPLACE 4. fam-ui 成员命名页改为人物管理页: 所有人物照片墙+命名重命名+统计去重 5. event_receiver/member_manager 配套适配
Sentinel Home AI — 家庭多模态智能监控系统
多模型容灾降级 + 交互式命名 + AI 对话的家庭监控系统。 本文档为项目需求文档与 README 的整合版,按当前代码实际状态(v1.0 E2E 已打通)编写。 最后更新:2026-08-20(云端多模型方案整合)
1. 项目简介与设计目标
系统持续分析家庭监控摄像头(Synology Surveillance Station)录制的视频,抽取关键帧后调用视觉语言模型(VLM)分析画面中的人物、动作、衣着,落库为结构化事件;用户通过 Web UI 浏览事件、给"人物A/B/C"命名(批量回溯历史记录)、以及用自然语言向 AI 询问"汤圆今天干嘛了"。
1.1 首期范围(已基本完成)
- FAM-Core(NAS 端单进程):Task-Scheduler / Dispatcher / Event-Receiver / Chat-Handler / Member-Manager / Video-Server 六个子模块
- FAM-Edge(Oracle 端单进程):接收视频上传 → FFmpeg 快速抽帧 → OpenCV 关键帧筛选 → 云端 VLM 视觉分析(Gemini/NVIDIA NIM)直出结构化 JSON → Edge 仅做格式化/校验 → 结果同步返回 全链路(本地模型不参与视频分析)
- FAM-UI(NAS 端):Streamlit 直读 DB,事件列表 + 成员命名页 + AI 对话页 + 对话历史
- 数据库六张表:process_tasks / monitor_events / event_details / chat_history / family_members / daily_summaries(预留)
- 任务状态机:PENDING → PROCESSING → SUCCESS/FAILED,含退避重试与僵尸任务回收
- AI 对话:查 event_details 拼上下文 → 经 FAM-Edge 问答编排(Gemini → NVIDIA → 本地 Ollama 兜底)生成回答 → 返回并写 chat_history
- 交互式成员命名:VLM 按特征提取"人物A/B/C"落库,用户命名后批量回溯更新历史,后续分析直接用真名
1.2 不在首期范围(推迟 v1.1+)
- Nginx 静态服务(Video-Server 用 Flask
send_from_directory替代,且推送模式下已不再必需) - 资源保护策略 B(CPU/内存过载返回 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 + 融合 3min);30min/360MB 视频实测 929s | 已切换到云端 VLM 直出结构化 JSON(无本地融合步骤),吞吐瓶颈转移至云端调用延迟,整体明显改善 |
| Tailscale 端口不通 | NAS tailscaled 以 userspace 模式运行(无 TUN 网卡),Oracle 无法反向访问 NAS;当前 NAS→Oracle 走公网 IP | 推送模式已规避反向访问,但流量走公网 |
| 历史视频积压(已处理) | 正式目录 /volume1/surveillance/Generic_ONVIF-001 原有约 285 个历史视频(~100GB),已切回生产目录并 forward-only 处理:历史任务占位 FAILED,scheduler dedup 自动跳过,仅处理新增视频 |
已解决;生产目录已生效,新视频正常入链 |
规划方向(新框架):云端大模型负责视觉识别与结构化输出、本地大模型仅做智能问答兜底。按任务类型分工:
| 任务 | 执行方 | 模型 | 说明 |
|---|---|---|---|
| 视觉分析 + 结构化输出(看图识人/动作/衣着 → 直出 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 二次汇总 |
| AI 对话("汤圆今天干嘛了") | 云端优先 + 本地兜底 | Gemini → NVIDIA NIM → 本地 Ollama | 两云端任一成功即用;仅当 Gemini 与 NVIDIA 都失败才回退本地 Ollama qwen2.5:7b |
- Google Gemini(
gemini-flash-latest,API Key 已验证,支持多图单请求,视觉 + 问答均参与) - NVIDIA NIM(
meta/llama-3.2-11b-vision-instruct,OpenAI 兼容 API,云端 GPU 推理,单请求限 1 图故逐帧调用;视觉 + 问答均参与) - 本地 Ollama(qwen2.5:7b)仅参与智能问答,且仅作兜底:视觉链路两云端全失败 → 任务 FAILED 走重试,绝不回退本地模型做视觉/融合
Orchestrator 视觉阶段按 fallback 模式顺序降级:Gemini → NVIDIA NIM;云端模型直出结构化 JSON 后由 format_cloud_result 格式化。问答阶段按 gemini → nvidia → ollama 顺序,仅末位本地模型作兜底。
2. 系统架构
2.1 物理节点与部署
| 节点 | 角色 | 硬件 | IP | 服务与端口 |
|---|---|---|---|---|
| Synology NAS | FAM-Core + FAM-UI + 数据库 | DS220+ (Geminilake), DSM 7 | 家庭局域网 192.168.50.64 / Tailscale 100.70.234.39 | FAM-Core :8000, FAM-UI :8501, MariaDB :3306, Surveillance Station |
| Oracle Cloud | FAM-Edge | Ampere A1 2C12G ARM64(无 GPU), Ubuntu 20.04 | 公网 129.146.203.203 / Tailscale 100.74.137.126 | FAM-Edge :5000, Ollama :11434(仅本地) |
| 家庭网络 | 用户入口 | 普通终端 | 192.168.50.0/24 | 浏览器访问 http://192.168.50.64:8501 |
网络要点(推送模式):
- 服务间通信只有一条:NAS → Oracle 公网 IP:5000(HTTP 上传视频)。Edge 不需要反向访问 NAS(无 webhook、无视频拉取)
- Oracle 端 Ollama 端口 11434 不对外暴露,聊天请求经 FAM-Edge
/api/edge/chat代理转发 - Tailscale 两节点已安装在线,但 NAS tailscaled 为 userspace 模式且防火墙端口不通,暂走公网 IP
2.2 部署拓扑与数据流(推送模式)
┌─────────────────────────────── NAS (192.168.50.64) ──────────────────────────────┐
│ │
│ Surveillance Station ──► /volume1/surveillance/Generic_ONVIF-001/ │
│ (YYYYMMDDAM / YYYYMMDDPM 两级目录,~30min/350MB) │
│ │ │
│ ▼ │
│ FAM-Core (Flask :8000, gunicorn) │
│ ├─ Task-Scheduler: 60s 轮询视频目录,稳定文件建 PENDING 任务 │
│ ├─ Dispatcher: 30s 轮询,multipart 上传视频 ──────────┐ │
│ │ (push_timeout=1800s,同步等待响应) │ │
│ ├─ 僵尸回收: PROCESSING 超 push_timeout+120s 重置 PENDING │
│ ├─ Event-Receiver: /api/core/callback/event(兼容保留) │
│ ├─ Chat-Handler ── /api/edge/chat 代理 ──────────────┐ │
│ ├─ Member-Manager / Video-Server(/media, token) │ │
│ ▼ │ │
│ MariaDB (sentinel_home_ai, 6 张表) │ │
│ │ │
│ FAM-UI (Streamlit :8501) 直读 DB │ │
└─────────────────────────────────────────────────────────┼─────────────────────────┘
│ HTTP (公网)
▼
┌──────────────────────── Oracle Cloud (129.146.203.203) ──────────────────────────┐
│ FAM-Edge (Flask :5000, gunicorn --timeout 1800, 单 worker) │
│ ├─ POST /api/edge/video/push(multipart 视频,同步分析,结果随响应返回) │
│ ├─ AI-Orchestrator: 健康检查 → 抽帧 → 选帧 → 压缩 → 云端VLM视觉直出结构化JSON → 格式化校验(无本地融合) │
│ ├─ POST /api/edge/chat/ask(问答编排: Gemini→NVIDIA→本地Ollama 兜底) │
│ ▼ │
│ 云端: Gemini / NVIDIA NIM (视觉直出结构化 + 问答) 本地: Ollama :11434 (qwen2.5:7b, 仅问答兜底) │
└───────────────────────────────────────────────────────────────────────────────────┘
2.3 主链路时序(推送模式)
- Scheduler 扫描到新视频(修改时间 > 60s 且大小稳定)→ 写
process_tasks(PENDING) - Dispatcher 领取 PENDING 任务 → 状态置 PROCESSING → 读本地视频文件,multipart POST 到 Edge
/api/edge/video/push,payload 含 task_id / camera_name / event_start_time(文件 mtime)/ known_members_context - Edge 同步执行:
- a. 保存上传视频到临时目录(超时 60s)
- b. FFmpeg 快速 seek(
-ss <ts> -frames:v 1)粗抽候选帧,帧数随视频时长自适应 - c. OpenCV MSE 帧差分析筛选关键帧 → 压缩(长边 ≤ 1024px,JPEG 质量 80)
- d. 云端视觉模型按
orchestrator.mode(fallback)顺序降级:Gemini(timeout 30s)→ NVIDIA NIM(timeout 20s);首个直出结构化 JSON 成功的模型即采用,两云端全失败 → 任务 FAILED 走重试(绝不回退本地 Ollama,本地模型不参与视频分析) - e.
format_cloud_result格式化校验(无模型调用):字段归一化、补source_providers=[provider]/compute_provider=[provider]、缺失entities_json由frame_details推导、缺失global_summary时事实拼接 → 合法入库 schema - f.
event_end_time= event_start_time + 视频时长(Edge 推算) - g.
finally清理临时文件
- Edge 把结果 JSON 直接作为 HTTP 响应返回(无 webhook)
- Dispatcher 收到响应 → 调用
apply_success_event()写monitor_events(1 条聚合)+event_details(每关键帧 1 条)+ upsert 未命名成员 → 任务置 SUCCESS;失败则退避重试(min(60×(retry+1)×2, 600)s,超 3 次 FAILED)
容错设计:
- Dispatcher 僵尸回收:PROCESSING 状态超过
push_timeout + 120s自动重置 PENDING(应对进程重启/Edge 重启导致 in-flight 请求丢失) - fam-core 日志双写:stdout +
fam-core/logs/fam-core.log(daemon 模式下 stdout 不可见) - 所有日志带
task_id作为 trace_id,各阶段耗时打 INFO
3. 模块设计
3.1 FAM-Core(NAS 端)
| 模块 | 文件 | 职责 |
|---|---|---|
| Task-Scheduler | scheduler/scheduler.py |
60s 轮询视频目录(os.walk 递归,支持 AM/PM 子目录),video_path 去重,稳定文件建 PENDING 任务 |
| Dispatcher | dispatcher/dispatcher.py |
30s 轮询 PENDING;multipart 上传视频至 Edge push 端点;收响应后经 apply_success_event 落库;僵尸 PROCESSING 回收;退避重试 |
| Event-Receiver | event_receiver/event_receiver.py |
/api/core/callback/event(webhook 兼容保留);核心逻辑抽为 apply_success_event(data) 供 Dispatcher 推送模式复用;未命名 abstract_label 自动 upsert family_members |
| Chat-Handler | chat_handler/chat_handler.py |
/api/chat/ask 查 event_details 拼上下文 → 经 Edge /api/edge/chat/ask 问答编排(Gemini→NVIDIA→本地 Ollama 兜底)→ 写 chat_history;明细 > 50 条按小时聚合 |
| Member-Manager | member_manager/member_manager.py |
/api/member/unnamed / /api/member/name / /api/member/list;命名后批量回溯 UPDATE event_details(MariaDB 不支持 $[*] JSON 路径,Python 层逐行更新) |
| Video-Server | video_server/video_server.py |
/media/<path>?token=xxx 静态视频服务(推送模式下主链路不再使用,保留备用) |
| 公共层 | db_layer.py / config_loader.py / logger.py |
PyMySQL 连接(unix_socket);datetime 空串归一化 NULL + NOT NULL 列兜底;文件日志 |
3.2 FAM-Edge(Oracle 端)
| 模块 | 文件 | 职责 |
|---|---|---|
| API-Gateway | api_gateway/api_gateway.py |
POST /api/edge/video/push(multipart 上传 + 同步分析 + 结果返回);POST /api/edge/video/analyze(旧拉取模式,兼容保留);POST /api/edge/chat(Ollama 代理);GET /health;单并发控制(处理中返回 429) |
| Video-Preprocessor | video_preprocessor/preprocessor.py |
save_upload 保存上传视频;FFmpeg 快速 seek 粗抽候选帧(帧数自适应);OpenCV MSE 帧差筛选关键帧(首末帧必选);压缩;compute_timestamps 用 start+偏移算绝对时间戳;video_duration 供 event_end_time 推算 |
| AI-Orchestrator | ai_orchestrator/orchestrator.py |
模型健康检查 → 云端视觉适配器按 orchestrator.mode(fallback 顺序降级)调度,直出结构化 JSON → format_cloud_result 格式化校验(无本地融合)→ JSON schema 校验;run_qa 实现问答编排(Gemini→NVIDIA→本地 Ollama 兜底);process_push_task 为推送模式入口(不触发 webhook);记录各模型实际执行耗时与成功状态到 compute_provider 数组 |
| Model-Adapters | model_adapters/ |
BaseModelAdapter 抽象基类(__init__ / health_check / analyze_frames / chat / get_timeout / 熔断器实例);build_adapter 工厂函数按 provider 字段分发实例化;视觉适配器(gemini/nvidia)直出结构化 JSON,文本适配器(ollama)仅智能问答兜底 |
| └ OllamaAdapter | model_adapters/ollama_adapter.py |
requests 直调本地 REST /api/generate,num_predict 可配;role: text, usage: qa_fallback(仅智能问答兜底,不参与视觉分析、不参与融合) |
| └ GeminiAdapter | model_adapters/gemini_adapter.py |
requests 直调 Google REST :generateContent,多图单请求直出结构化 JSON;role: vision;chat() 参与问答 |
| └ NvidiaVisionAdapter | model_adapters/nvidia_adapter.py |
基于 openai SDK(NIM 兼容 OpenAI API 规范),base_url=https://integrate.api.nvidia.com/v1,api_key 从 ${NVIDIA_API_KEY} 展开;逐帧返回结构化单帧 JSON 并聚合为 frame_details(NIM 限 1 图/请求);health_check 调 client.models.list();role: vision;chat() 参与问答 |
| Storage-Cleaner | storage_cleaner/ |
finally 删除临时视频与帧图片 |
3.3 FAM-UI(NAS 端)
Streamlit 应用(fam-ui/src/app.py),侧边栏切换页面:
| 页面 | 功能 |
|---|---|
| 📊 事件列表 | 按日期筛选 + 分页(20 条/页)展示 monitor_events |
| 👤 成员命名 | 列出未命名人物 + 特征描述,输入真名后调 /api/member/name 批量回溯 |
| 💬 AI 对话 | 输入框 + 调 /api/chat/ask;按 queried_person 预设快捷提问 |
| 📜 对话历史 | chat_history 倒序展示 |
| 📈 统计图表 | compute_provider 占比(bar_chart) |
4. 数据库设计
库名 sentinel_home_ai,MariaDB 10.11.11,utf8mb4。完整 DDL 见 scripts/ddl.sql。
4.1 表清单
| 表 | 用途 | 关键字段 |
|---|---|---|
process_tasks |
视频处理任务 | task_id, video_path, video_url, status(PENDING/PROCESSING/SUCCESS/FAILED), retry_count, max_retries, next_retry_at, error_message, failure_stage(ENUM) |
monitor_events |
事件聚合(每任务 1 条) | event_id, task_id, event_start_time, event_end_time, camera_name, global_summary, entities_json(JSON), compute_provider(JSON 数组) |
event_details |
每关键帧一条明细 | detail_id, event_id, task_id, frame_index, frame_timestamp, person, action, clothing, is_attention_event, source_providers(JSON) |
family_members |
交互式命名 | member_id, abstract_label(如"人物A"), real_name(NULL=未命名), feature_description, first_seen_at, named_at, named_by |
chat_history |
AI 问答记录 | chat_id, user_question, ai_answer, context_summary, queried_date, queried_person |
daily_summaries |
每日摘要(预留) | target_date, summary_text |
4.2 表关系与命名回溯
process_tasks (1) ─── (N) monitor_events (1) ─── (N) event_details
family_members 独立表:
- event_details.person 存 abstract_label(未命名)或 real_name(命名后)
- 命名后: UPDATE event_details SET person = real_name WHERE person = abstract_label
- monitor_events.entities_json 由 Python 层解析逐行更新(MariaDB 不支持 $[*] 路径)
chat_history 独立表
4.3 compute_provider / source_providers
monitor_events.compute_provider:JSON 数组,记录本次任务实际成功调用(视觉分析)的云端模型,如["gemini"]或["nvidia"];本地 Ollama 不参与视频分析,不会出现在该字段event_details.source_providers:该条明细被哪些模型识别到(可能少于 compute_provider)- 多模型交叉验证:多模型一致 → 可信度高;仅单一模型描述 → source_providers 仅含该模型;冲突 → 多数派为准
4.4 兼容性注意
- MariaDB 10.11 严格模式:空字符串不能插 DATETIME 列(1292 错误)。
db_layer._dt_or_none将空串归一化 NULL;event_end_timeNOT NULL 列按 end→start→NOW 兜底;frame_timestamp空值兜底 NOW - MariaDB 不支持 MySQL 的
$[*]JSON 通配路径与->操作符,JSON 字段在 Python 层处理
5. API 规范(实际实现)
5.1 FAM-Edge(Oracle :5000)
POST /api/edge/video/push(主链路,推送模式)
- 请求:
multipart/form-data,字段video(文件) /task_id/camera_name/event_start_time/known_members_context - 处理:同步执行完整分析流水线(可能耗时数分钟,gunicorn timeout 1800)
- 响应(200):
{
"task_id": 289,
"status": "success",
"event_start_time": "2026-08-20 01:06:44",
"event_end_time": "2026-08-20 01:07:13",
"camera_name": "客厅",
"global_summary": "...",
"entities_json": [{"person": "汤圆", "action": "...", "clothing": "..."}],
"frame_details": [
{"frame_index": 1, "frame_timestamp": "...", "person": "...", "action": "...",
"clothing": "...", "is_attention_event": false, "source_providers": ["gemini"]}
],
"compute_provider": ["gemini"]
}
- 失败:
{"task_id": ..., "status": "failed", "failure_stage": "vlm_visual", "error_message": "..."} - 429:已有任务处理中(单并发);503:全部模型不健康
POST /api/edge/video/analyze — 旧拉取模式(Edge 拉 video_url + webhook 回调),兼容保留,主链路不再使用
POST /api/edge/chat/ask — 智能问答编排(FAM-Core Chat-Handler 调用):请求 {"prompt"} → 响应 {"answer","provider"};内部按 Gemini → NVIDIA → 本地 Ollama 顺序,仅两云端都失败才用本地兜底
POST /api/edge/chat — Ollama 直连代理(兼容旧调用,保留)
GET /health — 服务与模型健康状态(任务处理中可能无响应,单 worker 忙)
5.2 FAM-Core(NAS :8000)
| 端点 | 方法 | 说明 |
|---|---|---|
/health |
GET | 服务健康 |
/api/status |
GET | scheduler/dispatcher 运行状态 |
/api/core/callback/event |
POST | Edge 回调(webhook 兼容保留);推送模式下由 Dispatcher 内部调用 apply_success_event |
/api/chat/ask |
POST | 用户问答:{"question","queried_person","queried_date"} → {"answer","context_summary","chat_id"} |
/api/chat/history |
GET | 对话历史(?date= 或 ?person=&limit=) |
/api/member/unnamed |
GET | 未命名人物列表(含特征描述、出现次数) |
/api/member/name |
POST | 命名:{"abstract_label","real_name","named_by"} → 批量回溯 event_details/entities_json,返回更新条数 |
/api/member/list |
GET | 全部成员 |
/media/<path>?token=xxx |
GET | 视频静态服务(token 鉴权,推送模式下备用) |
5.3 云端结构化输出 JSON Schema
云端 VLM 直接产出结构化 JSON,经两道处理入库:
- 适配器内三层容错解析(
json_parser.parse_vlm_json):直接json.loads→ 提取 markdown fence```json ... ```→ 贪婪匹配最大{...};失败抛VLMOutputInvalidError format_cloud_result归一化/校验(无模型调用):frame_details必须为非空列表并做字段类型归一化;source_providers缺失时补为[provider];compute_provider置为本次成功 provider;entities_json缺失时由frame_details按人物去重推导;global_summary缺失时格式化拼接生成
任一步骤失败 → 任务 FAILED 走重试。action 由 AI 自由生成无枚举过滤,is_attention_event 由 AI 自行判断。
6. 关键技术
6.1 视频预处理(自适应关键帧)
- 粗抽候选帧:FFmpeg 快速 seek(逐帧
ffmpeg -ss <ts> -frames:v 1),替代 fps 滤镜全解码(30min 视频从 180s+ 降到 33s,6-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)
本地 Ollama(qwen2.5:7b,纯文本模型)不参与视觉分析、不参与云端结果融合。它只在智能问答场景下、且 Gemini 与 NVIDIA 两云端模型都失败时才被启用作为兜底。视觉分析与结构化输出全部由云端模型承担。以下参数作为问答任务的调优依据保留。
| 参数 | 值 | 依据 |
|---|---|---|
OLLAMA_KEEP_ALIVE=-1 |
模型常驻内存 | 消除 55s 冷启动(常驻约 4.3GB,12GB 内存够用) |
num_predict=512 |
限制生成 token | ARM 约 5 tok/s,过长生成会拖慢问答响应 |
| 视觉/模型 timeout | 600s | 实测 1024px 帧视觉编码 ~36s + 生成 ~12s/60token(问答链路改用 config 中各模型 timeout) |
| gunicorn(Edge) | --timeout 1800 |
同步分析模式,默认 30s 会杀 worker |
| push_timeout(NAS) | 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 顺序降级,直出结构化 JSONtext:仅参与智能问答(usage: qa_fallback),且为 Gemini/NVIDIA 都失败时的兜底,不参与视觉分析、不参与云端结果融合
NvidiaVisionAdapter 关键实现(fam_edge/adapters/nvidia_adapter.py):
- 基于
openaiPython SDK,base_url=https://integrate.api.nvidia.com/v1,api_key从${NVIDIA_API_KEY}展开 health_check:调client.models.list()轻量验证 Keyanalyze_frames:构建 OpenAI 标准content列表(1 个 text item + N 个image_urlitem,图片以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 直出的结构化 JSON(frame_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)s,max_retries=3 - 僵尸任务回收:PROCESSING 超过
push_timeout + 120s自动重置 PENDING(应对 fam-core 重启 / Edge 重启 / in-flight 请求丢失) failure_stageENUM:download / 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 帧) | 820s(avg 68s/帧,0.9 tok/s) |
| 总计 | 929s(15.5min) |
7.3 E2E 验证(2026-08-20)
task_id=289(30s 测试片段)全链路打通:推送 5.7MB → Edge 分析 1129s → 同步返回 → monitor_events event_id=1 + event_details 2 条落库,event_end_time 推算正确(start+29s)。
8. 部署说明
8.1 部署位置与启动命令
| 组件 | 节点 | 路径 | 启动 |
|---|---|---|---|
| FAM-Core | NAS | /volume1/web/sentinel-home-ai/fam-core/ |
./venv/bin/gunicorn --chdir <路径> -w 1 -b 0.0.0.0:8000 --timeout 120 --daemon --pid /tmp/fam-core-gunicorn.pid src.fam_core.app:app |
| FAM-UI | NAS | /volume1/web/sentinel-home-ai/fam-ui/ |
./venv/bin/streamlit run src/app.py(headless, :8501) |
| FAM-Edge | Oracle | /opt/fam-edge/ |
venv/bin/gunicorn -w 1 -b 0.0.0.0:5000 --timeout 1800 src.fam_edge.app:app(日志 /tmp/fam-edge.log) |
| Ollama | Oracle | systemd 托管 | 环境变量 OLLAMA_KEEP_ALIVE=-1 |
8.2 依赖
- FAM-Core/UI(NAS, Python 3.10 venv):Flask, Gunicorn, PyMySQL(45KB 纯 Python 替代 19MB mysql-connector), PyYAML, requests;FAM-UI 另需 Streamlit + pandas
- FAM-Edge(Oracle, Python 3.8+ venv):Flask, Gunicorn, requests, PyYAML, opencv-python, numpy, openai(NVIDIA NIM 兼容 OpenAI API 规范,复用同一 SDK);Gemini 用 requests 直调 REST(不依赖 google-generativeai SDK)
- 系统级:FFmpeg(两端)、Ollama + qwen2.5:7b(Oracle)、MariaDB 10.11(NAS)
8.3 配置文件要点
fam-core/config/config.yaml(NAS,生产值):
scheduler:
video_dir: "/volume1/surveillance/Generic_ONVIF-001" # 生产目录(YYYYMMDDAM/PM 两级子目录)
# 285 个历史视频由占位 FAILED 任务占用路径,scheduler dedup 自动跳过(forward-only 模式)
dispatcher:
edge_url: "http://129.146.203.203:5000/api/edge/video/push"
push_timeout: 1800
chat_handler:
qa_url: "http://129.146.203.203:5000/api/edge/chat/ask" # 问答统一走 Edge 编排(Gemini→NVIDIA→Ollama)
timeout: 120
fam-edge/config/config.yaml(Oracle,多模型池配置,新架构):
# 编排调度模式: fallback(顺序降级, 默认) | ensemble(并行交叉验证)
orchestrator:
mode: "fallback"
overall_timeout: 600
# 多模型池配置(新框架:本地大模型不参与视频分析,仅智能问答兜底)
# 视频分析链路: 云端 VLM 直出结构化 JSON → Edge format_cloud_result 格式化/校验 → 直存 NAS DB(无本地融合)
# 智能问答链路: Gemini → NVIDIA → 本地 Ollama(仅两云端都失败才启用本地兜底)
models:
# 1. Google Gemini(视觉主 + 参与问答)
- provider: "gemini"
role: "vision" # 视觉分析 + 问答(vision role 也参与 chat)
enabled: true
model_name: "gemini-flash-latest" # v1beta 下 gemini-1.5-flash 会 404
api_key: "${GEMINI_API_KEY}"
timeout: 30
circuit_breaker:
enabled: true
threshold: 3
cooldown: 600
# 2. NVIDIA NIM 托管 API(视觉备 + 参与问答)
- provider: "nvidia"
role: "vision" # 视觉分析 + 问答(vision role 也参与 chat)
enabled: true
model_name: "meta/llama-3.2-11b-vision-instruct" # 或 qwen/qwen2-vl-72b-instruct
base_url: "https://integrate.api.nvidia.com/v1"
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 环境变量文件):
export GEMINI_API_KEY="AQ.Ab8RN6I0l8hC7hLnNHRY6qOXdch5CTWczDNlS4c1XrneGHipUQ"
export NVIDIA_API_KEY="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"
NVIDIA NIM 单图连通性验证:
pip3 install openai
python3 -c "
import os
from openai import OpenAI
client = OpenAI(
base_url='https://integrate.api.nvidia.com/v1',
api_key=os.environ['NVIDIA_API_KEY']
)
response = client.chat.completions.create(
model='meta/llama-3.2-11b-vision-instruct',
messages=[{'role': 'user', 'content': 'Hello, are you ready?'}],
max_tokens=30
)
print('NVIDIA NIM 连接成功:', response.choices[0].message.content)
"
8.4 运维注意事项
- NAS 部署目录不是 git 仓库(文件拷贝部署),同步代码用 stdin 管道:
ssh ... "cat > 远端路径" < 本地文件 - NAS scp 子系统被禁用,同样用 stdin 管道传文件
- 远端 kill gunicorn 时 pkill/pgrep 会匹配 SSH 自身命令行导致断连,用
pgrep -f 'gunicorn -w [1]'字符类技巧或 PID 文件 - fam-core 启动模块路径是
src.fam_core.app:app(不是fam_core.app:app) - Edge 单 worker 处理任务期间
/health可能不响应,属正常
9. 快速开始
# 1. 初始化数据库(NAS)
python scripts/init_db.py
# 2. 启动 FAM-Core(NAS)
cd fam-core && gunicorn -w 1 -b 0.0.0.0:8000 --timeout 120 src.fam_core.app:app
# 3. 启动 FAM-Edge(Oracle)
cd fam-edge && gunicorn -w 1 -b 0.0.0.0:5000 --timeout 1800 src.fam_edge.app:app
# 4. 启动 FAM-UI(NAS)
cd fam-ui && streamlit run src/app.py
# 或使用脚本
./scripts/start_core.sh && ./scripts/start_edge.sh && ./scripts/start_ui.sh
10. 服务器访问信息
10.1 Synology NAS(FAM-Core + FAM-UI + MariaDB)
| 项目 | 值 |
|---|---|
| IP | 192.168.50.64 |
| SSH 端口 | 2222(scp 禁用,用 stdin 管道传文件) |
| SSH 用户 / 密码 | ericwyuan / iLoveJava5 |
| 系统 | Synology DS220+ (Geminilake), DSM 7 |
| Tailscale IP | 100.70.234.39(userspace 模式,端口不通待修) |
| 登录命令 | ssh -p 2222 ericwyuan@192.168.50.64 |
10.2 MariaDB(NAS)
| 项目 | 值 |
|---|---|
| 版本 | 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 Cloud(FAM-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 API(Google 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 测试可用 |
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 SDK(NIM 兼容 OpenAI API 规范,直接复用) |
| 验证状态 | 已验证(2026-08-20 部署验证:/api/edge/chat/ask 实测 provider=nvidia,Gemini 超时后 NVIDIA 兜底成功;单图连通性验证脚本见 8.3) |
环境变量:
export NVIDIA_API_KEY="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"
11. 提交规范(AI Agent 必读)
工作流程(强制):
- 开工前:仓库根目录
git pull --rebase - 每完成一项验收子任务:立即
git add+git commit+git push,一任务一 commit,不批量合并 - 遇到阻塞:先 commit 可工作部分,message 加
[WIP]前缀 - 修 Bug:单独 commit,格式
fix(模块): 问题简述
commit message 格式:
- 任务:
[阶段X.Y子任务号] 子任务名称 - 完成内容简述 - Bug:
fix(模块): 问题简述 - 性能/文档:
perf(...)/docs(...)
禁止:不 commit 直接继续;一次 commit 多个子任务;git push --force;跳过 hooks(--no-verify)。
自检清单:任务对应哪一行验收标准?是否已 commit + push?message 是否合规?开工前是否 pull --rebase?
12. 当前进度与 v1.1 计划
已完成(截至 2026-08-20)
- 全部模块代码 + DDL + 部署脚本;NAS/Oracle 双端部署运行
- 模型基准测试(原 llava-phi3 预热 4.3s PASS,已替换为 qwen2.5:7b);Ollama 常驻内存
- 架构改为推送模式(NAS 上传整段视频 → Edge 同步分析 → 结果随响应返回)
- 关键帧自适应帧数 + FFmpeg 快速 seek(6-8x 提速)
- FAM-Core API 全端点测试通过
- FAM-UI 部署(Streamlit 1.61.1)
- 真实视频性能基准(30min/360MB → 929s)
- E2E 全链路打通:task 289 → SUCCESS,monitor_events/event_details 落库正确
- 可靠性加固:僵尸任务回收、文件日志、datetime 空值兜底、超时按实测调整
- 架构重构(移除本地融合,云端直出直存):
- 视频分析链路:云端 VLM(Gemini
gemini-flash-latest/ NVIDIA NIMllama-3.2-11b-vision-instruct)直出结构化 JSON → Edgeformat_cloud_result格式化/校验(无模型调用)→ 直存 NAS DB;本地 Ollama 不再参与视频摘要与融合(run_text_fusion已移除) - 智能问答链路:新增
chat()方法,run_qa按 Gemini → NVIDIA → 本地 Ollama 降级编排;端点/api/edge/chat/ask;NAS Chat-Handler 经 Edge 编排(qa_url),不再直连 Ollama - 生产目录已切回
/volume1/surveillance/Generic_ONVIF-001,285 历史视频 forward-only 占位跳过
- 视频分析链路:云端 VLM(Gemini
- 双端部署验证通过(2026-08-20):Oracle Edge 7 文件部署 + 重启,
/api/edge/chat/ask实测 provider=nvidia(Gemini 超时→NVIDIA 兜底成功);NAS chat_handler + config 手术式更新 + HUP 重载,插入临时上下文实测 NAS→Edge 编排链路 43s 返回并落库(测试数据已清理);Edge 日志确认 task 294(360MB)以新架构处理中、三模型健康检查全通过
详细进度见 PROGRESS.md。
v1.1 待办
| # | 任务 | 优先级 |
|---|---|---|
| 1 | 单元测试(JSON parser / circuit breaker / schema 校验) | 中 |
| 2 | Tailscale 修复(NAS userspace 模式升级,流量不走公网) | 低 |
| 3 | daily_summaries 每日摘要 | 低 |
文档结束