# 2026-08-21 工作记录 ## #51 部署验证 - rclone 同步 Google 硬盘 + 全链路跑通 ### Oracle(fam-edge)部署完成 - rclone v1.75.0 装到 /usr/local/bin(aarch64,zip 解压) - 服务账号 JSON 存 /opt/fam-edge/gdrive-sa.json(chmod 600,用户提供) - rclone remote `gdrive:`(drive scope + service_account_file) - 同步脚本 /opt/fam-edge/rclone_sync.sh + systemd timer(每 5 分钟): `rclone sync --drive-shared-with-me gdrive:SS/Generic_ONVIF-001 /opt/fam-edge/gdrive_videos` - 首次全量同步 20 个视频 7.52GB(~2 分钟完成) - 共享目录是 `SS`(群晖监控站),摄像头 `Generic_ONVIF-001` 按 YYYYMMDDAM/PM 分目录 ### 踩坑与修复(3 个 commit) 1. `watch_processor._scan_files` 改 os.walk 递归(原 listdir 只看根目录,视频在子目录) 2. `.env` 的 GEMINI/NVIDIA key 是旧无效值 → 用 env.sh 的真实 key 修复(config_loader 只读 .env) 3. **Gemini 上传**:必须用 `/upload/v1beta/files` 端点(/v1beta/files 是元数据端点)+ resumable 协议,370MB 整视频 ~55s 上传成功 4. **NVIDIA**:base64 塞 payload 超 25MB 上限 → 改 Assets API(POST 拿 assetId+uploadUrl,PUT 上传);PUT 必须**全小写 content-type 且值=POST contentType(video/mp4)**,否则 S3 预签名 SignatureDoesNotMatch 5. `parse_vlm_json` schema 从旧架构(entities_json/frame_details)改为新架构(global_summary/events/people_mentioned),兼容旧结构转换 ### 端到端验证通过 - 21 视频全部登记,video 1/2 已 done(provider=gemini,凌晨视频无事件=分析正确) - NAS 重启 fam-core 触发立即同步:sync_videos=21,cursor 推进正确 - 剩余 19 个视频串行处理中(Gemini 免费配额 429 会降级 flash-lite,之后可考虑 NVIDIA) ### 待观察 - Gemini 免费 key 配额限制(~20 req/day),后续 19 个视频可能大量 429 - rclone timer 每 5 分钟增量同步验证 - NAS 30 分钟自动同步已生效 ## 生产-消费队列 + 模型超时×2(新需求,commit ce27e4a) - 新增 `fam-edge/src/fam_edge/video_queue.py`:VideoQueue 生产-消费队列 - 生产者线程:30s 轮询 rclone 落地目录,新文件登记 pending 并入队;启动时补入队 DB 未处理完的(重启恢复,实测 14 个) - 消费者线程:max_concurrent=1,从队列取 video_id → VideoProcessor.process_video(timeout_multiplier=2) - 防重复入队:_queued set;重试上限:max_retries=2(oracle_db.videos 加 retry_count 列,mark_video_failed 递增) - `video_processor.process_video` 加 timeout_multiplier:遍历 adapter 时临时 adapter.timeout = 原配置 ×倍数,finally 恢复(gemini 600→1200s 生效日志确认) - config.yaml video_processing 加 timeout_multiplier: 2 / max_retries: 2 - 删除旧 watch_processor.py,app.py 改启动 VideoQueue - oracle_db:retry_count 列(建表+ALTER 兼容旧库)、get_video_by_id、PRAGMA busy_timeout=10000 - 部署验证:启动恢复入队 14 个、video_id=8 gemini 600→1200s、生产者登记 id=22 新视频、done 持续增长 ## 模型调用统计界面 + lite 超时实测×4(commit b87b38b + 3e94c2a) - Oracle 端:oracle_db 新增 model_calls 表(provider/model/video_id/started_at/duration_sec/success/error/created_at);BaseModelAdapter 加 model_call_hook + _emit_model_call,gemini(_generate_video 每模型每attempt)/nvidia(analyze_video) 每次请求记录;get_sync_delta 下发 model_calls(created_at >= since + NAS 幂等 upsert 防漏) - gemini 支持模型级 model_timeouts(最终值不参与 ×2):实测 lite 370MB 视频耗时 22.6s(生产 27~34s),lite 限制 = 22.6×4 ≈ 90s;flash 仍 1200s(600×2)。日志确认:flash 本轮 1200s / lite 本轮 90s - NAS 端:sync_model_calls 镜像表(ddl 8.1);db_layer upsert_sync_model_calls/get_sync_model_calls/get_sync_model_calls_stats;oracle_sync 拉 model_calls(last_count 变 4 元组) - fam-ui 新增 "🤖 模型统计" 页:按模型聚合(成功/失败/成功率/平均耗时/最后调用)+ 最近 100 条调用明细(请求时间/模型/耗时/状态/失败原因/视频) - 验证:Oracle model_calls 正常记录(flash 429_quota 失败、lite 成功 27~34s);NAS 同步 4 条,fam-ui 200 ## 整体排查 + lite 超时 8 分钟(commit 83eefc1) - **NVIDIA 400 排查结论**:`nvidia/nemotron-nano-12b-v2-vl` 对整视频分析**稳定复现 400/500 服务端内部错误**("not enough values to unpack (expected 2, got 1)"),所有视频、有无 num_frames 均复现 → NVIDIA 兜底在当前模型/端点**不可用**(asset 上传 OK,chat.completions 必失败)。建议从 vision_order 移除或换模型 - gemini-flash-lite 超时 90s → **480s(8 分钟)**,config model_timeouts 已改并部署(日志确认 480s) - 处理进度:done 33 / failed 1(video24 待重试)/ pending 12 ## NVIDIA 多模型降级链 + 实测结论(commit b7b5fe6) - nvidia_adapter 改造:model_chain(asset 上传一次,逐个模型 video_url 引用尝试)+ switch_interval_sec 切换间隔(默认 5s)+ model_timeouts 每模型独立超时 + compute_provider 带模型名(nvidia:{model}) - **实测全部 NVIDIA 云端视频候选不可用**:omni 500(asset_id 引用失败)、12b 400、llama-3.2-11b-vision 400(不支持视频)、cosmos-reason2/phi-3-vision/gemma-3/kosmos-2/fuyu-8b/paligemma 404;base64 超 25MB;public URL 方案被用户否决(不暴露视频) - config:model_name=omni + fallback 12b/llama-11b,链机制保留,未来可用模型出现只需改配置 - 手动验证:模型链 [1/3]→[2/3]→[3/3] 逐个尝试+5s 间隔+失败原因记录,全部失败返回 None - ⚠️ 安全提醒:Oracle 曾短暂起 http.server:8000 暴露视频目录做 POC,已按用户要求关闭 ## 前端修复:SQL 1054 + 视频缩略图(commit 4148507) - **SQL 1054 修复**:fam-ui 事件查询 `SELECT ... camera_name FROM sync_events`(该列在 sync_videos)→ 改 JOIN sync_videos 取 v.camera_name - **视频缩略图**:Oracle 装 opencv-python-headless(5.0);video_processor 处理成功后抽首帧(宽≤640, JPEG q65)存 /opt/fam-edge/thumbs/{video_id}.jpg;api_gateway 新增 GET /api/oracle/video/{id}/thumb(token 校验,无 token 401);已 done 46 视频 backfill 46/46 - **fam-ui**:config 加 oracle_url(129.146.203.203:5000)+oracle_token(${ORACLE_SYNC_TOKEN});事件时间轴视频会话头显示缩略图(img onerror 优雅降级);start_ui.sh 补 source 项目 .env 注入 token - 验证:NAS→Oracle thumb HTTP 200/40KB;fam-ui 进程 token env 就绪;UI 8501 正常 - 人物管理无照片:架构局限(LLM 仅输出人物名,无图像锚点),如需人物照片需从视频定位+裁剪,待后续 ## 每个事件对应时间点画面截图(commit 05f727a) - oracle_db.mark_video_processed 返回 event_ids(与 events 一一对应) - video_processor._generate_event_thumbs:事件 ts - 视频 event_start_time = 偏移秒 → cv2 跳帧(CAP_PROP_POS_MSEC)截图,存 /opt/fam-edge/thumbs/ev_{event_id}.jpg(宽≤640, q65;解析失败/负偏移取首帧) - api_gateway 新增 GET /api/oracle/event/{event_id}/thumb(token 校验) - fam-ui 事件查询加 e.id;render_event_list 每条事件显示对应截图(onerror 隐藏降级) - backfill:已 done 视频 102 个事件截图全部生成;验证 NAS→Oracle HTTP 200/51KB、无 token 401 ## 事件截图时间对不上修复(commit 5233296 + 3fcd00d) - **根因**:模型输出"绝对北京时间"靠自身推算,1 小时视频内误差可达分钟级 → 截图按不准的时间定位帧必然图文不符 - **修复**:① prompt 改为要求输出"视频内相对时间 HH:MM:SS"(模型对相对位置判断准);② 后端 _parse_event_ts 解析相对时间 → 绝对时间 = start + offset 精确落库(兼容旧绝对格式);③ 截图直接用 offset 跳帧 - **连带 bug**:_parse_event_start_from_filename 只认带分隔符日期(2026-08-21),监控文件名是纯数字 20260820-140416 → event_start_time 空 → 新增纯数字格式解析 - 启发式:相对时间 >6h 视为模型误输出绝对时间,不强行定位 - 验证:重置 video 46 重分析 → event_start=14:04:16 ✓,事件 ts 14:05:37/14:06:21/14:13:02,截图 offset 36/48/81/125/526s 精确 ✓ - 旧视频(除重分析的)仍用旧时间戳截图;如需全部修正需批量重分析(成本高,用户确认后再做) ## 全部旧视频重分析 + 人物管理图片(commit 92ef9fe) - **全量重分析**:清空 events(104) + 旧事件截图(108) + 重置全部 45+1 个视频 pending → 队列后台串行重跑(新相对时间逻辑,预计 1.5-2.5h,Gemini flash 429 → lite 兜底) - **人物管理图片**:fam-ui 每个人物身份取其一 label 出现事件的截图作头像(查 sync_events.person_list_json LIKE → 显示 Oracle event thumb) - 修正:video 46 起初被排除重跑但 events 被清 → 一并重置统一重跑 ## 队列健壮性三项(commit 6afdda5,用户需求清单) 1. **失败重试间隔 30s→1h**:videos 表加 last_fail_at(mark_video_failed 记录);video_queue._retry_allowed 对 failed 要求 retry_count0/可打开/可读帧/元数据 fps-frames-duration-分辨率);新文件先过 mtime 稳定窗口(stable_window_sec=60 防 rclone 半成品)再校验,失败登记 status='invalid' 不入队(可追溯,_retry_allowed 排除);process_video 处理前二次确认(失败标 failed:invalid_file:xxx) - 验证:正常视频 ok+meta(30min/2880x1620/14.28fps)、损坏 cannot_open、空文件 file_empty;部署后日志"重试上限 10" - 重分析 42 个 pending 继续后台跑 ## 代码审查 18 项处理(commit 034dca9 + 4e85a98) - **已解决(新增)**:#18 密钥明文直配 config.yaml(token/gemini/nvidia,弃 .env 依赖,验证 token_ok/key 长度正常);#5 appearances 改 distinct 视频数覆盖校准(set_person_appearances,防 reconcile 累加膨胀);#9 oracle_sync _pull_lock 防 trigger_now 与后台并发双拉;#11 LLM 合并命名解析 target 已有 canonical(统一显示名);#12 熔断器状态转换加锁;#10 问答人名匹配改 JSON_CONTAINS 精确匹配(MariaDB 语法验证 OK);#4 OracleDB _write_lock 复合写串行化;#6 每消费者独立 VideoProcessor(消 adapter.timeout 共享竞争);#15 done 视频文件被覆盖 mtime>processed_at 自动重置重分析 - **累计已解决**:#1/#2/#3/#4/#5/#6/#9/#10/#11/#12/#15/#18;部分缓解 #7/#14/#17 - **未解决(说明)**:#8 删除传播(tombstone 大工程,暂缓);#13 Gemini 文件/NVIDIA asset 残留清理(暂缓);#16 gunicorn 线程数(运维配置,暂缓) - 重分析进度:done 13 / pending 33(约剩 1h) ## Prompt 集中化重构(commit a72b286,用户提供详细设计文档) - 新增 fam-edge/src/fam_edge/ai_orchestrator/prompts.py:build_video_prompt(含 3 秒密度抽取/7 维度描述/people_mentioned 一致性强制/输出硬约束/边界情况/camera_name 注入)、build_chat_prompt、build_person_merge_prompt(唯一性+保守不合并);__init__ 导出 - gemini/nvidia adapter._build_video_prompt 改调共享函数(消除两份发散),注入 camera_name - person_service._llm_merge 改调 build_person_merge_prompt - fam-core chat_handler 新增 prompts.py(跨模块独立维护,风格一致),删除内联 CHAT_SYSTEM_PROMPT,chat_ask 改调 build_chat_prompt - 部署验证:prompt 生成正常(3 秒密度/camera/唯一性均含);fam-edge active;fam-core health OK - 注意:3 秒密度可能让有人时段 events 数百条,Gemini maxOutputTokens=4096 可能截断——若出现 JSON 解析失败需提高 max_tokens ## 最新代码调试 + 人物标识清洗(commit 0d73c23) - 调试结果:服务全正常(fam-edge active、NAS core/ui 200、rclone timer active);重分析 done 26/pending 20;新 prompt 正常——有人视频 5-16 事件、无人时段(晚 19:31/凌晨 5 点)events=0 合理、无 JSON 解析失败 - 发现并修复:模型输出 people 含 known_members 上下文格式串"人物A(别名/标识:人物B)"污染人物表 → prompts.py 明确"people 只填标识本身不带括号注释" + video_processor._clean_person 后端清洗(events.people 与 people_mentioned 均清洗)+ 清理存量脏 label(人物A(别名/标识:人物B)→ 人物A) - people 表现状:A/B/C/D 四标签,LLM 合并 B/C/D → canonical 人物A ## 前端日期标签与视频文件对不上(commit da76319) **用户反馈**:前端显示"2026-08-21 · 画面静止/无人员活动",和下面的视频文件对不上。 **根因链**(排查发现): 1. 前端事件时间轴/统计按 `processed_at`(**分析处理时间**)做日期分组——全量重分析都在 8/21 完成,导致 8/15~8/20 录制的视频全部堆在"2026-08-21"标签下,与文件名(录制时间)错位 2. Oracle videos 表 8/21 11:29~12:59 被整表重建(created 全在此区间),重分析串行进行 3. 20 个视频 event_start_time 为空:video 30-45 在 13:00~13:25 处理时服务器跑的是旧文件名解析代码(无纯数字正则 YYYYMMDD-HHMMSS),且被后续部署重启中断(pending);28/29 从未处理 4. NAS 镜像状态与 Oracle 错位(35-45 NAS 显示 done、Oracle 实为 pending)——Oracle 重建后 NAS 增量未对齐 **修复**: 1. **日期维度统一改视频实际录制时间**:fam-ui(事件时间轴统计/列表/关注事件统计)与 db_layer(get_sync_videos/get_sync_stats/query_sync_events_for_person_date)全部改用 `COALESCE(NULLIF(event_start_time,''), processed_at)`(录制时间优先,回退处理时间) 2. **Oracle backfill**:对 12 个缺失 start 的视频按文件名解析回填(30/33-42/45)→ Oracle 46 个视频全部有 event_start_time 3. **NAS 强制全量重同步**:删 sync_cursor last_since + 重启 fam-core → since='' 全量拉取 → EMPTY_START 20→0、状态与 Oracle 对齐(done 28/failed 4/pending 14) **验证**:done 视频按录制日期分布 8/15:10、8/16:8、8/17:2、8/18:2、8/19:2、8/20:1、8/21:3;8/15/8/20 筛选正确;fam-ui 200 无报错。 **遗留**:Oracle 端 pending 14 个在队列继续串行处理(video 28 处理中);failed 4 个(24/25/26 等,Gemini 429)等 1h 重试间隔自动重试。 ## Google 硬盘删除联动 + DB 摘要保留(用户需求确认,无代码改动) - **用户需求**:①谷歌删了甲骨文也删(文件层);②Oracle/NAS 数据库生成的摘要不能删(DB 层) - **验证结论**: - 文件层:rclone sync 本就是镜像语义(远程删→本地删),实测放临时文件→sync→Deleted:1 确认生效 - DB 层:video_queue 生产者只扫描"目录存在的文件",文件消失不影响 Oracle videos/events/people 记录;NAS 镜像照常同步,前端摘要保留;截图接口 404 由前端 onerror 降级 - **加固**:rclone_sync.sh 加 `--max-delete 200`(防 Google API 临时故障级联误删本地,单次最多删 200 个),脚本验证 exit=0、75 文件不受影响 - 注意:当前是**单向镜像**(Google 为源→本地),不建议反向传播(本地删→谷歌删会误删原始监控),如需 rclone bisync 真双向需用户确认风险 ## 实时服务状态界面 + 7 天活动记录(commit 0d0a7f6 + fec9a3e) - **Oracle**:service_activity 表(service/action/detail/ts,写入时清 7 天前);record_activity/get_recent_activities/get_queue_status;VideoQueue 打点(register/reanalyze/process_start/process_done/process_fail)+_current 当前处理跟踪+status();PersonService 打点(merge_done/merge_skip);api_gateway 新增 GET /api/oracle/activity(token 鉴权,返回 queue/db.by_status/rclone/person/model_calls/最近50条活动) - **rclone_sync.sh**:同步结果写 activity 表(service=rclone, transferred/deleted/exit);TRANS 提取正则修过一次(输出带 B 单位) - **fam-ui**:新增"🖥 服务状态"页(导航第7项):状态卡(队列运行/排队/完成/待处理/失败 + 当前处理视频 + rclone/人物/NAS同步/模型 4 张服务卡)+ 最近活动时间流(服务徽章),直连 Oracle /api/oracle/activity(复用 oracle_url+token) - **踩坑**:video_queue.status() 用 Dict 注解未导入 → worker 启动 NameError → 补 typing.Dict - 验证:接口数据正常(queue running/queued 19/当前 video 56;rclone sync_done/person merge_done 打点生效;401 鉴权;NAS→Oracle HTTP 200);fam-ui 200 ## 模型调用时间差 8 小时修复(commit 02fc80b) - **根因**:gemini/nvidia adapter 的 model_calls.started_at 用 `datetime.now()`(Oracle 服务器 UTC),而 created_at 用 _now_iso()(北京时间)→ 前端模型统计/服务状态页时间差 8h - **修复**:两个 adapter started_at 改 `datetime.now(timezone(timedelta(hours=8)))`;Oracle 历史 215 条 +8h;NAS 镜像 sync_model_calls 210 条 DATE_ADD +8h - 验证:最新记录 started 16:05:02 与 created 对齐(北京时间)✓ ## 人物管理无照片修复(commit 27bfd6a) - **根因**:NAS sync_events 残留重分析前旧事件(223 条 vs Oracle 115 条,id 1-108 旧事件未删除——#8 删除传播未做的副作用)。人物页头像查 `ORDER BY e.id LIMIT 1` 取到旧事件 id=1 → Oracle ev_1.jpg 不存在(重分析后事件从 109 起)→ 404 → onerror 隐藏 → 无照片 - **修复**:① NAS 清空 sync_events + 重置 cursor 全量重拉 → 精确 115 条镜像;② api_gateway event_thumb 兜底:ev 缺失时查所属 video 返回 thumbs/{video_id}.jpg(视频首帧);③ 清理 Oracle people 脏 label(人物A(别名/标识:人物B)→人物A) - 验证:NAS→Oracle ev109 HTTP 200/51KB ✓;人物页查询到的都是新事件 id → 截图存在 ## 人物头像改为"出现事件画面"(commit 2ba3478,用户否决视频首帧方案) - **用户意见**:不能用视频首帧当头像(首帧可能无人/非本人)——该人物在众多视频中多次出现,肯定能在他出现的事件里找到画面 - **新方案**:Oracle 新增 GET /api/oracle/person/avatar?label=X(token 鉴权):events.person_list_json 按 `%"label"%` JSON 数组精确匹配,按 id DESC 遍历返回第一个 ev_{id}.jpg 存在的截图(人物出现事件中最新的有截图画面);撤销 event_thumb 的视频首帧兜底(改回 404) - **fam-ui 人物页**:头像改调 avatar 接口(先 requests 探测 200 再渲染 img,label 用 urllib.parse.quote 编码) - 验证:人物A/B avatar 均 200(39-41KB 真实事件画面)、无 token 401、NAS→Oracle 200、UI 200 ## Oracle 磁盘扩容(用户控制台扩盘 + 重启生效) - **背景**:rclone 持续同步视频导致 45G 盘写满(剩 46M,gdrive_videos 31G/75 个视频)。用户说已扩容但服务器 lsblk 一直 46.6G(在线未生效) - **处理**:growpart/resize2fs 均 NOCHANGE(块设备没变);临时清理 /tmp 残留+apt+journal 释放 ~900M;禁用 rclone timer 防写满;按用户要求重启服务器 - **重启后扩容生效**:sda 46.6G→**150G**,Ubuntu cloud-init 开机自动扩展分区+文件系统 → df 146G 可用 90G(39%) - **恢复**:enable --now rclone-sync.timer;同步恢复(视频 75→121 个持续下载中);fam-edge active - 经验:Oracle 在线扩容偶尔不立即生效,重启可触发(Ubuntu 自动 growfs);扩容后无需手动 growpart - 提醒:视频持续增长(121 个已占 56G),150G 约可再装 240 个视频,长期需考虑清理策略(如只保留分析完的摘要+删本地视频,需改 rclone 策略避免重新拉回)或再扩盘 ## 人物模块重构 v3 - 大模型特征值替代 OpenCV(commit ff01d14 + 68337f8 + 677c5bd,用户提供设计文档) - **核心**:prompt 增加 person_appearances(uid+7特征+action),VLM 直接产出结构化特征,跨视频靠特征合并,彻底移除 cv2 - **改动**(12 文件):prompts.py(6原则+schema+规则8条+合并prompt重写)、gemini _normalize 透传、oracle_db(events.person_appearances_json/people.features_json/display_uid + _merge_features + upsert_person/mark_video_processed)、video_processor(validate_video 改 ffprobe 去 cv2、_store_result 聚合 uid 特征落 people、删 thumb/event_thumbs)、person_service(_aggregate_features/_collect_features_text 特征文本合并)、api_gateway 删 3 图接口、NAS db_layer+ddl 加字段、fam-ui 特征卡替代头像+事件人物特征块 - **踩坑 2 个**: 1. max_tokens 4096→16384:3秒密度+特征使 JSON 巨大被截断解析失败 2. **json_parser.validate_schema 白名单丢弃 person_appearances**(在 _normalize 之前执行)→ 补透传 - **验证**:video 46 重分析 → lite 输出 16 事件含完整特征(性别男/中年/中等/短发/蓝色POLO衫/无辨识);events 带特征 6、people.features_json 落库;NAS 同步 6 条+人物A特征卡数据 ✓ - 部署:Oracle 卸载 opencv/numpy(ffprobe 已有);NAS ALTER 加 3 列 - 遗留:NVIDIA 模型链仍不可用(gemini 429 时 lite 兜底);历史视频无特征(前端显示"特征待大模型补充",下段分析自动补)