# 项目进度追踪 > 最后更新: 2026-08-31 ## 服务运行状态 | 服务 | 节点 | 地址 | 状态 | 验证结果 | |------|------|------|------|---------| | FAM-Core | NAS | 0.0.0.0:8000 | ⚠️ 代码已完成,待生产部署 | health=ok, gunicorn 单 worker;Oracle-Sync + MotionNotifier 轮询 SS 事件推送(游标 DB 续用/失败重试);已部署事件时间轴删除功能;`/api/status`/`/api/ss/status` 500 bug 已修复;**登录改接 auth-hub 统一登录(OIDC),本地全链路验证通过,生产 client 注册 + NAS `.env` 配置 + 部署重启尚未执行** | | FAM-Edge | Oracle(新机 129.146.26.249) | 0.0.0.0:5000 | ✅ 运行中 | systemd 守护(fam-edge.service);素材→运动片段分割→只分析片段;问答改为转发 AI-Gateway;队列消费正常;已部署 `/api/oracle/video/delete`;FFmpeg 已补装;新增 DiskGuard 磁盘守护 | | **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 | | Ollama | Oracle | 127.0.0.1:11434 | ✅ 运行中 | qwen2.5:7b,仅智能问答兜底(不参与视觉/融合);**被 AI-Gateway 调用,不再被 FAM-Edge 直接调用** | ## 2026-08-31 FAM-Core 接入 auth-hub 统一登录(OIDC) - **背景**:独立的统一登录/SSO 服务 [auth-hub](http://129.146.26.249:3000/ericwyuan/auth-hub)(跟本项目完全独立的仓库/数据库/部署,OAuth2 Authorization Code + PKCE + OIDC)已单独开发验证完成(本地全链路 47 单测通过),目标是自己的几个网站统一接到这一处身份服务,不用每个网站各自维护一份账号密码。FAM-Core 是第一个接入方。 - **改造范围**(`fam-core/src/fam_core/auth.py` 全量重写,commit 待提交): - 原来自己校验 `FAM_AUTH_USER`/`FAM_AUTH_PASS` 的账号密码逻辑、内置深色登录表单页全部移除;`GET /login` 改为直接 302 跳转 auth-hub `/authorize`(带 PKCE `code_challenge`/`state`,均存进程内 `_pending` 表,10 分钟过期) - 新增 `GET /api/auth/callback`:收 auth-hub 回跳的 `code`,服务端到服务端 `POST /token` 换 `id_token`(用 `PyJWT` + `PyJWKClient` 拉 auth-hub JWKS 验 RS256 签名 + `iss`/`aud`),验证通过后种回原有的 `fam_session` HttpOnly cookie(**2 小时有效,机制不变**)——下游 `is_authed()`/`init_auth()` 全局拦截逻辑完全没动 - 接入参数 `AUTH_HUB_ISSUER`/`AUTH_HUB_CLIENT_ID`/`AUTH_HUB_CLIENT_SECRET`/`AUTH_HUB_REDIRECT_URI` 走环境变量(NAS `.env`,沿用 `FAM_AUTH_*` 时代"未配置齐全直接拒绝所有登录"的 fail-closed 取舍),`AUTH_HUB_REDIRECT_URI` 必须与 auth-hub 端 `manage_clients create` 登记的 redirect_uri 逐字符一致 - `requirements.txt` 新增 `PyJWT>=2.8.0`、`cryptography>=42.0.0` - **访问控制取舍(用户明确决策)**:不在 fam-core 侧加用户名白名单——任何在 auth-hub 上(自助注册 + 管理员审批后)拥有账号的人登录后都能访问本系统;不保留 `FAM_AUTH_USER`/`FAM_AUTH_PASS` 作为备用登录方式,SSO 是唯一入口 - 测试:`tests/test_auth.py` 全量重写(PKCE 生成、state 校验、token 交换成功/失败、id_token 验签失败、白名单、`before_request` 拦截),fam-core 全量 39/39 通过 - **端到端验证(本地)**:本地起了一份 auth-hub 开发实例(:5300)+ 一个仅含 `auth_bp` 的最小 Flask 壳子(模拟 fam-core,跳过 MariaDB 依赖),用真实浏览器走完整 Authorization Code + PKCE 闭环——`/` 未登录 302 `/login` → auth-hub 登录页 → 登录成功回跳 `/api/auth/callback` → 换 token + 验签 → 种 cookie → 落地首页;`/api/auth/check` 确认 cookie 生效;`/api/logout` 确认清会话。验证完把临时创建的 OAuth client 和测试账号都从 auth-hub 本地库删掉了。 - **生产部署进度**: 1. ✅ 已在 Oracle 生产 auth-hub(`/opt/auth-hub`,:5300)注册正式 client `FAM-Core`(client_id/secret 已生成,明文只显示过一次,未写入仓库,需要的话找 auth-hub 管理后台或 `manage_clients list` 核对 client_id) 2. ⏳ **NAS `.env` 待补**(本次会话没有 NAS SSH 免密权限,未执行):`AUTH_HUB_ISSUER=http://129.146.26.249:5300`、`AUTH_HUB_CLIENT_ID`、`AUTH_HUB_CLIENT_SECRET`、`AUTH_HUB_REDIRECT_URI=http://129.146.26.249/api/auth/callback` 3. ⏳ 部署新代码到 NAS(tar 管道,见 `docs/DEPLOY.md` §3)并 `bash start_core.sh` 重启——待执行 4. ⏳ 生产环境浏览器实测一遍完整登录闭环——待执行 ## 2026-08-28 Oracle 迁移故障排查:FFmpeg 缺失 + 磁盘写满死循环 + DiskGuard - **触发**:用户反馈事件时间轴历史图片丢失、"谷歌同步是不是有问题"。逐层排查,发现的是三个叠在一起的独立问题,不是一个: 1. **8/25 迁移新机器时漏装 FFmpeg**:`ffprobe: command not found`。导致 `videos.duration_sec` 全部读成 0、运动片段分割 `_segment_motion_clips` 全部失败(`ffmpeg` 也没装),190 个素材全部"分割 0 段",`events` 表完全是空的,`motion_clips/` 目录空的——事件时间轴从 8/25 起就没有任何新内容,图片自然也生成不出来。**已修复**:`apt-get install ffmpeg`,恢复正常。 2. **8/25 之前的历史图片无法找回**:迁移时只搬了 `data/oracle.db`(数据库/文字记录),没有搬 `motion_clips/`(磁盘上的运动片段视频文件,缩略图的生成源);旧机器(129.146.203.203)已经释放销毁。历史事件的文字描述还在(早就同步进了 NAS 镜像),但配图永久丢失,无法找回。 3. **Oracle↔Google Drive 同步"只下载不删除"**:排查一度走了弯路(先怀疑 NAS CloudSync 停摆,后发现新文件其实一直在下载,是判断证据不足导致的误判)。最终精确对比 Google Drive 远端(141 文件,最早 8/25)vs Oracle 本地(264 文件,最早 8/15)实锤:148 个文件是"远端已删、本地未删"的孤儿文件。根因是磁盘被写满到 100%(`no space left on device`),触发 rclone 内置安全机制——同步过程中遇到 IO 错误就整体拒绝执行删除,形成"越满删不掉,删不掉越满"的死循环。**已修复**:手动删除 148 个孤儿文件,释放 41G,磁盘 100%→59%;重新跑 `rclone_sync.sh` 验证 `exit=0` 无错误,下载+删除恢复正常。 - **新增 DiskGuard 磁盘守护**(commit `ef56ae5`,永久性预防措施):`fam-edge/oracle_db.py` 新增 `get_oldest_purgeable_material()`(只挑最旧的、已完成分割阶段的整段素材,绝不碰运动片段和处理中的素材);`disk_guard.py` 新增后台线程,5 分钟检查一次,剩余空间 <10GB 触发清理,删到 15GB 水位为止;`/api/oracle/activity` 新增 `disk` 字段(实时剩余空间 + 最近清理动作)。9 个新测试,fam-edge 全量 139/139 通过。 - **用户决策**:8/25-28 期间因 FFmpeg 缺失而"分割 0 段"的 190 个素材/736 个运动事件,明确选择不重新处理,翻页接受这段时间的空白。 - **顺带修复**(commit `09708bd`):`/api/status`、`/api/ss/status` 500——上次"移除 SS Webhook"重构(`cf3d145`)删了 `MotionNotifier` 的 `camera_name_to_id` 等属性但 `status()` 忘记同步更新,导致这两个端点自那次重构起就一直是坏的。 - **文档同步**(commit `3ec79de`):README 里 5 处 + `vite.config.js` 1 处仍描述"`/api/ss/webhook` 保留为可选"的过时内容,实际已被彻底删除,逐处订正。 ## 2026-08-24 事件时间轴支持删除视频会话 - **决策**:用户要求事件时间轴能删除某个视频会话;调研确认项目里没有任何 delete 类端点先例,且增量同步(`get_sync_delta`)只做 upsert 感知不到 Oracle 端的物理删除,NAS 镜像必须显式清理,不能靠 `trigger_now()` 拉增量。用户明确要求连同磁盘上的视频文件一起删除,明确选择不做"防止误删事件被重新分割"的额外保护机制(风险极低:素材一旦标记 done 就不会被生产者重新捡起) - **三端实现**(commit `28397e9`): - fam-edge:`oracle_db.py` 新增 `delete_video(video_id)`(写锁保护,先删 events 再删 videos,再删磁盘文件;不清理 `ss_motion_events` 源事件);`api_gateway.py` 新增 `POST /api/oracle/video/delete` - fam-core:`oracle_sync.py` 新增 `push_video_delete()`;`db_layer.py` 新增 `delete_sync_video()`(项目里第一个"NAS 直接写自己镜像表"的函数);`ui_api.py` 新增 `DELETE /api/ui/videos/`(先回推 Oracle 成功后才清本地镜像,回推失败 502 不改本地状态,避免数据不一致) - fam-ui:`Timeline.vue` 详情卡片新增"🗑 删除会话"按钮(原生 `confirm()` 二次确认,删除成功后从本地列表移除 + 重新选中 + 刷新统计卡);`api.js` 新增 `deleteVideo()` - **测试**:`test_oracle_db.py` 新增 6 个 `delete_video` 单元测试(正常删除/磁盘文件删除/文件已缺失不报错/不存在返回 None/不影响 ss_motion_events);fam-edge 全量 130/130 通过 - **端到端验证**:全程用自造测试数据(`motion_TESTDELETE*`),未触碰任何真实监控记录——Oracle 侧直接调 `/api/oracle/video/delete` 验证记录+文件删除+幂等 404;NAS 侧手动插入镜像行模拟已同步状态,登录后调 `DELETE /api/ui/videos/`,验证 Oracle 记录+文件、NAS 镜像记录均被清理 - **已知限制**:前端浏览器 UI 层面未做可视化验证(fam-core 全站要求登录,出于"不代用户输入密码"的原则没有走浏览器交互式登录),只验证了 `npm run build` 编译通过 + 完整 API 链路 ## 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) - **决策**:不再分析整段视频。rclone 整段素材保留在 Oracle,按 NAS 推送的 `ss_motion_events`(start_time/duration)**ffmpeg 分割成运动片段**,只分析片段。 - **改动**(commit `a1523b4` + `8d6cfad`,已部署 Oracle systemd 重启): - `oracle_db.py`:videos 表兼容加 `motion_event_id`/`camera_id` 列;新增 `get_motion_events_in_range`(窗口内已结束事件,grace 容差)/`has_unfinished_motion_in_range`/`get_video_by_motion_event_id` - `video_processor.py`:素材→分割(只分割已结束事件,`-c:v copy -c:a aac` 保留音频,motion_event_id 幂等)/片段→只分析 双分支;修复 ffmpeg args 缺可执行文件 bug - `video_queue.py`:素材分割出的片段入队 - `config.yaml`:新增 `motion_segment` 块(clips_dir/keep_audio/min_duration/unfinished_grace_sec) - **E2E 验证(真机)**:素材 `Generic_ONVIF-001-20260822-110859-...mp4` → 分割 35 段(真实 event_id 25504-25538、duration 1-132s)→ 片段只分析(summary 真实:人物/动作)→ events(绝对时间 ts)→ NAS 同步 → `/api/ui/videos` 显示 motion_ 片段(字段契约不变:filename/event_start_time/camera_name/event_count/summary_json/compute_provider)→ `/api/ui/videos/` 详情 events + people 正常 → `/api/proxy/frame` 帧图 200 OK - **关键认知**:SS 事件 duration=0 仅在动作**进行中**(结束时必为正数,实测 id=25538 从 0→2s),故只分割已结束事件(start+duration ≤ now+grace),进行中的等下一轮。 - **部署注意**:Oracle fam-edge 由 **systemd `fam-edge.service` 守护**(Restart=always),代码部署后必须 `sudo systemctl restart fam-edge`(手动 setsid 会与守护打架导致端口冲突)。 ## 2026-08-22 清数据重提取 + 人物管理重设计 + 文档同步(commits 4cf4fc4/6a29e88/9664459) - **清数据重提取**(用户决策:整段提取旧数据全删,按运动视频重新提取):Oracle videos(364)/events(1687)/people(44)/model_calls/service_activity 全清 + motion_clips 清空(保留 ss_motion_events 93 条 + 素材 + 配置,先备份);NAS sync_* 四表全清;重启两端自动重提取(8/22 素材→运动片段;8/15-21 历史素材无运动事件→0 段) - **时间轴过滤**:历史素材"分割 0 段"空会话淹没时间轴 → db_layer.get_sync_videos/get_sync_stats 加内容过滤(`LEFT(filename,7)='motion_'` OR 有事件),前端契约不变;**踩坑:pymysql execute 用 % 做占位符,SQL 字面量 'motion_%' 的 % 报 "unsupported format character" 500 → 改 LEFT 判断** - **人物管理重设计**:`GET /api/ui/people/clips?label=`(db_layer.get_sync_people_clips:按 label/canonical_name 匹配 person_list_json → 关联运动片段,含 first_ts/clip_events);PersonCard.vue 新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳 `/timeline?video=`);Timeline.vue 支持 query 定位 - **文档同步**:README 更新 2.1 网络要点/2.2 拓扑(Streamlit→Vue3)/3.1-3.3 模块表(poll 主路径+people clips)/4.1 表清单(motion_event_id/ss_motion_events)/5.1-5.2 API 表(people/clips+frame/avatar)/6.1 历史标注/8 部署(systemd+Vue3+motion_segment config)/12 进度;docs/DEPLOY.md 重写为 v3;已 push - **验证**:/api/ui/videos 全为 motion_ 片段(15 条);/api/ui/people 3 身份;people/clips 返回媳妇 3 个片段;前端 dist 已构建部署 | Tailscale | Oracle ↔ NAS | 100.74 ↔ 100.70 | ⚠️ 待修复 | Tailscale 运行但端口不通,当前用公网IP | | FAM-UI | NAS | 0.0.0.0:8501 | ✅ 运行中 | Streamlit 1.61.1, HTTP 200, health=ok | ## 环境状态 ### Oracle Cloud (129.146.26.249) - FAM-Edge 节点 | 项目 | 状态 | 备注 | |------|------|------| | OS | Ubuntu 20.04 ARM64 | Ampere A1 2C12G | | Python | 3.8.10 | python3-venv 已安装 | | pip | 25.0.1 (venv内) | | | FFmpeg | 已安装 | apt 安装 | | Ollama | 0.32.14 | | | qwen2.5:7b | 已拉取 | 纯文本模型,仅问答兜底(原 llava-phi3 已废弃) | | Python 依赖 | 全部安装成功 | flask, gunicorn, requests, PyYAML, opencv, numpy | | 模块导入验证 | PASS | FAM-Edge 全部 8 个模块导入正常 | | 代码部署 | /opt/fam-edge/ | rsync 同步 | | venv | /opt/fam-edge/venv/ | 已创建 | | Tailscale | 1.102.2 已安装 | **待用户认证** | ### Synology NAS (192.168.50.64) - FAM-Core + FAM-UI 节点 | 项目 | 状态 | 备注 | |------|------|------| | 型号 | DS220+ (Geminilake) | DSM 7 | | Python | 3.8.15 (系统) + 3.10.20 (venv) | FAM-Core/UI venv 使用 Python 3.10 | | pip3 | venv 内可用 | fam-core/venv + fam-ui/venv | | MariaDB | **10.11.11 已运行** | 端口 3306, root 密码 iLoveJava5! | | sentinel_home_ai 库 | **6 张表已创建** | DDL 执行成功 | | Docker | 24.0.2 (ContainerManager) | 路径 /var/packages/ContainerManager/target/usr/bin/docker | | Tailscale | **已运行** | IP: 100.70.234.39, hostname: ericwyuan-nas | | FAM-Core venv | ✅ 已创建 | Python 3.10.20, flask/PyMySQL/gunicorn/PyYAML/requests | | FAM-UI venv | ✅ 已创建 | Python 3.10.20, Streamlit 1.61.1 + pandas 2.3.3 | | Streamlit | ✅ 已安装 | 1.61.1, 运行于 0.0.0.0:8501 (headless) | ### 网络互通 (Tailscale) | 节点 | Tailscale IP | 状态 | |------|-------------|------| | NAS (ericwyuan-nas) | 100.70.234.39 | 已连接 | | Oracle (oracle-fam-edge) | 待认证 | **需用户访问认证 URL** | | 当前方案 | Oracle Tailscale 待认证,认证后可直连 | | ## 模型可行性基准测试 (Task 1.4) ### 测试环境 - Oracle Ampere A1 2C12G ARM64 - Ollama 0.32.14 + llava-phi3:latest (2.9 GB) - Python 3.8.10 + requests ### 测试结果 | 场景 | 加载时间 | Prompt 处理 | 生成时间 | Token 数 | 总耗时 | 是否达标(≤8s) | |------|---------|------------|---------|---------|--------|-------------| | 冷启动 (首次推理) | 54.56s | 37.40s | 17.22s | 86 | 109.21s | 否 (首次加载) | | 预热 (无限制) | 0.06s | 0.20s | 13.06s | 65 | 13.35s | 否 | | 预热 (num_predict=30) | 0.07s | 1.41s | 2.83s | 15 | **4.34s** | **PASS** | ### 结论 1. **冷启动延迟**: 首次推理需加载 2.9GB 模型到内存,耗时 ~55s。建议通过 systemd keep-alive 或定时心跳保持模型常驻 2. **预热性能**: 模型加载后,单图推理可控制在 4-5s(限制输出 30 token),满足 ≤8s 设计目标 3. **ARM CPU 瓶颈**: token 生成速度 ~0.1-0.2s/token,多帧分析(5-8帧)需合理设置 `num_predict`(默认 500) 4. **已优化**: OllamaAdapter 新增 `num_predict` 可配置参数,默认 500 ## 任务进度 ### 已完成 | # | 任务 | 提交 | 日期 | |---|------|------|------| | 1 | 项目骨架 + 全部 Python 模块代码 | `d741ade` 等 | 2026-08-19 | | 2 | DDL 数据库建表脚本 | `d741ade` | 2026-08-19 | | 3 | 部署脚本 + 文档 | `d741ade` | 2026-08-19 | | 4 | __init__.py 导出链路 | `c45464c` | 2026-08-19 | | 5 | Storage-Cleaner 补全 | `5e4587b` | 2026-08-19 | | 6 | 服务器凭证写入 README | `ba905d2` | 2026-08-19 | | 7 | requirements.txt 兼容 Python 3.8 | `c1bfac5` | 2026-08-19 | | 8 | Oracle 部署: venv + 依赖 + 导入验证 | `50d8353` | 2026-08-19 | | 9 | 模型基准测试 (llava-phi3 ≤8s PASS) | `50d8353` | 2026-08-19 | | 10 | OllamaAdapter 优化 (num_predict) | `50d8353` | 2026-08-19 | | 11 | Tailscale 安装 (Oracle + NAS) + 认证 | `e5ac667` | 2026-08-19 | | 12 | NAS MariaDB DDL 执行 (6 张表) | `e5ac667` | 2026-08-19 | | 13 | db_layer.py 切换 PyMySQL (45KB 替代 19MB) | ✅ 完成 | 2026-08-19 | | 14 | config_loader.py 路径修复 (3 级 dirname) | ✅ 完成 | 2026-08-19 | | 15 | NAS Python 依赖安装 (flask/gunicorn/pymysql/PyYAML) | ✅ 完成 | 2026-08-19 | | 16 | NAS 代码部署 + FAM-Core 导入验证 PASS | ✅ 完成 | 2026-08-19 | | 17 | NAS DB 连接验证 PASS (PyMySQL → MariaDB 10.11.11) | ✅ 完成 | 2026-08-19 | | 18 | config.yaml 实际配置 (FAM-Core + FAM-Edge + FAM-UI) | ✅ 完成 | 2026-08-19 | | 19 | NAS 启动 FAM-Core (gunicorn, Python 3.10 venv) | ✅ 完成 | 2026-08-19 | | 20 | Oracle 启动 FAM-Edge (gunicorn) | ✅ 完成 | 2026-08-19 | | 21 | MariaDB JSON 路径兼容修复 (name_member) | ✅ 完成 | 2026-08-20 | | 22 | FAM-Core API 全端点测试通过 | ✅ 完成 | 2026-08-20 | | 23 | FAM-Edge 聊天代理端点 (/api/edge/chat) | ✅ 完成 | 2026-08-20 | | 24 | FAM-UI PyMySQL 迁移 | ✅ 完成 | 2026-08-20 | | 25 | FAM-Core 配置切换至 Oracle 公网 IP | ✅ 完成 | 2026-08-20 | | 26 | 端到端聊天验证 (Core→Edge→Ollama) | ✅ 完成 | 2026-08-20 | | 27 | NAS 安装 Streamlit + pandas + 部署 FAM-UI | ✅ 完成 | 2026-08-20 | | 28 | Ollama 模型常驻内存 (OLLAMA_KEEP_ALIVE=-1) | ✅ 完成 | 2026-08-20 | | 29 | 关键帧提取改为自适应帧数 (随视频时长动态计算) | ✅ 完成 | 2026-08-20 | | 30 | FFmpeg 快速 seek 替代 fps 滤镜 (6-8x 提速) | `7ce8aff` | 2026-08-20 | | 31 | 真实视频性能基准测试 (30min 360MB 视频) | ✅ 完成 | 2026-08-20 | | 32 | 视频端到端集成测试 (task 289 → SUCCESS, 落库正确) | ✅ 完成 | 2026-08-20 | | 33 | 生产目录切换 forward-only (285 历史视频占位跳过) | ✅ 完成 | 2026-08-20 | | 34 | 今日 17 片段诊断(根因:Ollama 本地融合 300s 超时) | ✅ 完成 | 2026-08-20 | | 35 | **架构重构**:移除本地融合(run_text_fusion),云端 VLM 直出 JSON → format_cloud_result → 直存 DB | `babf5b0` | 2026-08-20 | | 36 | 适配器 chat() 方法 + run_qa 三模型降级(Gemini→NVIDIA→Ollama 兜底) | `babf5b0` | 2026-08-20 | | 37 | 端点 /api/edge/chat/ask + chat_handler 改走 Edge 编排(qa_url) | `babf5b0` | 2026-08-20 | | 38 | 双端部署验证:Oracle 7 文件 + 重启;NAS chat_handler + config 手术 + HUP;实测 /api/edge/chat/ask provider=nvidia、NAS→Edge 编排 43s 落库 | ✅ 完成 | 2026-08-20 | | 39 | **fix: NVIDIA 单帧 JSON 解析** — parse_vlm_json 要求全 schema 但 NVIDIA 只返回单帧 JSON,改用轻量 _parse_single_frame_json | `853cb21` | 2026-08-20 | | 40 | **fix: Gemini timeout 30s→90s + 熔断器 threshold 3→5 cooldown 600→300** | `853cb21` | 2026-08-20 | | 41 | **fix: dispatcher crash on invalid failure_stage** — Edge 返回 'process' 不在 ENUM 中导致 DataError 崩溃,db_layer 加容错映射 + dispatcher per-task try/except | `d75c745` | 2026-08-20 | | 42 | **视频管线批量处理验证** — task 291/297 SUCCESS,NVIDIA 6 帧分析 ~15s/任务,events + event_details 落库正确 | ✅ 完成 | 2026-08-20 | | 43 | **异步任务队列架构** — Edge 端 SQLite 队列 + TokenBucket 速率限制 (Gemini 1000 RPM, NVIDIA 40 RPM, burst 2x) + 消费者线程 | `02da23e` | 2026-08-20 | | 44 | **NAS Dispatcher 重构为 enqueue 模式** — 上传视频后立即返回 202,不等 AI 处理结果 | `02da23e` | 2026-08-20 | | 45 | **NAS Poller 线程** — 定期从 Edge /api/edge/results 拉取处理结果写 MariaDB | `02da23e` | 2026-08-20 | | 46 | **fix: dispatcher/poller 超时分离模式** — timeout 改为 (connect, read) 元组,stale_timeout 3600→600s | `4f0f19b` | 2026-08-20 | | 47 | **异步队列端到端验证** — task 312 (5.7MB e2e clip): 上传38s + Edge处理54s + Poller落库 = 93s 总耗时,event_id=12 落库 PASS | ✅ 完成 | 2026-08-20 | | 48 | **分块断点续传上传** — 20MB/块 + 分块级重试(3次) + 断点查询 + /assemble 合并入队 | `b6c13a9` | 2026-08-20 | | 49 | **串行上传 + 看门狗** — Dispatcher limit=1 避免带宽争抢 + 线程存活检测(is_alive) + 60s 看门狗自动重启 + 退避缩短至 30×(n+1)s | `881ea3f` | 2026-08-20 | | 50 | **fix: 分块大小 5MB + chunk_size 变更防护** — Edge 端 total_chunks 变更自动清理旧分块;NAS 端断点续传检测 total_chunks 不匹配时从头上传;timeout (60,180) | `5915cf4` | 2026-08-20 | | 51 | **运动监测重构为 Webhook 驱动(去轮询)** — 关闭 MotionNotifier 轮询;SS 事件经「行動規則/Webhook」`POST :8000/api/ss/webhook` 推送,FAM-Core 映射(%DEVICE_NAME%→camera_id、%EVENT_TIME%→epoch、合成稳定 event_id)后 `POST /api/ss/motion` 推甲骨文;甲骨文 `ss_motion_events` 落库、video_processor 改本地运动预过滤 | `ca6d425` | 2026-08-22 | | 52 | **fix: MotionNotifier 补回 camera_ids 属性** — 避免重新开启轮询时 `_fetch_events` 引用缺失属性崩溃 | `e52d8e9` | 2026-08-22 | | 53 | **文档同步** — README 架构章节(2.2.1 运动监测链路)+ API 表 + 模块表补充 v3 Webhook 架构;PROGRESS 状态/变更记录更新 | - | 2026-08-22 | ### 待完成 | # | 任务 | 依赖 | 优先级 | |---|------|------|--------| | 48 | 单元测试 (JSON parser, circuit breaker, schema) | - | 中 | | 49 | Tailscale 防火墙修复 (NAS↔Oracle) | - | 低 | | 50 | daily_summaries 每日摘要 | - | 低 | | 51 | **视频预压缩** — 360MB 生产视频 HTTP 上传超时/断连,需在 NAS 端预压缩后再上传 Edge(5MB 分块已缓解但 72 块仍需 ~18min) | - | 中 | ## 技术决策记录 1. **移除 google-generativeai SDK 硬依赖** - 代码用 requests 直接调 REST API,兼容 Python 3.8 2. **rsync 替代 git clone** - Oracle 无法直连 NAS Gitea (无 Tailscale),用 rsync 从本地同步 3. **num_predict 参数** - ARM CPU 上 qwen2.5:7b 生成速度较慢,限制输出 token 数控制延迟 4. **PyMySQL 替代 mysql-connector-python** - 45KB 纯 Python 替代 19MB C 扩展,NAS 无需编译 5. **MariaDB JSON 路径兼容** - MariaDB 10.11 不支持 MySQL 的 `$[*]` 通配符 JSON 路径和 `->` 操作符,name_member() 改用 Python 层解析 + 逐行 UPDATE 6. **Python 3.10 venv** - NAS 系统 Python 3.8 过旧,用 Synology Python3.10 包创建 venv 7. **FAM-Edge 聊天代理** - 历史:/api/edge/chat 直连 Ollama 代理;架构重构后被 `/api/edge/chat/ask` 三模型编排端点取代(旧端点保留兼容) 8. **Oracle 公网 IP 替代 Tailscale** - Tailscale 两节点在线但端口不通(防火墙),edge_url 和 qa_url 改用 Oracle 公网 IP 129.146.26.249 9. **Ollama 模型常驻内存** - systemd 加 `OLLAMA_KEEP_ALIVE=-1`,模型加载后永不卸载,消除 55s 冷启动延迟,常驻占用 4.3GB 内存(系统 12GB 够用) 10. **关键帧自适应帧数** - 原固定 5-8 帧对长视频太稀疏(30分钟仅8帧=每3.75分钟1帧),改为随视频时长自适应:候选帧 `clamp(duration_min×2, 30, 120)`,关键帧上限 `clamp(duration/150s, 8, 30)`。30分钟→12帧,60分钟→24帧,封顶30帧 11. **云端直出直存(架构重构)** - 本地 Ollama 完全移出视频链路:云端 VLM(Gemini 多图单请求 / NVIDIA 逐帧聚合)直接产出结构化 JSON,Edge 仅 `format_cloud_result` 格式化/校验(无模型调用)后直存 NAS DB。根因:CPU 版 Ollama 对无上限融合 prompt 预填充极慢导致 300s 超时 12. **Q&A 三模型降级编排** - 智能问答由 Edge `run_qa` 编排:Gemini → NVIDIA → 本地 Ollama(仅两云端都失败才启用本地兜底);各适配器实现 `chat()` 纯文本接口 13. **FFmpeg 快速 seek 替代 fps 滤镜** - 原方案 `ffmpeg -vf fps=1/interval` 需全解码视频,30min 360MB 视频在 ARM CPU 上需 180s+(超 120s 超时)。改为逐帧 `ffmpeg -ss -frames:v 1` 快速 seek,仅 33s(6-8x 提速) 14. **异步任务队列架构 (SQLite + TokenBucket)** - 原 Dispatcher 同步推送 360MB 视频至 Edge 等待 AI 处理,单任务阻塞 15+ 分钟导致后续任务积压。重构为:NAS Dispatcher 仅上传视频入 Edge SQLite 队列(202 立即返回)→ Edge 消费者线程异步处理(TokenBucket 按 API 限制 2x 速率:Gemini 1000 RPM / NVIDIA 40 RPM)→ NAS Poller 定期拉取结果写 MariaDB。e2e 验证:5.7MB 视频全链路 93s (上传 38s + 处理 54s + 落库 1s) 15. **超时分离模式** - `requests.post(timeout=300)` 对大文件上传无总超时限制(per-write 不触发),改为 `timeout=(10, 60)` 分离连接/读取超时,配合 stale_timeout 600s 回收僵尸任务 16. **生产视频预压缩(待实施)** - 360MB 30min 1080p H.264 视频跨公网 HTTP 上传超时/断连,需在 NAS 端 FFmpeg 预压缩(降分辨率/码率)后再上传 17. **串行上传策略** - Dispatcher 轮询 limit=1,一次只处理一个任务,避免多任务并行上传争抢带宽。NAS 上行带宽仅 ~1Mbps,并行上传会导致所有任务都超时 18. **5MB 分块大小** - 实测 NAS→Oracle 上行带宽 ~1Mbps:1MB 需 6s,5MB 需 35s,10MB 需 85s。20MB 分块在 ~1Mbps 下需 170s 超出 120s 读取超时。5MB 分块 + timeout(60,180) 在 ~35s/块 下有充裕余量,72 块 360MB 约 18min 完成上传 19. **chunk_size 变更防护** - Edge 端 upload_chunk 检测 total_chunks 变更(由 chunk_size 变化导致),自动清理旧分块防止混合不同大小分块导致 assemble 后文件损坏。NAS 端 _query_uploaded_chunks 比对 expected_total != edge_total 时跳过断点续传 20. **看门狗机制** - app.py 启动 daemon 看门狗线程,每 60s 检查 scheduler/dispatcher/poller 线程 is_alive(),崩溃自动 check_and_restart()。/api/status 改用 thread.is_alive() 替代 _running 标志,防止线程假死误报 ## 时区修复收尾 + 队列表 Schema 迁移 (2026-08-20 18:50, commit 9aba71c/1b82db1) **时区修复的残留漏洞**(795e8ac 的补充):该提交只改了 `CREATE TABLE` 的 DEFAULT,但 **DEFAULT 约束固化在已存在表的 schema 中,`IF NOT EXISTS` 不会更新旧表**——enqueue INSERT 未显式传时间戳,新行 created_at 仍走旧 DEFAULT `localtime`(UTC 机器上=UTC)。实证:行 19 (task 42) 18:14 入队 created_at=10:14:22 (UTC),同刻 UPDATE 语句已是北京时间 18:18。 **三处修复**(commit 9aba71c): 1. enqueue INSERT 显式写 `created_at/updated_at = datetime('now','+8 hours')`,不再依赖 schema DEFAULT 2. `_init_db` 检测旧 schema 含 `localtime` 时事务内重建表迁移(新表+复制数据+drop+rename,幂等可重复执行) 3. 存量数据修正:行 16/19 created_at +8h **验证**:本地迁移单测 3 例通过(迁移+数据保留/INSERT 北京时间/幂等);线上部署后行 20 (task 44) created_at=18:43:35 为北京时间(UTC 机器当时 10:43);task 44 全链路 SUCCESS(event_id=25,帧时间戳 22:31:15→22:56:15 与视频起点一致);MariaDB monitor_events/event_details 时间戳全链路北京时间正确。 **附带修复**(commit 1b82db1):NAS 压缩命令补 `-f mp4` 显式指定 muxer(.tmp 扩展名导致 ffmpeg41 无法推断输出格式,task 43 首轮压缩失败回退 360MB 原始上传)。实测 task 43 压缩 360MB→18.9MB (19.1x) 534s 后原子 rename 成功,直传入队。 **运维教训**:Edge 服务重启必须 `--bind 0.0.0.0:5000`(误用 127.0.0.1 导致 NAS 无法访问,Poller 持续 RemoteDisconnected;本地 curl 掩盖问题)。task 43 因暂时性网络抖动三连超时标记 FAILED,已重置 PENDING 待压缩缓存复用重试。 ## 压缩缓存原子性修复 (2026-08-20 18:25, commit 1994de2) **根因**(task 300 损坏视频之谜的答案):服务被 kill 时 dispatcher 死亡但其 ffmpeg 子进程成为**孤儿**(父进程转 init)继续写压缩产物。旧代码直接写最终路径,半成品文件 `size>0` 且 mtime 较新 → 缓存复用判断误判为有效 → 重试任务上传损坏视频。多次重启叠加多个孤儿 ffmpeg 争抢 NAS ARM CPU(本轮实测 3 个并行)。 **修复**:压缩输出写 `{out}.{pid}.tmp`(带 PID 防多进程冲突),成功后 `os.replace` 原子 rename——缓存目录只可能出现完整产物;`_cleanup_compress_cache` 顺带清理超 1h 的 .tmp 残留。 **运维**:已杀孤儿 ffmpeg×2(task 43/44)并清理缓存目录、重置任务状态;每次重启 NAS 服务后应检查 `ps aux | grep ffmpeg41` 是否有孤儿残留。 ## 视频时间语义修正 (2026-08-20 18:05, commit 80b2fcc) 时区统一的延伸排查发现三处**时间语义** bug(时区对了但语义错): | # | 位置 | 问题 | 修复 | |---|------|------|------| | 1 | NVIDIA 集锦偏移 | frame_timestamps 是绝对时间(`21:31:14`→当日 77474s)被直接当视频内偏移,seek 超出 30min 视频 → 0 帧 → 集锦恒空 → 视频模式永远降级逐帧 | `analyze_video` 增加 `event_start_time` 参数,偏移 = 帧时间 − 视频开始时间(跨午夜 +86400);orchestrator 透传;ffprobe 时长过滤超界片段;fps=15 统一 CFR | | 2 | NAS dispatcher | `event_start_time` 用文件 mtime(=录制**结束**时刻),事件整体后移一个视频周期(~30min) | 新增 `_probe_duration`(ffmpeg 解析 Duration,NAS 无独立 ffprobe),start = mtime − duration | | 3 | 直传超时 | 压缩后 ~19MB 低于 20MB 阈值走直传,`timeout(10,120)` 在 1Mbps 下传 19MB 需 ~152s 必超时(task 42 三连败) | 加大到 `(10, 300)` | **两轮测试数据陷阱**:集锦 0KB/9.47s 异常均为测试时间戳超出视频时长(1801s > 1800s)所致,单输入与多输入 concat 行为本身正常——生产帧时间戳必在视频内,已加防护过滤。 **数据治理**:历史 FAILED 全量重置后 307 PENDING,其中 36 个源文件已被 NAS 清理 → 标记 FAILED('源文件已清理,跳过处理',failure_stage 枚举无 'dispatch' 用 'callback');271 个有效任务排队消化(~54h)。 ## 时区统一北京时区 (2026-08-20 17:45, commit 795e8ac) **背景**: Oracle Edge 机器实际为 UTC 时区(非凤凰城 MST),NAS 与视频均为北京时间,网页要求统一北京时区。 **排查结论**: NAS 端(dispatcher 的 `fromtimestamp(mtime)`、MariaDB NOW()、db_layer `_dt.now()`)与 UI(直读 MariaDB 时间)全部正确——正常链路数据无误(event 15/16/18/19 验证)。问题集中在 Edge 端(UTC 机器)三处: | # | 位置 | 问题 | 修复 | |---|------|------|------| | 1 | queue_manager(7 处) | `datetime('now','localtime')` 在 UTC 机器=UTC,队列时间戳慢 8h | 改 `datetime('now','+8 hours')`(SQLite 原生时区修饰符) | | 2 | compute_timestamps fallback | event_start_time 缺失时用 Edge 本地时间(UTC) | 改 `datetime.now(timezone(+8h))`;ISO 带时区输入统一转北京 | | 3 | nvidia_adapter._ts_to_seconds | 不支持生产格式 `YYYY-MM-DD HH:MM:SS`,解析全失败→集锦永远为空→视频模式永远降级逐帧(上一轮引入的 bug,HH:MM:SS 测试格式未暴露) | partition 取时间部分;drawtext 标签/提示词同步完整时间戳 | **数据修正**: event 17(task 298 手动测试缺 event_start_time 导致 start=end=落库时刻、frame 全 UTC)按视频文件名 04:34:10 重算;Edge 队列存量时间戳 +8h。 **验证**: 单测三例通过;部署后 task 41 队列时间正确写入北京时间 17:36:52。注意 SQLite `+8 hours` 修饰符写入的是 wall-clock 北京时间,与 MariaDB/SYSTEM 时区无关。 ## NVIDIA 原生视频输入切换 (2026-08-20 17:25, commit 55633d3) **调研结论**(免费托管 API `integrate.api.nvidia.com`,40 RPM 无限次调用): | 模型 | 状态 | 说明 | |------|------|------| | nvidia/nemotron-3-nano-omni-30b-a3b-reasoning | ✅ 采用 | Omni 模型,原生支持 video_url 输入(MP4,base64 data URI) | | meta/video-llama3-8b-instruct | ❌ 404 已下线 | 曾是 NVIDIA 官方视频理解模型 | | qwen/qwen2.5-vl-72b-instruct | ❌ 404 | 该 ID 不存在 | **实现**:NVIDIA 从"逐帧单图调用"切换为**原生视频输入**: 1. `analyze_video`:按关键帧时间点截取 ±1.5s 片段(drawtext 叠加时间戳,用连字符 `00-05-00` 避免 ffmpeg 冒号转义),拼成集锦视频(640 宽 CRF28,~35KB/片段),base64 后 `video_url` 单次调用 2. 输出全 schema JSON(frame_details + global_summary + entities_json),归一化对齐时间戳 3. `analyze_frames` 保留为降级路径;orchestrator 传 `video_path`,优先视频模式失败自动降级逐帧 4. timeout 20→120s,chat max_tokens 512→2048(reasoning 模型 token 消耗大) **实测**(360MB 测试视频,3 关键帧):集锦 107KB,全程 37s,动态动作识别准确(走动→坐沙发→坐餐桌),跨片段综合摘要正常——**显著优于旧逐帧静态识别**。 **注意**:nemotron-omni 为 reasoning 模型,10s 视频 ~20s 返回;Gemini 仍是首选(多图 34s),NVIDIA 视频模式作为同级降级链路。 ## Gemini 视觉解析排查 + 业务全流程梳理 (2026-08-20 16:30, commit f4e7424) **结论: Gemini 可以解析视频(多帧视觉分析),且有 40s 完成整段分析的实测记录,但当前被三重问题压制** ### 排查过程 1. 日志证据: 07:43:05 Gemini 返回了完整 JSON(耗时 40s,6 帧多图分析),但被本地 `parse_vlm_json` 拒收——"缺失字段: compute_provider" 2. 根因: `validate_schema` 把 `compute_provider` 列为必填,但提示词从不要求模型输出它(该字段是 orchestrator 解析成功后自行填充的记账字段)→ **Gemini 的有效响应被 100% 误杀**;NVIDIA 走单帧轻量解析绕过校验故不受影响 3. 修复 (commit f4e7424): `compute_provider` 移出必填清单,归一为空数组由 `format_cloud_result` 覆盖填充,已部署 Edge ### 当前外部服务状态(与代码无关的临时故障) | 模型 | 状态 | 表现 | |------|------|------| | Gemini | ❌ 503 过载 | "high demand, temporary",早间多次 503 | | NVIDIA NIM | ❌ 500 推理连接错误 | llama-3.2-11b-vision 端点持续 Internal Server Error | | Ollama qwen2.5:7b | ✅ 但纯文本 | 无法做视觉分析,故双云故障期间任务全失败 | task 298/300 失败均因双模型同时故障;NAS 端 FAILED 已重置 PENDING(22 个积压),模型恢复后自动重跑。 ### 强杀服务引发的边缘情况(已自愈,无需修码) - 强杀 processing 中的 worker 后,视频文件消失但队列行卡 PROCESSING → 新 run 的 60 次 seek 全部 0.07s 快速失败,但复用上一 run 残留的帧目录完成分析 - 已通过手动 reset PROCESSING→PENDING 恢复;正常流程下不建议 kill -9 处理中的任务 ### 业务全流程(当前架构) ``` [摄像头 SurveillanceStation] → 30min/段 落盘 NAS /volume1/surveillance (360MB 1080p) ↓ Scheduler (60s 扫描, 文件稳定 60s 判定) [MariaDB process_tasks: PENDING] ← 每段视频一个任务 ↓ Dispatcher (30s 轮询, 串行 limit=1) ① FFmpeg 预压缩: 480p/CRF28/veryfast, 360MB→22MB (16x), ~560s, 缓存24h ② 5MB 分块上传 Edge: 5 块, ~60-100s, 断点续传+3次块级重试 [Edge SQLite task_queue: PENDING] (UNIQUE nas_task_id, 失败重派发自动重置) ↓ Consumer 线程 (10s 轮询, TokenBucket 限速: Gemini 1000RPM/NVIDIA 40RPM) ③ 抽帧: 快速 seek 粗抽 60 候选 → MSE 帧差筛 6-12 关键帧 → 压缩 JPEG ④ 视觉分析: Gemini 多图(首选,~40s) → NVIDIA 单帧(降级,~30s) → 全失败则 FAILED ⑤ 融合: frame_details + global_summary + entities_json (compute_provider 记账) ↓ Poller (30s 拉取 /api/edge/results, SUCCESS+FAILED 均回传) [MariaDB monitor_events + event_details 落库, event_id 递增] ↓ FAM-UI (Streamlit :8501) / Chat (Gemini→NVIDIA→Ollama 降级) [用户查看事件时间线 / 命名成员 / 问答] ``` **单任务周期 ~12 分钟**(压缩 9.3min + 上传 1min + 分析 1min),瓶颈在 NAS ARM CPU 压缩速度。 ## 视频上传问题根因排查与修复 (2026-08-20 15:45, commit 8cb5553) **问题定性**: 网络问题 + 逻辑问题双重叠加 **网络层(已解决)**: NAS→Oracle 跨境上行带宽仅 ~1Mbps,360MB 原始视频上传需 10-20 分钟且频繁超时。 方案: NAS 端 FFmpeg 预压缩 (480p/CRF28/veryfast, 36 倍压缩比 360MB→22MB) + 5MB 分块断点续传。 **逻辑层(本次修复)**: 压缩方案生效后上传成功,但 Edge 端分析全部失败 "All models failed in visual analysis": | # | Bug | 根因 | 修复 | |---|-----|------|------| | 1 | 全部视觉模型调用失败 | Edge 服务手动重启时未 source .env,GEMINI/NVIDIA API key 丢失 | config_loader 启动时自动加载 .env(export KEY=VALUE 格式,不覆盖已有环境变量) | | 2 | 失败结果黑洞 | get_undelivered_results 只查 SUCCESS,FAILED 永不回传 NAS | 包含 FAILED 状态,/results 构造 failed 负载,NAS Poller 标记任务 FAILED | | 3 | 重派发死锁 | INSERT OR IGNORE + UNIQUE(nas_task_id),失败后重传被静默忽略 | enqueue 检测 FAILED/已交付 SUCCESS 行时重置为 PENDING 重跑 | | 4 | Poller 落库崩溃 | db_layer valid_stages 含 'process' 但 MariaDB ENUM 不含 → pymysql DataError 1265 | ALTER TABLE 扩展 ENUM + ddl.sql 同步 | **验证结果**: - Task 293 (压缩后 22.2MB): NVIDIA 降级路径分析成功,event_id=13 落库(Gemini 临时 503 自动降级) - Task 295: 全链路成功 event_id=14(压缩 9.3min → 上传 1min → 分析 55s → Poller 落库) - 流水线单任务周期 ~12 分钟,积压 ~20 个任务自动消化中 **运维要点**: Edge 重启务必 `source /opt/fam-edge/.env` 或使用 start_gunicorn.sh(代码已兜底自动加载 .env,双保险) ## 真实视频性能基准测试 (2026-08-20) 测试视频: `Generic_ONVIF-001-20260819-210403` (30分钟, 360MB, 1080p H.264) | 步骤 | 耗时 | 详情 | |------|------|------| | 1. 视频传输 (NAS→Oracle) | 70s | 360MB, 5.1 MB/s (Tailscale) | | 2a. FFmpeg 快速 seek 抽帧 | 33s | 60 张候选帧 (每30s一张) | | 2b. OpenCV 关键帧筛选 | 5s | MSE 帧差 → 12 张关键帧 | | 2c. 压缩 | 0.6s | 12 帧, 总 1.9MB | | 3. Ollama llava-phi3 分析 | 820s | 12 帧, avg 68s/帧, 0.9 tokens/s | | **总计** | **929s (15.5min)** | 30 分钟视频处理 | AI 分析瓶颈: 帧5耗时 229s (疑似 ARM CPU 热降频), 其余帧 50-65s ## FAM-Core API 测试结果 (2026-08-20) | 端点 | 方法 | 测试数据 | 结果 | |------|------|---------|------| | /health | GET | - | ✅ `{"service":"fam-core","status":"ok"}` | | /api/status | GET | - | ✅ `{"dispatcher_running":true,"scheduler_running":true}` | | /api/member/list | GET | - | ✅ 返回 2 名成员 (人物A未命名, 人物B=汤圆) | | /api/member/unnamed | GET | - | ✅ 返回 1 名未命名成员 (人物A) | | /api/member/name | POST | 人物A→张三 | ✅ 成功命名, 0 条事件记录回溯更新 | | /api/chat/ask | POST | "今天有什么事件?" | ✅ 端到端成功, 回答"今天没有观察到张三" | ## 端到端聊天链路验证 (2026-08-20,新架构) ``` 用户 → FAM-Core (NAS:8000) → FAM-Edge (Oracle:5000) → Gemini / NVIDIA NIM / 本地 Ollama (兜底) /api/edge/chat/ask run_qa 三模型降级编排 ``` - FAM-Core Chat-Handler 调用 `qa_url` 配置的 `http://129.146.26.249:5000/api/edge/chat/ask`(不再直连 Ollama) - FAM-Edge `run_qa` 按 Gemini → NVIDIA → 本地 Ollama 顺序调用 `chat()`,首个成功即返回 `{answer, provider}` - **验证结果**: - Edge 单测:`/api/edge/chat/ask` → 200,`provider=nvidia`(Gemini 30s 超时后 NVIDIA 兜底成功,耗时 51s) - NAS E2E:插入临时事件上下文后 `/api/chat/ask` → 43s 返回模型回答,`context_summary` 命中 1 条事件,chat_id 落库;测试数据已清理 - 无上下文时走短路分支(直接返回提示,不调 Edge),响应 <1s,属正常设计 ## UI 时间轴重构 + 关键帧全链路持久化 (2026-08-20 20:45, commit 0d5173f/c5c8496) **UI 重构(fam-ui/src/app.py 全量重写)** - 深色监控面板主题(CSS 变量 + 卡片化设计) - 事件时间轴页:左侧事件列表(时间/摄像头/帧数/摘要),右侧竖向时间轴(时间点 + 关键帧缩略图 224px + 人物/动作/衣着摘要 + 模型徽章),需关注事件红色高亮 - 其余页面:AI 对话、对话历史、成员命名、统计图表同步适配新主题 - 侧边栏任务队列状态(待处理/处理中/失败计数) **关键帧持久化链路(Edge → NAS → UI)** - Edge `orchestrator._attach_frame_images`:处理结果注入关键帧 base64(按视觉分析输入帧位置对齐) - NAS `event_receiver._save_frame_images`:base64 解码落盘到 `fam-ui/static/frames/event_{id}/frame_{idx}.jpg`(先落盘 pop 掉 base64 再入库,避免大字段进 MariaDB) - UI `load_frame_b64`:读取落盘帧图内联 data URI 渲染;旧事件无帧图显示占位符 - 配置:`fam-core/config.yaml` + `fam-ui/config.yaml` 新增 `storage.frame_image_dir` **multipart 上传超时修复(commit c5c8496)** - 现象:task 50/51 直接上传 17.1MB 100% 失败 `('Connection aborted.', TimeoutError)`,恰好 10s 超时 - 根因:requests/urllib3 发送 multipart body 期间 socket timeout 取的是 **connect timeout** 参数而非 read timeout,原 `(10, 300)` 下 17MB 跨境 1.4MB/s 需 ~117s 必然超时 - 修复:`_dispatch_direct` timeout 改 `(120, 300)` **端到端验证(2026-08-20 20:24-20:41)** - task 53:压缩 20.6MB → 分块上传 5/5(36s)→ Edge 分析 → Poller 落库 event_29(6 帧落盘) - task 50:直接上传成功(117s,超时修复生效)→ event_30(6 帧) - task 51:直接上传成功 → event_31(6 帧);task 52:重压缩 21.9MB → event_32(7 帧) - UI 浏览器实测:点击事件 #29 → 时间轴 6 个时间点、6 张关键帧全部加载(夜视画面可见人物)、每帧下方人物/动作摘要正常渲染 - 重启残留处理:task 52 重启时被中断的孤儿 ffmpeg 已清理(`{out}.{pid}.tmp` 原子改名机制保证无损坏文件),重置 PENDING 后自动重跑成功 **经验** - Streamlit 1.61 窄视口(<960px)下 st.columns 自动垂直堆叠,详情区被挤到事件列表下方;浏览器自动化验证需注意 window 尺寸,用户桌面浏览器不受影响 - 旧事件(新代码部署前)无落盘帧图属预期,占位符正常显示 ## 历史事件关键帧补抽 (2026-08-20 22:05, commit 8a333d7) **现象**:用户打开 UI "全是暂无帧图"。落盘链路本身正常(event 29-39 持续产出),根因是**事件列表按 event_start_time 倒序,第一屏前 11 条全是帧图注入上线前的旧事件(2026-08-20 的 event 8-17),有帧图的新事件都是 2026-08-15 录像、排在列表深处**。 **修复**:`fam-core/tools/backfill_frames.py` 从原始视频补抽关键帧 - 偏移计算:offset = frame_timestamp - event_start_time(5 分钟间隔帧) - NAS ffmpeg41 没有 image2 muxer(`-f image2` 报 not suitable),也没有 ffprobe → 用 `-f singlejpeg` 输出 jpg,用 ffmpeg header 的 `Duration:` 行解析时长 - 抽帧参数与预压缩一致:480p 等比缩放 + `-q:v 5`(~53KB/张) - 幂等:单帧文件存在跳过,帧数齐全的事件整条跳过 - 实跑:11 事件 65 帧全部成功,8.1MB;2026-08-14 的 10 个事件(event 18-27)视频已被 NAS 清理无法补,UI 占位符兜底 - 浏览器验证:默认打开的最新事件 #11 帧图 6/6 全部加载,无占位符 ## 历史事件帧图 100% 补齐 (2026-08-20 22:40, commit 80cc324) **问题**:用户翻到 2026-08-14 21:31 事件(event 27),仍是"暂无帧图"——第一轮补帧时原始视频已被 Surveillance Station 保留策略删除(event 18-27 共 10 个事件)。 **副本排查**(按优先级): 1. NAS 原始目录:`/volume1/surveillance/.../20260814PM/` 整目录已删,无 #recycle 2. Synology 元数据 `@SSRECMETA`:Thumbnail(缩略图)+ Preview(每 20s 一张 8KB jpg)都在,但保留约 8 天,20260814PM 目录最早条目只到 08-15 00:31(北京),21:31 时段已被清掉 3. **dispatcher 压缩缓存 `/tmp/fam_compressed/task_36~45/`:480p 压缩副本全部健在**(每事件 11-33MB)✓ **修复**:`backfill_frames.py` 增加压缩缓存回退——原始视频缺失时改用 `/tmp/fam_compressed/task_{task_id}/{basename}` 抽帧。实跑 event 18-27 补 78 帧 0 失败(task 41 一份视频覆盖 event 21/23 两个事件)。event 27 六帧校验 JPEG 魔数全部有效。**至此数据库 36 个有明细的事件帧图 100% 齐全,UI 不再有占位符**(共 16MB)。 **注意**:压缩缓存是 /tmp(重启即失 + 24h 清理策略),本次是赶在副本消失前抢救出来的;新管道从 event 29 起处理时即落盘帧图,不再依赖事后补抽。 ## 事件列表带年月日 (2026-08-20 23:05, commit f50f98e) - 跨多天的事件列表仅显示 `HH:MM` 无法区分日期,用户反馈要带上年月日 - 按钮:`05:34 · 客厅 · 6帧` → `08-20 05:34 · 客厅 · 6帧`(MM-DD HH:MM) - 摘要行:前缀浅蓝完整日期 `2026-08-20 · 摘要…`(年月日齐全,#7dd3fc) - 摘要截断 26→30 字适配前缀长度,单行不溢出 - 浏览器验证 15 个按钮均正确显示、无排版错乱 ## Gemini 全量降级 NVIDIA 排查修复 (2026-08-21 00:10, commit eaf4ef3) **现象**:UI 事件全是"模型: nvidia",Gemini 不出结果。 **排查发现的三个链路问题**: 1. **Gemini 每日配额耗尽(根因)**:`GenerateRequestsPerDayPerProjectPerModel-FreeTier` 每天每模型仅 **20 个请求**(实测 429 响应体 quotaValue=20,模型 gemini-3.7-flash)。日均 30+ 任务,配额烧光后持续 429 → 全部降级 NVIDIA(最近 40 任务:35 nvidia / 4 gemini)。且 429/503 无重试无换模型,一次失败即放弃。 2. **Edge 日志黑洞**:gunicorn 手动 nohup 启动,stdout/stderr 指向 /dev/null,**所有 Edge 日志全部丢失**,线上问题无法排查。 3. **配置漂移**:commit 55633d3 当时 nemotron 模型名只改了 Oracle 线上 config 未回传 git 仓库;本次 scp 部署 config 时把线上 nvidia 覆盖回 llama-3.2(丢失原生视频输入能力)。 **修复**: - `gemini_adapter` 抽 `_generate()` 统一模型链调用:`gemini-flash-latest → gemini-flash-lite-latest`(各自独立 20/天配额,合计翻倍);429 立即切换下一模型(当日配额不会恢复,不重试);503 退避 3s 同模型重试一次再切换;视觉分析与问答统一走链 - config:gemini 增 `fallback_models`;nvidia 恢复 `nemotron-3-nano-omni-30b-a3b-reasoning` + timeout 120(nemotron 实测仍在线 200) - **systemd 接管 Edge 服务**:`fam-edge.service`(Restart=always + 日志 append 到 `/opt/fam-edge/logs/fam-edge.log`),取代 nohup 黑洞 - 健康检查校验整条模型链 **端到端验证**:task 73(30 分钟视频 12 关键帧)—— flash 429 → lite 1s 接管 → 12 帧直出 JSON → event_52 落库 `provider=["gemini"]`。链路恢复:Gemini flash(20/天) → Gemini lite(20/天) → NVIDIA nemotron 兜底。 **经验**: - Gemini 免费层配额按模型独立计算,多模型 fallback 链是免费层扩容的唯一手段;429 响应体的 `quotaValue` 字段会写明每日上限 - 线上手动改过的配置必须回传 git 仓库,否则下次部署必然回退(本次真实踩坑) ## 出现人物统计去重 + 导航移页面顶部 (2026-08-21 00:40, commit 3fb86ee) **问题 1:出现人物 20 是错的**。`person` 是自由文本,一帧可能是多人组合且带括号特征描述(如 `张三 (红色T恤, 白色裤子), 汤圆 (蓝色上衣), 人物3`),`COUNT(DISTINCT person)` 把每种组合字符串当作一个"人物"。全局 20 个 distinct 字符串实际只有 6 个真实人物。 - 修复:新增 `resolve_persons()` —— 正则去括号描述 → 按中英逗号/顿号/斜杠拆分 → `family_members` 抽象标签映射真名(人物A→张三、人物X→爸)→ 集合去重。统计显示 20 → 6。 **问题 2:成员命名/问答界面"不见了"**。两页面实际正常(浏览器实测渲染无误),根因是导航放侧边栏 radio,窄屏(手机)下 Streamlit 自动收起侧边栏,页面入口藏进汉堡菜单。 - 修复:导航改为页面顶部 `st.segmented_control` 横向分段控制器,任何屏宽直接可见;侧边栏保留品牌标题和任务队列状态。 浏览器验证:顶部分段导航 5 页可达、成员命名页显示 张三/汤圆/爸、AI 对话页表单正常、无报错。 ## 摄像头对外入口收口为单一域名 smart-camera.zichuan.xyz (2026-09-01) **决策**:摄像头系统(fam-ui 前端 + NAS fam-core :8000 后端)对外只暴露一个域名 `smart-camera.zichuan.xyz`(前端 `/` + 后端 `/api`),下线独立的 `api.zichuan.xyz`,并从 `oracle.zichuan.xyz` 移除摄像头相关路由(其 `/ai`、`/fam`、`/wordpress` 仍保留给其它服务)。 **改动(基础设施,不在本仓库)**: - Oracle Caddy(`/etc/caddy/Caddyfile`):新增 `smart-camera.zichuan.xyz` 块(`/` 静态 fam-ui + `/api`、`/login` 反代 `127.0.0.1:8000`,经 frp 隧道回源 NAS);删除 `api.zichuan.xyz` 块;`oracle.zichuan.xyz` 与 `:80` 兜底块移除摄像头 root + `/api` 路由(改 404)。 - DNS:新增 `smart-camera.zichuan.xyz → 129.146.26.249`(RecordId 2387553886)。 - NAS `/volume1/web/sentinel-home-ai/.env`:`AUTH_HUB_ISSUER` 改为 `https://auth.zichuan.xyz`、`AUTH_HUB_REDIRECT_URI` 改为 `https://smart-camera.zichuan.xyz/api/auth/callback`,且 `AUTH_HUB_*` 四行补全 `export` 前缀(见下)。 - auth-hub FAM-Core 客户端 `a2PJTZuKQYxt1oGh` 回调改为 `https://smart-camera.zichuan.xyz/api/auth/callback`(保留旧 IP 作兜底)。 **顺带修复两个生产 Bug**(见 `scripts/start_core.sh`): 1. 脚本先 `cd "$APP_DIR"` 再算相对 `SCRIPT_DIR`,导致 `source .env` 路径错位、`set -e` 下直接退出,gunicorn 起不来。 2. `.env` 里 `AUTH_HUB_*` 是裸赋值(无 `export`),`source` 后不进环境,gunicorn 子进程读不到 → 登录一直 503 fail-closed(与之前“生产未部署”一致)。脚本加 `set -a` 包裹 source 修复。 **验证**:`smart-camera.zichuan.xyz/` 200、`/login` 302→auth.zichuan.xyz(redirect_uri=smart-camera)、`/api/*` 401;`oracle.zichuan.xyz/` 404、`/fam` 200;`api.zichuan.xyz` 已不通。fam-core 经修复脚本重启后 health ok。