人物图片功能重做: 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>
59 lines
1.8 KiB
Python
59 lines
1.8 KiB
Python
from datetime import datetime
|
||
|
||
from fam_edge.video_processor import (
|
||
_parse_event_start_from_filename,
|
||
_parse_event_ts,
|
||
_clean_person,
|
||
)
|
||
|
||
|
||
def test_parse_filename_pure_digit_format():
|
||
assert _parse_event_start_from_filename(
|
||
"Generic_ONVIF-001-20260820-140416-1787205856321-7.mp4"
|
||
) == "2026-08-20 14:04:16"
|
||
|
||
|
||
def test_parse_filename_underscore_date_format():
|
||
assert _parse_event_start_from_filename("20260821_081500.mp4") == "2026-08-21 08:15:00"
|
||
|
||
|
||
def test_parse_filename_dashed_date_format():
|
||
assert _parse_event_start_from_filename("2026-08-21_081500.mp4") == "2026-08-21 08:15:00"
|
||
|
||
|
||
def test_parse_filename_no_match_returns_empty():
|
||
assert _parse_event_start_from_filename("客厅.mp4") == ""
|
||
|
||
|
||
def test_parse_event_ts_relative_offset():
|
||
start = datetime(2026, 8, 21, 15, 53, 3)
|
||
abs_ts, offset = _parse_event_ts("00:18:22", start)
|
||
assert abs_ts == "2026-08-21 16:11:25"
|
||
assert offset == 18 * 60 + 22
|
||
|
||
|
||
def test_parse_event_ts_relative_offset_no_start():
|
||
abs_ts, offset = _parse_event_ts("00:01:23", None)
|
||
assert abs_ts == "00:01:23"
|
||
assert offset == 83.0
|
||
|
||
|
||
def test_parse_event_ts_over_6_hours_falls_back_to_absolute():
|
||
"""相对时间 > 6 小时视为模型误输出绝对时间,不强行按偏移定位。"""
|
||
start = datetime(2026, 8, 21, 8, 0, 0)
|
||
abs_ts, offset = _parse_event_ts("2026-08-21 09:00:00", start)
|
||
assert abs_ts == "2026-08-21 09:00:00"
|
||
assert offset == 3600.0
|
||
|
||
|
||
def test_clean_person_strips_fullwidth_parens():
|
||
assert _clean_person("人物A(别名/标识:人物B)") == "人物A"
|
||
|
||
|
||
def test_clean_person_strips_ascii_parens():
|
||
assert _clean_person("人物A(alias: 人物B)") == "人物A"
|
||
|
||
|
||
def test_clean_person_no_parens_unchanged():
|
||
assert _clean_person("汤圆") == "汤圆"
|