Commit Graph

53 Commits

Author SHA1 Message Date
ericwyuan
28397e9e6a feat(timeline): 事件时间轴支持删除视频会话
三端联动实现(fam-ui -> fam-core -> fam-edge),沿用项目里"NAS 转发写请求
到 Oracle"的既有模式(照抄 identity_correct 那套骨架):

- fam-edge:oracle_db.py 新增 delete_video()(删 events+videos 行 + 磁盘上
  的运动片段文件;不清理 ss_motion_events 源事件,因为素材一旦处理完就不会
  被生产者重新捡起,删片段不会触发重新分割);api_gateway.py 新增
  POST /api/oracle/video/delete
- fam-core:新增 db_layer.delete_sync_video()(第一个"NAS 直接写自己镜像表"
  的函数——增量同步只做 upsert 感知不到删除,不能像纠错那样靠 trigger_now()
  拉增量顺带清理)+ oracle_sync.push_video_delete() + ui_api.py 新增
  DELETE /api/ui/videos/<id>(先回推 Oracle,成功后才清本地镜像,避免数据
  不一致)
- fam-ui:Timeline.vue 详情卡片加删除按钮(原生 confirm() 二次确认,项目里
  之前没有确认弹窗组件先例);api.js 新增 deleteVideo()

新增 6 个 delete_video 单元测试;已在 Oracle/NAS 用自造测试数据完整跑通端
到端链路(转发成功、两端记录清理、磁盘文件删除、幂等 404),未触碰任何真
实监控数据。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 04:25:43 +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
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
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
177c2a83b7 [修复] 进行中事件 duration=0 分割丢失 - 推送过 duration<=0 的事件记入 _zero_dur_ids, 下轮窗口回查已结束(duration>0)补推覆盖 Oracle; 保证 ss_motion_events 最终持有真实 duration, 分割不再跳过 2026-08-22 13:25:20 +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
7adc32486a [重构] 运动监测恢复轮询主路径(简单稳定) - poll_enabled 默认开启;修复3隐患: 推送失败批次不前进游标(下轮重试不丢事件)、重启优先续用DB游标(停机期间事件补推)、fetch limit 提到1000;Webhook降级为可选补充 2026-08-22 12:00:03 +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
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
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
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
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
ericwyuan
034dca92b2 fix(优先组): #18 密钥明文直配 config(弃 .env 依赖);#5 appearances 改 distinct 视频数覆盖校准(防 reconcile 累加膨胀);#9 oracle_sync 拉取加锁防 trigger_now 与后台并发双拉;#11 LLM 合并命名解析 target 已有 canonical(统一显示名) 2026-08-21 14:22:28 +08:00
ericwyuan
3e94c2a8b0 feat(nas): 模型调用统计镜像与界面 - sync_model_calls 表+拉取+聚合统计;fam-ui 新增'🤖 模型统计'页(按模型成功/失败/成功率/平均耗时 + 最近调用明细含失败原因) 2026-08-21 12:23:32 +08:00
ericwyuan
dbc5193659 fix(nas): sync_cursor 查询 key/value 列加反引号(MariaDB 保留字导致 /api/status 500) 2026-08-21 11:23:25 +08:00
ericwyuan
44026b65ea [阶段3] FAM-Core 瘦身为管理后台+Oracle-Sync 同步引擎,FAM-UI 改读同步镜像 - 删除 scheduler/dispatcher/poller/event_receiver/video_server,新增 oracle_sync 每30分钟拉增量写 sync_* 镜像表;member_manager/chat_handler 改走同步数据;UI 移除帧图改为事件时间线;DDL 新增 sync_videos/events/people/cursor 2026-08-21 10:38:26 +08:00
ericwyuan
43b3178a9d feat(人物管理/红框标记): 关键帧人脸标记 + 人物命名重命名回溯 + 人物管理页改版
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 配套适配
2026-08-21 00:53:30 +08:00
ericwyuan
80cc3249e5 feat: backfill_frames 支持压缩缓存回退 — 补齐全部历史事件帧图
原始视频被 Surveillance Station 保留策略删除后,回退到
/tmp/fam_compressed/task_{id}/ 的 480p 压缩副本抽帧。实测
event 18-27(08-14 晚间 10 事件)补 78 帧全部成功,至此
所有历史事件帧图齐全,UI 不再出现暂无帧图。
2026-08-20 22:42:08 +08:00
ericwyuan
8a333d752d feat: 历史事件关键帧补抽工具 backfill_frames.py
Edge 帧图注入上线前的旧事件无帧图,事件列表第一屏全是占位符。
按 frame_timestamp - event_start_time 偏移从原始视频抽帧补齐,
幂等可重跑。NAS ffmpeg41 无 image2 muxer,用 -f singlejpeg 输出。
实测补 11 事件 65 帧全部成功(2026-08-14 的 10 个事件视频已被
清理,无法补)。
2026-08-20 22:07:36 +08:00
ericwyuan
c5c8496409 fix: 直接上传multipart超时 — connect timeout 10s→120s
requests/urllib3 发送 multipart body 期间 socket timeout 取的是
connect timeout 值而非 read timeout,跨境 1.4MB/s 下 17MB 视频需
~117s,原 10s 上限导致 100% 超时重试耗尽标记 FAILED。实测 task
50/51 修复后直接上传成功。
2026-08-20 20:43:50 +08:00
ericwyuan
0d5173f442 feat: UI 时间轴重构 + 关键帧图片全链路持久化
界面重做(深色监控面板主题):
- 事件时间轴页: 左侧事件列表卡片 + 右侧关键帧时间轴
  (时间点 + 视频帧 + 人物/动作/衣着摘要 + 关注标记)
- 全局 CSS: 深色主题、卡片化按钮、统计卡、对话气泡、成员卡片
- 侧边栏任务队列状态徽章; 历史帧无图时优雅降级占位

关键帧持久化链路:
- Edge orchestrator: process_push_task 成功后把 keyframes
  base64 注入 frame_details[i].frame_image (位置对齐视觉输入帧)
- NAS event_receiver: 落库前 base64 解码写盘到
  fam-ui/static/frames/event_{id}/frame_{idx}.jpg
- payload 增量 ~300KB/事件 (6帧 jpeg q80), 队列/拉取均无压力

配置: fam-core/fam-ui 新增 storage.frame_image_dir
2026-08-20 19:50:36 +08:00
ericwyuan
1b82db1493 fix: NAS压缩输出显式指定 -f mp4 — .tmp 扩展名导致 ffmpeg41 无法推断 muxer 2026-08-20 18:28:14 +08:00
ericwyuan
1994de22e0 fix: 压缩缓存原子性 — tmp+rename 杜绝半成品缓存被复用
根因: 服务被 kill 时 dispatcher 死亡但其 ffmpeg 子进程成为孤儿
(父进程转 init)继续写压缩产物。旧代码直接写最终路径,半成品文件
size>0 且 mtime 较新,缓存复用判断(size>0 && mtime>=src)会误判
为有效 → 重试任务上传损坏视频(此前 task 300 损坏视频之谜的根源)。
多次重启还会叠加多个孤儿 ffmpeg 争抢 ARM CPU。

修复:
1. 压缩输出写 {out}.{pid}.tmp(带 PID 防多进程冲突),
   成功后 os.replace 原子 rename — 缓存目录只可能出现完整产物
2. _cleanup_compress_cache 顺带清理超过 1h 的 .tmp 残留
3. TimeoutExpired/失败路径 _safe_remove(tmp)

运维: 已清理孤儿 ffmpeg×2(task 43/44)+缓存目录,重置对应任务。
2026-08-20 18:11:24 +08:00
ericwyuan
80b2fcc40c fix: 视频时间语义修正 — 绝对时间戳偏移换算 + mtime 起点修正 + 直传超时
三处时间语义 bug(时区统一排查的延伸发现):

1. NVIDIA 集锦偏移语义: frame_timestamps 是绝对时间
   'YYYY-MM-DD HH:MM:SS'(当日秒 77474s)被直接当视频内偏移用,
   seek 超出 30 分钟视频 → 0 帧 → 集锦恒为空 → 视频模式永远降级。
   修复: analyze_video 增加 event_start_time 参数,偏移 =
   帧时间 - 视频开始时间(跨午夜 +86400);orchestrator 两处
   调用点透传。另加 ffprobe 时长过滤跳过超界片段(日志明示),
   fps=15 统一 CFR 输出。

2. NAS dispatcher event_start_time: 原用文件 mtime(=录制结束
   时刻),真实开始时间应为 mtime - 视频时长,事件时间整体偏移
   一个视频周期(~30min)。新增 _probe_duration 用 ffmpeg 解析
   Duration(NAS 无独立 ffprobe)。

3. 直传超时: 压缩后 ~19MB 略低于 20MB 分块阈值走直传,
   timeout(10,120) 在 1Mbps 下传 19MB 需 ~152s 必超时(task 42
   三连败 FAILED)。加大到 (10, 300)。

测试数据陷阱记录: 两轮 0KB/9.47s 集锦异常均为测试时间戳超出
视频时长所致(1801s>1800s),单输入/多输入 concat 行为本身正常。
2026-08-20 17:58:16 +08:00
ericwyuan
203b076783 feat: NAS端FFmpeg预压缩 — 根治360MB视频跨境上传超时
根因诊断(网络问题,非代码逻辑):
- NAS(中国移动) → Oracle(美国Phoenix) 跨境上行带宽实测仅 0.5-0.9MB/s 且波动大
- 下载方向 3.0MB/s(非对称,典型国际出口拥塞)
- Tailscale P2P: ping 可通(224ms)但持续数据流被阻断(0 B/s, 127s超时)
- 360MB 原始视频上传需 10-20min,任何大分块都会超时

NAS dispatcher:
- 新增 _compress_video: 480p/CRF28/veryfast,静态监控场景实测 ~36x 压缩比
  (360MB → ~12MB,上传时间 20min → ~30s)
- ffmpeg 自动探测: CodecPack ffmpeg41(带libx264) > /usr/local/bin > PATH
  (Synology 系统 ffmpeg 被裁剪,无 h264 编解码)
- scale 两级滤镜: force_original_aspect_ratio=decrease + trunc(iw/2)*2
  (h264 要求偶数尺寸,853x480 会报错)
- 压缩缓存 /tmp/fam_compressed/task_{id}/,源文件未变则重试复用,TTL 24h
- 压缩完成后重置 PROCESSING 状态(重置 stale 回收计时基准)
- 压缩失败回退原始文件分块上传
- _query_uploaded_chunks 失败时记录日志(原静默失败导致全量重传无感知)

Edge api_gateway:
- fix: total_chunks 变更清理旧分块后同步重写 meta.json
  (原 bug: meta.json 不更新导致每块上传都触发清理,删除同批新分块死循环)

config:
- stale_timeout 600→1800(覆盖压缩+上传+Edge队列积压总时长)
- 新增 ffmpeg_path / compress_timeout 配置项
2026-08-20 14:59:50 +08:00
ericwyuan
5915cf4be3 fix: 分块大小减至5MB + Edge端chunk_size变更自动清理 + 断点续传防护
NAS dispatcher:
- CHUNK_SIZE 10MB→5MB(~1Mbps上行带宽下可靠传输)
- chunk上传timeout (30,120)→(60,180)(增加连接和读取余量)
- _query_uploaded_chunks 返回 (set, edge_total) 元组
- expected_total != edge_total 时跳过断点续传(防止chunk_size变更导致文件损坏)

Edge api_gateway:
- upload_chunk 检测 total_chunks 变更,自动清理旧分块
- 防止不同chunk_size的旧分块与新分块混合导致assemble后文件损坏
2026-08-20 14:18:29 +08:00
ericwyuan
d6054de060 fix: 分块大小减至10MB + 上传失败确认机制 + 重试前置检查
- CHUNK_SIZE 20MB→10MB,CHUNK_THRESHOLD 50MB→20MB,降低单次超时概率
- 新增 _query_uploaded_chunks 独立方法,connect timeout 10s→30s
- 分块上传失败后查询 Edge 确认是否实际收到,避免响应丢失导致不必要重试
- 补全 uploaded_set.add(idx) 防止重复上传已成功分块
- _poll_once 中前置 retry_count 检查,超限任务直接标记 FAILED
2026-08-20 13:55:59 +08:00
ericwyuan
881ea3f470 fix: dispatcher 串行上传 + 看门狗自动重启 + 线程状态真实检测
- Dispatcher limit=10→1: 一次只传一个视频,传完再传下一个
- 退避缩短: min(30*(n+1),300)s → 失败后更快重试(原 min(60*(n+1)*2,600)s)
- /api/status: 用 thread.is_alive() 替代 _running 布尔标志
- 三组件(scheduler/dispatcher/poller)添加 is_alive()+check_and_restart()
- app.py 新增 watchdog 线程: 每 60s 检测线程死亡并自动重启
2026-08-20 13:37:07 +08:00
ericwyuan
b6c13a9047 feat: 分块断点续传上传 — 20MB/块 + 分块级重试 + 断点查询
Edge 端新增 3 个端点:
- POST /api/edge/video/chunk: 接收单块,保存到 task_{id}/chunk_{index:04d}
- GET /api/edge/video/chunks: 查询已上传分块(断点续传)
- POST /api/edge/video/assemble: 合并全部分块入队

NAS Dispatcher 重写:
- 大文件(>50MB)自动分块上传(20MB/块)
- 每块最多重试 3 次(分块级重试,非整文件级)
- 上传前查询已上传分块,跳过已有的(断点续传)
- 全部上传后调 /assemble 合并入队
- 小文件(<=50MB)走直接上传路径
- max_retries 3→5(文件级重试次数)
- scheduler 切回生产目录

解决: 360MB 视频跨公网单次上传超时/断连问题
2026-08-20 12:27:30 +08:00
ericwyuan
4f0f19bccd fix: dispatcher/poller 超时改为分离模式,stale_timeout 缩短到 10 分钟
- dispatcher 上传 timeout 从 300s 改为 (10s connect, 60s read)
  避免大文件上传无限阻塞 dispatcher 线程
- poller 拉取 timeout 从 30s 改为 (10s connect, 15s read)
- stale_timeout 从 3600s 缩短到 600s(10 分钟)
  更快回收卡死的 PROCESSING 任务
2026-08-20 12:12:19 +08:00
ericwyuan
02da23ef42 feat: 异步任务队列架构 - SQLite队列 + 速率限制 + NAS Poller
Edge端:
- 新增 SQLite 异步任务队列 (queue_manager + consumer)
- 新增 TokenBucket 速率限制器 (Gemini 1000 RPM, NVIDIA 40 RPM, burst 2x)
- 新增 /api/edge/video/enqueue + /api/edge/results 端点
- 消费者线程从队列消费任务,按速率限制调用AI模型
- orchestrator 集成 rate_limiter,Gemini优先→NVIDIA兜底

NAS端:
- Dispatcher 重构为 enqueue 模式(上传后立即返回,不等结果)
- 新增 Poller 线程(定期从Edge拉取结果写 MariaDB)
- app.py 启动 Poller,config.yaml 新增 poller 配置
- db_layer 更新 valid_stages 添加 'process'
2026-08-20 12:07:09 +08:00
ericwyuan
d75c745b95 fix: dispatcher crash on invalid failure_stage + per-task error isolation
Two fixes:
1. db_layer.update_task_status: map invalid failure_stage values (e.g.
   'process' from Edge) to 'callback' before DB write, preventing
   MariaDB ENUM DataError (1265 "Data truncated")
2. dispatcher._poll_once: wrap each task dispatch in try/except so one
   task's failure doesn't skip remaining tasks in the batch

Root cause: Edge returns failure_stage='process' but DB ENUM only allows
  download/extract/vlm_visual/vlm_fusion/callback. The DataError crashed
  _poll_once(), causing all subsequent PENDING tasks to be skipped.
2026-08-20 11:09:10 +08:00
ericwyuan
babf5b09a9 [架构重构] 移除本地Ollama融合,云端直出JSON直存DB,Q&A三模型降级
1. 视频摘要链路:云端VLM直出结构化JSON → Edge format_cloud_result格式化校验 → 直存NAS DB(移除run_text_fusion本地融合)
2. 智能问答链路:Gemini→NVIDIA→Ollama降级,新增chat()纯文本问答方法
3. 适配器重构:base/gemini/nvidia/ollama adapter新增chat();gemini多图单请求结构化JSON;nvidia逐帧调用聚合
4. 端点变更:/api/edge/chat → /api/edge/chat/ask,调orchestrator.run_qa()
5. chat_handler改经Edge Q&A编排,不再直连Ollama
6. 配置更新:ollama_url → qa_url,Ollama role注释改为Q&A兜底
7. README同步更新架构描述、拓扑图、时序图、模块表
2026-08-20 10:21:09 +08:00
ericwyuan
486eee4feb fix(db): frame_timestamp ISO8601 归一化 + perf(edge): ollama num_predict 1024→512
- db_layer._dt_or_none 支持 ISO 8601(带 T/Z/时区偏移) 归一化为 MariaDB DATETIME 标准格式, 修复 1292 Incorrect datetime value
- edge config ollama num_predict 512: qwen2.5:7b 在 ARM 上 1024 token 融合需 200s, 降到 512 加速且输出足够
2026-08-20 09:26:42 +08:00
ericwyuan
6a9d1626fb fix(event-time): event_end_time/frame_timestamp NOT NULL 列兜底
- Edge push 模式: event_end_time 未提供时用 start+视频时长推算
- fam-core db_layer: insert_event 空值兜底(end→start→NOW)
- insert_event_detail: frame_timestamp 空值兜底 NOW
解决 1048 Column cannot be null 落库失败
2026-08-20 02:20:01 +08:00
ericwyuan
c635dd69f1 fix(db): 空字符串datetime归一化为NULL,兼容MariaDB严格模式
monitor_events.event_end_time / event_details.frame_timestamp 收到空串时
插入 NULL 而非 '',避免 1292 Incorrect datetime value 报错
2026-08-20 02:10:37 +08:00
ericwyuan
e7c1641b72 fix(dispatcher): resp.json(silent=) 是 Flask API,requests 不支持会抛 TypeError
Edge 返回结果解析时 TypeError 逃逸到轮询循环,任务卡死 PROCESSING
2026-08-20 01:59:10 +08:00
ericwyuan
3a195d69b9 fix(dispatcher): 僵尸PROCESSING任务回收 + fam-core文件日志
- db_layer 新增 reclaim_stale_processing: PROCESSING 超过 push_timeout+120s 重置 PENDING
- dispatcher 轮询前先回收僵尸任务(进程重启/Edge重启导致 in-flight 请求丢失的场景)
- logger 增加 fam-core/logs/fam-core.log 文件输出(daemon 模式 stdout 不可见)
2026-08-20 01:42:59 +08:00
ericwyuan
727642d38b feat: adaptive keyframe count based on video duration
- Replace fixed 5-8 keyframe limit with duration-based adaptive sizing
- Candidate frames: clamp(duration_min × 2, 30, 120)
- Keyframe cap: clamp(duration / 150s, 8, 30)
- 30min video → 12 keyframes (was 8), 60min → 24, 120min → 30
- Short videos (<12min) still get floor of 8 keyframes
- Add Ollama keep-alive config doc to PROGRESS.md (OLLAMA_KEEP_ALIVE=-1)
- Update config.yaml and config.yaml.example with new video params
2026-08-20 01:29:14 +08:00
ericwyuan
40944428d1 feat: video analysis switched to push mode (upload whole video, sync response)
Rationale: Oracle cannot reach NAS (Tailscale userspace mode on NAS, no TUN),
the old pull+webhook design requires Edge to download video from NAS and
callback to NAS - both blocked. New design is one-way NAS -> Oracle:

- FAM-Edge: new POST /api/edge/video/push endpoint accepts multipart video
  upload, reuses existing OpenCV scene-change keyframe selection, analyzes
  synchronously and returns the result payload directly in the HTTP response
  (no webhook callback). Old /api/edge/video/analyze kept for compatibility.
- FAM-Edge: VideoPreprocessor.save_upload() saves the uploaded file
- FAM-Edge: AIOrchestrator.process_push_task() runs the full pipeline
  (health check -> extract -> select -> compress -> VLM -> fusion) and
  returns callback-style payload dict
- FAM-Core: Dispatcher rewritten to push mode - reads local video file,
  uploads with task metadata (camera_name, event_start_time from file mtime,
  known_members_context), applies the result to DB via shared
  event_receiver.apply_success_event()
- FAM-Core: event_receiver success logic extracted into reusable
  apply_success_event() (used by both webhook route and dispatcher)
- config: edge_url -> /api/edge/video/push, push_timeout 1800s, gunicorn
  Edge timeout raised to 1800s for long synchronous analysis
2026-08-20 01:03:48 +08:00
ericwyuan
94a8805045 feat: FAM-Edge chat proxy + PyMySQL migration for FAM-UI + config update
- FAM-Edge: add /api/edge/chat proxy endpoint forwarding to local Ollama
  (Ollama port 11434 not exposed externally, FAM-Edge acts as reverse proxy)
- FAM-Core config: edge_url and ollama_url switched from Tailscale IP to
  Oracle public IP (Tailscale firewall blocking between NAS and Oracle)
- FAM-UI: migrate mysql.connector to PyMySQL (same as FAM-Core)
- FAM-UI: cursor(dictionary=True) replaced with cursorclass=DictCursor
- End-to-end chat verified: FAM-Core -> FAM-Edge proxy -> Ollama -> response
  Answer: 今天没有观察到张三 (no events in DB yet, expected)
2026-08-19 23:59:28 +08:00
ericwyuan
c8cd2f2665 fix: MariaDB JSON path compat for name_member + FAM-Core API verified
- name_member(): replace MySQL-only JSON_REPLACE/JSON_CONTAINS with wildcard
  JSON paths (unsupported in MariaDB 10.11) with Python-side JSON parsing
- FAM-Core venv rebuilt with Python 3.10.20 on NAS
- All API endpoints tested: health, status, member list, unnamed, naming
- POST /api/member/name: 人物A successfully named to 张三
- PROGRESS.md updated with full test results and task completion
2026-08-19 23:48:35 +08:00