Commit Graph

153 Commits

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 17:40:55 +08:00
ericwyuan
5caeb299a4 refactor(fam-edge): 问答链路抽离到独立 ai-gateway 服务,fam-edge 改为转发客户端
原本嵌在 fam-edge 里的问答模型降级链(NVIDIA 文字模型 -> Gemini 非 flash 文字
模型 -> 本地 Ollama 兜底,含 key 轮换/熔断)跟视频分析业务无关,是通用能力,
抽成独立 ai-gateway 服务(OpenAI 兼容协议),除了 fam-edge 自己,别的项目也能
直接接入。

- 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 预热逻辑(现在由 ai-gateway 自己负责)
- config.yaml 移除 3 个问答专用 model 条目,新增 ai_gateway 客户端配置块

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 14:12:01 +08:00
ericwyuan
9dff19e6ac feat(fam-edge): 赤膊成年人直判爸爸(免费规则) - 用户确认家里只有爸爸会光膀子
用户原话:"赤裸的大人都是爸爸,家里没有其他人会赤裸"——加进闭集识别的免费
规则层(跟汤圆/媳妇同级),命中就不用再调用视觉大模型比对参考图,比 auto_id
更快更准。只对成年人生效(判断顺序上幼儿/儿童年龄档在前面,小孩光膀子玩很
正常,不适用这条规则)。

排查历史数据时用这条规则筛出了一个真实误判:video 1092 的一次赤膊出现被
auto_id(NVIDIA 视觉比对)错误识别成"爷爷",实际是"爸爸"。已用
correct_video_identity 手动纠正(标记 source=manual,受保护不会被以后的自动
识别覆盖回去),描述文字已确认同步更新。

顺带确认了另一批命中同样关键词的历史数据(74 条里有 3 条是"汤圆"光膀子)
是正确的——3 岁小孩光膀子玩正常,是通过幼儿/儿童年龄档规则识别出来的,跟
赤膊无关,不是误判,没有动。

新增 4 个测试:赤膊成年男性直判爸爸且不调用大模型(用会报错的假
person_identifier 验证短路生效)、覆盖各种赤膊近义词表述、赤膊小孩仍然
正确判成汤圆不受影响、没有赤膊关键词时行为不变照常走大模型比对。
2026-08-23 11:00:43 +08:00
ericwyuan
5f7b27bf04 fix(fam-edge): 人物识别结果没同步进事件描述文字
用户反馈:事件卡片上方的人物徽章正确显示"媳妇/爸爸",但描述文字里还是
"人物B双手叉腰站在客厅中央;人物A在远处厨房"这种原始 uid——两处对不上。

根因:rewrite_event_person_names() 只改了 person_list_json /
person_appearances_json[].uid 这些结构化字段,从没碰过 description(事件
描述)和 videos.summary_json(会话摘要)——这两个是大模型写的自然语言,
"人物A"这类 uid 是直接以文字形式嵌在句子里的,结构化字段改名不会带着改。

现在 rewrite_event_person_names() 额外对 description/summary_json 做文本
替换。替换顺序按 rename_map 的 key 长度降序——uid 可能带 "#2"/"#3" 这类
同名冲突后缀,"人物A" 是 "人物A#2" 的前缀,短的先替换会把长的错误地部分
命中,长的先替换才对。

同时跑了两次一次性回填修历史数据:
1. 对所有已有 person_identity_map 映射的 1265 个视频重新跑一遍
   rewrite_event_person_names,把之前只改了徽章、没改描述文字的历史数据
   补上(events 里"纯人物X不带真名"的还剩 258 条,涉及 94 个视频——都是
   性别/年龄特征本来就是 unknown 的画面,闭集识别本来就该留空不瞎猜,
   不是回填没覆盖到,是真的没法从图像信息判断是谁)
2. 找到 9 个从未跑过闭集识别的漏网视频(应该是 person_identifier 功能上线
   前处理的),重新识别后 1 个(video 2194)成功解析出汤圆+爸爸,其余 8 个
   同样是性别未知,正确保持未解析

新增 2 个测试覆盖 description/summary_json 文本替换,以及带后缀 label 的
替换顺序正确性。
2026-08-23 10:50:52 +08:00
ericwyuan
63b28663f1 feat(fam-edge): 启动时预热 Ollama,避免兜底触发时冷启动等 1-2 分钟
排查"Ollama 模型没有常驻内存":systemd 里 OLLAMA_KEEP_ALIVE=-1 其实一直配置
正确,但这个环境变量只保证"加载过之后不因为空闲被换出",不负责启动时主动
预加载。查 model_calls 表,ollama provider 从来没有一条调用记录——因为它是
问答链路最后一级兜底,前面 NVIDIA/Gemini 一直调用成功,从未真正轮到它,
自然也就从未被加载进内存过。

新增 OllamaAdapter.warm_up():送一条 num_predict=1 的最小请求强制模型加载,
超时给够 180s(冷启动实测能到 1-2 分钟)。app.py 启动时在后台线程调用,不
阻塞主服务启动;找不到 ollama 配置或预热失败都只记警告,不影响服务本身。

实测部署验证:重启后 36 秒完成预热,ollama ps 显示 qwen2.5:7b 已加载,
expires_at 显示不过期(keep_alive=-1 生效)——真正需要兜底的那一刻不会再有
冷启动延迟。

新增 test_ollama_adapter.py 4 个用例覆盖预热成功/HTTP失败/异常不上抛/超时
时长。
2026-08-23 09:33:49 +08:00
ericwyuan
bdf561a415 fix(fam-core): 登录密码不再硬编码兜底(fail-closed) + 会话有效期改 2 小时
之前 FAM_AUTH_USER/FAM_AUTH_PASS 没配置时会退回代码里写死的 ericwyuan/
iLoveJava5——这两个值跟 NAS SSH 密码是同一个,且这个登录页已经通过 frp 暴露在
公网上。.env 万一没配置好(这个项目里已经发生过好几次"忘了 source .env"的
情况),公网入口就会用一个和 SSH 密码相同、且写在源码里的已知密码兜底,双重
风险叠一起。

现在改成 fail-closed:FAM_AUTH_USER/FAM_AUTH_PASS 只要有一个没配置,直接拒绝
所有登录(打一条 ERROR 日志提醒去配 .env),不再有任何硬编码默认值。

会话有效期从 7 天改成 2 小时(按用户要求)。

新增 test_auth.py 11 个用例覆盖:未配置时 fail-closed、正确/错误凭据校验、
只配了一半也要拒绝、session 过期判定、白名单路径匹配。

已实测验证:当前 .env 里 FAM_AUTH_USER/PASS 仍是配置好的(还是 ericwyuan/
iLoveJava5,这个值本身没改——只是不再硬编码在代码里,运维如果想换成不同于
SSH 的密码,现在改 .env 就行,不用改代码),登录/拒绝错误密码都验证正常,
cookie Max-Age=7200 确认生效。
2026-08-23 07:56:33 +08:00
ericwyuan
9bba7b7e14 fix(chat): 流式问答中文乱码 + 问答模型链换成专用文字大模型
## 乱码修复
Gemini 流式接口 resp.iter_lines(decode_unicode=True) 没有显式设置
resp.encoding,requests 自己猜编码猜错了(Gemini 的 SSE 响应 Content-Type
不带 charset 参数),导致中文变成典型的 UTF-8 被当 Latin-1 解码的乱码。
fam-core 转发这一跳的 requests.iter_lines 调用有同样的坑,一并修了。
两处响应的 Content-Type 都补上 charset=utf-8,减少下游再犯同样错误的机会。
顺手用 latin-1/utf-8 往返把 NAS chat_history 里已经存了乱码的 3 条历史记录
恢复成正常文字。

## 问答模型链重新设计(跟视频分析完全独立)
用户要求问答不再用 flash/omni,优先用 NVIDIA 免费文字模型里上下文最大的几个,
不行再退 Gemini 非 flash 文字模型,最后本地 Ollama 兜底。

- base_adapter 新增 usage 字段(纯标记,不影响行为)
- qa.py 的 QAOrchestrator 只挑 role='text' 的适配器参与问答(按 config 出现
  顺序决定降级顺序),role='vision' 的视频分析适配器(gemini-flash-latest /
  nvidia omni)不再参与问答,两条链路彻底解耦
- 新增问答专用 NVIDIA 文字模型链(同一适配器内部 model_chain 降级):
  nemotron-3-ultra-550b-a55b(1M上下文/561B) -> nemotron-3-super-120b-a12b
  (1M上下文/124B) -> openai/gpt-oss-120b(131K上下文/117B)。三个都用真实
  API 调用验证过当前账号可访问;同时验证过 llama-3.1-70b-instruct 和
  nemotron-super-49b-v1.5 都收到"08/25/2026 起弃用"通知,排除;
  nemotron-ultra-253b/mistral-large-2/nemotron-4-340b/kimi-k2.6 在目录里
  能看到但实测调用返回 404"账号无权限",排除
- 新增问答专用 Gemini 非 flash 文字模型:gemini-pro-latest -> gemini-2.5-pro
  (1M 上下文,复用视频分析同一批多 key 轮换)
- /api/edge/chat/ask(/stream) 默认 max_tokens 从 1024 提到 3072:新链路
  优先用的都是"推理"模型,回答前会先吐一段思考过程,1024 经常在思考阶段
  就被截断,用户永远看不到真正答案

新增 test_qa.py::test_init_only_keeps_text_role_adapters_in_config_order
验证角色过滤+顺序正确。

本地检查过 Ollama 服务本身:systemd enabled+running(8h 正常运行,
qwen2.5:7b 已加载),并非没启动——用户观察到的现象另有原因。
2026-08-23 07:46:48 +08:00
ericwyuan
22bd4d4ff9 fix(fam-edge): 停用旧 person_service LLM 合并 - 已被闭集识别取代且仍在持续产生错误合并
person_identifier 闭集识别(汤圆/媳妇走性别年龄规则,爷爷/爸爸走视觉大模型比对
参考图)已经稳定跑起来了,people 表里 4 个人的真实姓名行数据干净(汤圆692/
爸爸323/爷爷320/媳妇290)。但发现 person_service 的老 LLM 合并调度还在跑(每
30 分钟一次),持续把不可靠的猜测结果(如"人物E"→"人物T"这类无意义合并)写进
canonical_name,跟新系统打架,是 people 表里 392 条历史垃圾行的根源。

停用 person_service.enabled,改用新系统。順帶清理 Oracle 库里已经产生的 392
条垃圾行:10 条能对应上真实姓名的(如"幼儿"→汤圆 100 次、"人物A"→爷爷 37 次)
appearances 累加合并回真正的规范行,其余 382 条(老 LLM 合并的无意义中间结果,
canonical_name 对不上任何真实姓名)直接删除——不影响 events/videos 里已经落库
的原始分析数据,只是清理 people 表的聚合展示行。
2026-08-23 02:23:00 +08:00
ericwyuan
2c5bf950c5 feat: 事件时间轴缩略帧 + 人物管理头像 + 人物合并硬规则校验
## 新架构:Oracle 集中计算 + NAS 代理展示

### Oracle 端 (fam-edge)
- 新增 frame_service: ffmpeg 视频抽帧 + VLM 人物定位裁剪头像(磁盘缓存)
- 新增 /api/oracle/frame: 按 video_id+ts 抽帧返回 jpeg(带 token)
- 新增 /api/oracle/avatar: 按 label 生成人物头像(VLM 定位人物 + 兜底整帧居中)
- 新增 person_identifier: 人物身份识别模块
- Gemini 适配器支持 flash/flash-lite 双模型切换,429 自动降级
- frame_service VLM 全模型 429 时进入 10 分钟熔断,避免每次请求白打配额
- 兜底头像不落缓存,配额恢复后自动重试 VLM 精确定位

### 人物合并硬规则校验(框架级修复)
- person_service: LLM 合并结果落库前加硬冲突检测
  - 性别冲突 → 绝不合并
  - 年龄档跨未成年/成年 → 绝不合并(防止把爷爷/宝宝并进同一人)
- oracle_db: upsert_person 入口剥离括号后缀(人物A(别名:人物B) → 人物A),消灭垃圾人物行
- 修复 set_canonical 丢弃 source 参数的 bug(旧代码硬编码 'manual' 导致错误合并被永久固化)
- get_events_for_label: 只提取该身份组的特征文本,头像定位更精准

### NAS 端 (fam-core)
- 新增 img_proxy: /api/proxy/frame 和 /api/proxy/avatar 代理 Oracle 图片
- app.py 注册 img_bp 蓝图
- oracle_sync / db_layer / member_manager 同步人物表

### UI 端 (fam-ui)
- 事件时间轴: 每条事件卡片加时间点缩略帧
- 人物管理: 每人卡片加头像(150x150 圆角)
- parse_persons: 剥离括号备注,与 Oracle 归一化一致
- 新增 EventItem 组件、Timeline 页改造
- Chat / ServiceStatus 页相应调整

### 数据库
- scripts/ddl.sql: 同步表结构更新
- Oracle people 表: features_json / display_uid / source 字段完善
2026-08-23 00:13:56 +08:00
ericwyuan
aca1a674b1 [文档] README 补充登录校验(auth 模块/凭据/白名单)与 frp 外网 8000 映射说明 2026-08-22 14:20:40 +08:00
ericwyuan
9c51c65e62 [功能] FAM-Core 登录校验 + frp 外网暴露 8000 - 新增 auth 模块(POST /api/login 校验 FAM_AUTH_USER/PASS 默认 ericwyuan/iLoveJava5, HttpOnly cookie 7天, 页面未登录 302 /login, /api/* 未登录 401; 白名单: /login /health /api/ss/webhook /assets/*); 内置深色登录页(SPA 零改动); frpc.toml 增加 fam-core 代理 外网8000->NAS8000 2026-08-22 14:19:54 +08:00
ericwyuan
1dd9df34c2 [功能] 事件-片段一致性对账 - /api/oracle/activity 加 segment.consistency(事件总数/已结束/已分割/缺口+明细/素材覆盖范围), 服务状态页展示一致性数字 2026-08-22 13:40:42 +08:00
ericwyuan
177c2a83b7 [修复] 进行中事件 duration=0 分割丢失 - 推送过 duration<=0 的事件记入 _zero_dur_ids, 下轮窗口回查已结束(duration>0)补推覆盖 Oracle; 保证 ss_motion_events 最终持有真实 duration, 分割不再跳过 2026-08-22 13:25:20 +08:00
ericwyuan
cbcc5d0325 [功能] 服务状态页新增「视频分割」状态 - Oracle /api/oracle/activity 加 segment(运动片段 done/pending/failed/文件数/最近分割活动), ServiceStatus.vue 加视频分割卡片 2026-08-22 13:18:57 +08:00
ericwyuan
01a8ac2a3f [文档] 拓扑图 FAM-UI 标注同步为 Vue3(清理残留 Streamlit :8501) 2026-08-22 13:15:04 +08:00
ericwyuan
354ff18101 [文档] 代码-文档一致性整理 - README 同步 v3 全量(2.1网络/3模块表/4表结构/5 API含people-clips/6.1历史标注/8部署systemd+Vue3+config/12进度); docs/DEPLOY.md 重写为 v3; 删除过时 start_ui.sh(Streamlit); start_edge.sh 注明 systemd 为准; 补提交 test_motion_notifier 2026-08-22 13:14:38 +08:00
ericwyuan
96644599d3 [修复] pymysql execute 字面 % 冲突 - SQL 中 motion_% 的 % 被当参数占位符导致 /api/ui/videos 500, 改用 LEFT(filename,7)='motion_' 判断前缀 2026-08-22 13:06:52 +08:00
ericwyuan
6a29e88d86 [优化] 时间轴/统计过滤空会话 - 只展示运动片段(motion_)或有事件的视频, 整段素材分割0段的空会话不占时间轴; get_sync_videos/get_sync_stats 加内容过滤 2026-08-22 12:59:21 +08:00
ericwyuan
4cf4fc4bc2 [功能] 人物管理按运动视频重设计 - 人物卡新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳时间轴定位); 新增 GET /api/ui/people/clips 后端接口(按 label/canonical_name 查关联运动片段,含 first_ts/clip_events); Timeline 支持 ?video= 定位 2026-08-22 12:55:30 +08:00
ericwyuan
ffcc292cce [文档] 同步 v3 运动事件驱动架构 - 整段素材分割运动片段再分析(不再整段送云端): README 1.1/2.2/2.2.1/2.3/模块表, PROGRESS 运行状态+变更记录; 记录 systemd 守护部署方式 2026-08-22 12:46:33 +08:00
ericwyuan
8d6cfad6c0 [修复] 运动片段分割 ffmpeg 调用缺可执行文件 - args 首元素补 ffmpeg 路径(此前被当成可执行文件导致全部分割失败) 2026-08-22 12:41:06 +08:00
ericwyuan
a1523b46c8 [重构] 运动事件驱动架构 - 整段素材按 ss_motion_events 分割运动片段再分析(不再整段送云端): oracle_db 加 motion_event_id/camera_id 列 + get_motion_events_in_range/has_unfinished_motion_in_range/get_video_by_motion_event_id; video_processor 素材→分割/片段→分析双分支(ffmpeg -c:v copy -c:a aac 保留音频); video_queue 片段入队; config 加 motion_segment 块 2026-08-22 12:19:17 +08:00
ericwyuan
2d9d96de56 [功能] fam-edge 运动心跳支持 - /api/ss/motion 空 events 心跳刷新, has_motion_in_range_local 按 max_heartbeat_age_sec(900s) fail-open 2026-08-22 12:08:00 +08:00
ericwyuan
7adc32486a [重构] 运动监测恢复轮询主路径(简单稳定) - poll_enabled 默认开启;修复3隐患: 推送失败批次不前进游标(下轮重试不丢事件)、重启优先续用DB游标(停机期间事件补推)、fetch limit 提到1000;Webhook降级为可选补充 2026-08-22 12:00:03 +08:00
ericwyuan
b2b537dde5 [文档] 同步 v3 运动监测架构(Webhook 驱动/去轮询) - README 补充 2.2.1 链路/API 表/模块表,PROGRESS 更新运行状态与变更记录 51-53 2026-08-22 11:11:15 +08:00
ericwyuan
e52d8e9103 [修复] MotionNotifier 补回 camera_ids 属性 - 避免重新开启轮询时 _fetch_events 引用缺失属性崩溃 2026-08-22 10:58:56 +08:00
ericwyuan
ca6d425b19 [重构] 运动监测改为 Webhook 驱动(去轮询) - 关闭 MotionNotifier 轮询,NAS 端合成稳定 event_id、摄像头名映射 camera_id、EVENT_TIME 解析为 epoch,SS Webhook 直接推送甲骨文 2026-08-22 10:53:03 +08:00
ericwyuan
01bbb39750 [新增] NAS 运动监测通知服务 MotionNotifier - 轮询/接收 SS 事件后主动 POST 推送到甲骨文 /api/ss/motion(单向 NAS->Oracle):新增 motion_notifier 子包、/api/ss/webhook 实时转发蓝图、db_layer 运动游标、app.py 接线与 config 块 2026-08-22 10:26:03 +08:00
ericwyuan
38dea6ba2e [重构] 运动预过滤改为本地事件(NAS推送) - 甲骨文不再反向访问NAS:新增 ss_motion_events 表/record_motion_events/has_motion_in_range_local,/api/ss/motion 接收端点,video_processor 改用本地运动事件预过滤,停用 dsm_motion_client 反向查询 2026-08-22 10:25:56 +08:00
ericwyuan
0d8e62607c feat(fam-edge): DSM 运动侦测预过滤 - 空转时段跳过云端视频分析
群晖 Surveillance Station 用 SYNO.SurveillanceStation.EventCenter.Event(未公开
文档的内部接口,参数是下划线风格 camera_ids/start_time/end_time,公开文档里的
cameraIds/fromTime/toTime 是旧版 Event API 的参数名,两者不通用)记录了摄像头
真实的运动侦测事件(event_type=10=运动,start_time+duration 精确到秒),比自己
本地跑 ffmpeg 帧差分更准、不需要额外算力,之前一直在这台 NAS 上验证可行性。

新增 DsmMotionClient:process_video() 分析每段视频前,先用视频的
[event_start, event_start+duration] 时间窗查一次这个接口,窗口内一条运动事件都
没有就跳过云端分析(标记 done,compute_provider=skipped_no_motion),省掉长期
无人时段白白消耗的 Gemini/NVIDIA 配额。

安全设计:查询本身失败/未配置/账号密码没填一律 fail-open(当作"有运动",照常
分析),不会因为这层可选优化漏检真实事件——这是一个纯粹的省配额优化,不能反过来
影响监控系统的可靠性。

新增 12 个单测覆盖:禁用/缺凭证时的 fail-open、登录失败、网络异常、无事件、有
事件、按 camera_id 过滤(响应是按 ds_id 分组不是按 camera_id,容易搞混)、
最短运动时长阈值、session 过期重登录重试、env 变量解析。

账号密码走 .env 的 DSM_ACCOUNT/DSM_PASSWORD,不明文入库。
2026-08-22 09:46:00 +08:00
ericwyuan
f798c31cab fix(fam-edge): 人物 uid 跨视频复用导致特征串人 - 拆分同 uid 下的性别冲突
根因: 大模型给的 人物A/B/C 这类临时 uid 只在单次视频分析内部稳定,不同视频各
自独立编号——同一个字符串在不同视频里完全可能指向不同的真人(实测生产数据里
"人物A" 在 29 个视频里混了男女两个人,"人物B" 混了男/女/儿童三个人)。但
people 表 label 全局 UNIQUE,upsert_person / person_service 的特征聚合都直接
按这个字符串当全局稳定身份用,导致不同真人的特征被硬合并进同一行,"我"这张卡
显示出来的描述其实是我和媳妇两个人的特征混在一起。

两处落地点都加了同一条硬规则(性别是相对稳定信号,冲突大概率是撞了另一个人):
1. oracle_db.upsert_person(): 单视频入库时,新特征性别与已有行冲突就不覆盖合
   并,改分配 uid#2/uid#3 这样的派生 label 单独建行。
2. person_service._aggregate_features(): 每次全量重新聚合时按性别在线聚类,
   同一 uid 下冲突的性别拆成独立分组,不再无脑覆盖成一坨。

拆出来的派生 label 走已有的"未命名 -> LLM 合并 -> 硬规则否决"流程,由现有机制
判断该并入哪个已命名身份。

新增 test_oracle_db.py(5 例)+ test_person_service.py(5 例)覆盖同性别合并 /
性别冲突拆分 / 后缀分配 / unknown 不触发拆分等场景。

生产数据已用新逻辑重新 reconcile 并手动核对 3 个因数据量太大 LLM 没能正确认领
的派生 label(人物A#2/人物B#2#2 -> 媳妇,爷爷#2 -> 爷爷),现在 4 个人物分组
(我/媳妇/爷爷/汤圆)特征都是内部一致的,不再互相串。
2026-08-22 08:45:24 +08:00
ericwyuan
708b6365f4 feat(fam-edge): Gemini key 分配改真随机 + 按项目名标注统计
用户明确要求: 每次调用应该是 1/n 概率的真随机抽取,而不是上一版的顺序轮转
(round-robin 在当前单消费者串行处理场景下已经是数学最优均匀分配,但用户想要
"数组里随机取一个,每次都是四分之一概率"这种更直观的随机语义)。_rotated_keys()
去掉持久的轮转下标,改用 random.randrange 每次调用随机选起点,仍保留"起点 key
全部模型失败就级联到下一个 key"的兜底逻辑不变。

同时新增 key_labels 配置:4 个 key 分别对应用户在 Google Cloud 上开的 4 个项目
(智能摄像头-1/2/3/4),model_calls 统计现在按项目名而不是泛化的 key1/key2
展示,ModelStats 页面的 Key 列/拆分逻辑同步改为按最后一个 "·" 分隔(原来写死
按 "·key" 前缀查找,项目名场景下不适用)。

新增/重写 7 个单测覆盖:随机起点级联顺序、randrange 调用范围正确性、大样本
随机分布均匀性、key_labels 默认值/自定义值/key 被丢弃时的对齐。
2026-08-22 07:37:33 +08:00
ericwyuan
22df28f2d3 feat(fam-ui): 各页面加载状态 - 数据到达前不再是空白/半截页面
新增 Spinner 组件,接入时间轴/AI对话历史/人物管理/统计图表/模型统计/
服务状态 6 个页面。People.vue 特殊处理:命名或合并后触发的刷新不重新
显示整页 Spinner(此时列表已有数据,会闪一下丢失滚动位置),只有首次
加载(列表为空)才转圈。ServiceStatus.vue 同理,手动点刷新只变按钮文
字,不重新蒙一层 Spinner。
2026-08-22 07:19:32 +08:00
ericwyuan
1ad6af3d5d fix(fam-edge): Gemini 多 key 从未真正轮转 - 流量全压在 key1
analyze_video/_generate_text 原来每次都从 api_keys[0] 开始遍历,只有当
key 的全部模型都失败才换下一个 key;由于 flash-lite 兜底基本总能在
key1 下成功,key2/3/4 实际零流量,配置的多 key 配额分散形同虚设。

新增 _rotated_keys():每次调用前记住上次轮转到的起点,按起点滚动排序
返回 key 列表并把起点推进到下一个,多次调用后流量均匀摊到全部 key。
model_calls 统计的 model 字段现在带上 ·key{n} 后缀,可以看出具体是哪个
key 在跑(ModelStats 页面对应拆出 Key 列展示)。

新增 4 个单测覆盖起点归零/逐次推进/回绕/单 key 情形。
2026-08-22 07:19:23 +08:00
ericwyuan
6b149a0dd0 fix(fam-edge): 人物合并加硬规则否决 - LLM 弱模型可能把爷爷/宝宝误并成同一人物
PersonService._merge_labels 原来完全信任 LLM 给出的合并提议(label -> canonical),
但弱模型偶尔会把性别不同或年龄档跨未成年-成年的两个人合并成一个身份(如把"爷爷"
和"宝宝"并到同一个人物)。

新增 _features_conflict 硬冲突检测,落库前校验候选合并双方的 gender/age_band:
性别不同、或年龄档跨 child/adult 边界(幼儿/儿童/少年 vs 青年/中年/老年)一律
否决合并,只把结果当 LLM 的提议,不当最终结论。否决的合并计入 blocked 计数,
连同 updated 一起写入 service_activity 日志,方便观察 LLM 合并的实际可靠性。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 06:53:51 +08:00
ericwyuan
7579782daf refactor(fam-ui): 重构第三阶段 - Streamlit 换成 Vue3+Vite+Tailwind 完全重写
范围变更:原计划是给 Streamlit 界面做视觉美化,用户中途要求换新框架完全重新实现。
最终方案:Vue 3 + Vite + Tailwind CSS v4,本地 npm run build 出静态文件,NAS 不装
Node.js,由 fam-core 的 Flask 直接提供(send_from_directory),原来独立跑在 :8501
的 Streamlit 进程整个退休,前端和 API 合并到 fam-core 的 :8000 一个进程。

fam-core 新增只读 API(ui_api.py):全部包装 db_layer.py 里已有的查询函数,没有
新写查询逻辑(除了下面两处真实缺口)。新增 static_app.py 做 SPA 静态文件服务
(assets 直出 + 非 API 路径回退 index.html 供前端路由接管),app.py 注册顺序上
必须排在其他 /api/* 蓝图之后。

顺带补的两个功能缺口(旧 Streamlit 版本自己绕开 db_layer 写了裸 SQL 才有的功能,
db_layer 本身不支持):
- get_chat_history 补 offset 参数(分页)
- 新增 get_attention_events(统计图表页"关注事件"表格)

fam-ui 完全重写为 Vue 3 项目:7 个页面 1:1 迁移功能(事件时间轴/AI对话/对话历史/
人物管理/统计图表/模型统计/服务状态),深色主题设计系统(Inter+JetBrains Mono
字体、蓝紫渐变强调色、语义色 token)。用本地 npm run dev 代理到真实 NAS 数据做
了完整联调,过程中发现并修了两个真 bug:
- URLSearchParams 把 undefined 转成字符串 "undefined" 传给后端,导致日期筛选失效
- MariaDB SUM() 返回 Decimal,Flask 默认序列化成字符串,前端字符串拼接出乱码数字
  (ui_api.py::_ser 统一转 int/float 修复)

移动端 H5 补了响应式:原来的固定宽度侧边栏 + flex-wrap 导航在手机宽度下会把每个
按钮挤到文字逐字换行;改成 lg 以上桌面侧边栏、lg 以下移动端顶栏 + 横向可滑动导航
胶囊;人物管理页命名表单(输入框+命名按钮+合并下拉)在窄屏下改为纵向堆叠。

已部署 NAS 并验证:curl 确认 index.html/assets/深层路由/API 路由全部 200;
Browser 工具在桌面宽度和手机宽度下把 7 个页面点了一遍(含真实提问一次 AI 对话、
人物头像/事件缩略图加载、命名合并表单),旧 Streamlit 进程已停止。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 06:48:25 +08:00
ericwyuan
e56d362d73 refactor(fam-core): 重构第二阶段 - img_proxy 收尾 + 稳定性小修 + 工程质量
img_proxy 收尾: 提交此前未提交的图片代理蓝图(app.py 注册),改为复用 oracle_sync
已解析好的 base_url/token/timeout,不再独立读一份配置(避免配置改动时两处不同步)。

稳定性/正确性: chat_handler 问答失败时不再把 Oracle 内部 HTTP 状态码等细节透传给
客户端,改为通用错误信息(详细原因仍记服务端日志); 删除 logger.py 里旧任务架构
遗留的死函数 log_task; 更新 config.yaml.example 到当前 v2 架构(原文件还是重构前
的 scheduler/dispatcher/video_server 旧结构,当前代码完全不读这些字段)。

工程质量: 新增 fam-core/tests(9 个单元测试,覆盖 _format_events 上下文格式化和
config_loader 的 ${ENV_VAR} 解析)。

部署时发现并修复一个和这次改动无关的运维问题: NAS .env 文件缺 export 关键字,
plain source 只在当前 shell 生效不会被子进程(gunicorn)继承,导致 Oracle-Sync
token 校验失败;用 set -a/set +a 强制导出重启,非代码改动。

已部署 NAS 并验证:/health、/api/status、/api/proxy/frame、/api/proxy/avatar
全部通过;浏览器实测事件时间轴、人物管理页正常渲染。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 00:50:49 +08:00
ericwyuan
61db82cb9b refactor(fam-edge): 重构第一阶段 - 人物图片零额外调用 + 运行时稳定性 + 工程质量
人物图片功能重做: bbox 随核心视频分析那一次 Gemini 调用一并产出(prompts.py 加
person_appearances.bbox 字段, [ymin,xmin,ymax,xmax] 0-1000 归一化), frame_service
直接用存好的 bbox 裁剪头像/事件缩略图, 删除原来"展示时额外调用 Gemini 定位人物"的
整套逻辑(locate_person_bbox/VLM 校验/熔断), 从架构上消除与核心视频分析共抢配额的
问题; 用真实数据验证裁剪结果正确框住人物本体。

NVIDIA 模型修复: 实测原配置的 3 个模型均不可用(asset_id 引用 500/400, 不支持视频),
改用 nemotron-3-nano-omni 的 base64 内嵌视频方式(唯一实测打通), 加 max_base64_mb
防止对大文件做注定失败的编码。

Gemini 多 Key 轮换: 支持 extra_api_keys 配置多个独立项目的 key, 配额用尽时依次
换 key 重试(每换 key 需重新上传, Files API 按项目隔离)。

稳定性加固: CircuitBreaker HALF_OPEN 清空旧失败计数(修复探测一失败就重新 OPEN 的
bug); chat() 统一接入熔断器(原来只有视频分析路径检查); NVIDIA 适配器改用共享
json_parser(原来自己重复实现且不做 schema 校验); Gemini Files API 上传超时也尝试
清理远程孤儿文件; video_processor/video_queue 里直接操作 OracleDB._conn 的裸 SQL
改走新增的 set_event_start_time/mark_video_invalid/reset_video_to_pending 方法;
/health 加入队列线程存活状态; 密钥改用 ${ENV_VAR} 引用(.env 已支持自动加载),
不再明文写入 config.yaml。

工程质量: 新增 fam-edge/tests(32 个单元测试, 覆盖熔断器状态机/JSON 解析容错/
时间戳解析/bbox 坐标换算/多 key 解析), 新增 scripts/smoke_test.py(发版前接口
稳定性检查); 清理死代码(OllamaAdapter.analyze_frames、get_sync_delta 死分支、
未使用的 vision_timeout/max_concurrent_tasks 配置项); 修正 get_events_for_label
排序(改最近优先 + 过滤畸形历史时间戳)。

已部署 Oracle 并跑通 smoke test 全部 6 项检查。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 00:22:57 +08:00
ericwyuan
677c5bdcc7 fix(fam-edge): validate_schema 透传 person_appearances - events 清洗白名单原只留 timestamp/description/people/is_attention,丢弃了特征字段导致特征链路断 2026-08-21 18:30:06 +08:00
ericwyuan
68337f81c4 fix(fam-edge): 视频分析 max_tokens 4096→16384 - 3秒密度+person_appearances 特征使输出 JSON 巨大被截断导致解析失败 2026-08-21 18:26:51 +08:00
ericwyuan
ff01d14c79 feat(v3): 人物模块重构 - 大模型结构化特征值替代 OpenCV 帧定位。prompt 增加 person_appearances(uid+7特征+action)并重写合并 prompt(稳定特征优先比对);gemini 透传特征字段;oracle_db events/people 加 person_appearances_json/features_json/display_uid + _merge_features;video_processor 去 cv2 改 ffprobe 校验、_store_result 聚合 uid 特征落 people、删缩略图/事件截图;person_service 聚合特征后基于特征文本 LLM 合并;api_gateway 删 3 个图接口;NAS 镜像+DDL 加新字段;fam-ui 特征卡替代头像、事件时间线人物特征块 2026-08-21 18:06:21 +08:00
ericwyuan
2ba3478291 fix(头像): 人物头像改为'出现事件画面'而非视频首帧 - 新增 /api/oracle/person/avatar(在该人物所有出现事件中返回第一个有截图的事件画面,JSON 精确匹配);撤销 event_thumb 的视频首帧兜底(首帧可能无人/非本人);fam-ui 人物页改调 avatar 接口 2026-08-21 16:13:05 +08:00
ericwyuan
27bfd6abbe fix(api): 事件截图接口兜底 - ev_{id}.jpg 缺失时返回所属视频首帧缩略图(thumbs/{video_id}.jpg),避免旧事件/未生成截图导致前端头像空白 2026-08-21 16:09:55 +08:00
ericwyuan
02fc80b3b2 fix(fam-edge): 模型调用 started_at 改北京时间(UTC+8) - 原 datetime.now() 取 Oracle UTC 导致前端模型统计时间差 8 小时 2026-08-21 16:05:17 +08:00
ericwyuan
fec9a3e586 fix(fam-edge): video_queue 补 Dict 类型导入(status() 注解导致 worker 启动 NameError) 2026-08-21 15:53:58 +08:00
ericwyuan
0d0a7f6ef8 feat(监控): 实时服务状态界面 - Oracle 新增 service_activity 活动表(只保留7天,写入时清理)与 /api/oracle/activity 接口(队列实时状态/当前处理视频/模型调用/rclone/人物合并/最近50条活动);VideoQueue 打点+当前处理跟踪+status();PersonService 打点;rclone_sync.sh 同步结果写活动表;fam-ui 新增'🖥 服务状态'页(状态卡+活动时间流,直连 Oracle 实时拉取) 2026-08-21 15:52:36 +08:00
ericwyuan
da7631976d fix(ui): 日期维度改用视频录制时间 - 前端事件时间轴/统计/关注事件与后端 db_layer 统一按 event_start_time(文件名解析) 分组排序,空值回退 processed_at;修复全量重分析导致处理时间(8/21)把 8/15-8/20 视频全堆同一天、与文件名对不上的问题 2026-08-21 15:00:41 +08:00
ericwyuan
0d73c23bd1 fix(fam-edge): 人物标识清洗 - _clean_person 去括号注释(防模型输出'人物A(别名/标识:人物B)'污染人物表);prompt 明确 people 只填标识本身;events.people 与 people_mentioned 均清洗 2026-08-21 14:47:47 +08:00
ericwyuan
a72b286bd7 refactor(prompt): Prompt 集中化 - 新增 fam-edge ai_orchestrator/prompts.py(视频分析/智能问答/人物合并三模板共享,消除 gemini/nvidia 两份发散);gemini/nvidia _build_video_prompt 改调共享函数并注入 camera_name;person_service 改调 build_person_merge_prompt;fam-core chat_handler 独立 prompts.py(跨模块风格一致)替代内联 CHAT_SYSTEM_PROMPT;输出硬约束(首字符{/禁markdown)、3 秒密度抽取、people_mentioned 一致性强制、描述 7 维度、人物合并唯一性约束 2026-08-21 14:42:27 +08:00
ericwyuan
4e85a98944 fix(锁与查询组): #12 熔断器状态转换加锁;#10 问答人名匹配改 JSON_CONTAINS 精确匹配(防 LIKE 子串/特殊字符误匹配);#4 OracleDB 复合写加锁;#6 每消费者独立 VideoProcessor(消除 adapter.timeout 共享竞争);#15 done 视频文件被覆盖(mtime>processed_at)自动重置重分析 2026-08-21 14:24:12 +08:00