人物图片功能重做: 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>
189 lines
11 KiB
Python
189 lines
11 KiB
Python
"""
|
||
Prompt 模板 - 集中管理,避免 gemini/nvidia 适配器各维护一份导致发散。
|
||
|
||
设计原则:
|
||
1. schema 用真实 JSON 示例展示,不靠文字描述字段名(VLM 对示例比对纯文字更可靠)
|
||
2. timestamp 统一"视频内相对时间 HH:MM:SS",消除绝对/相对歧义
|
||
3. 人物命名: 已知成员用真名,未知用"人物A/B/C"本视频内临时编号,
|
||
并强制 people_mentioned = events 内出现人物去重后的集合(下游合并依赖)
|
||
4. 人物特征: 每个人物在每个 event 里输出 person_appearances,含 uid + 结构化特征
|
||
文本(性别/年龄段/身形/发型/衣着/面部/辨识点);下游靠特征值跨视频绑定同一身份
|
||
5. 输出硬约束: 首字符必须是 {,禁止思考过程/markdown/解释
|
||
6. 边界情况: 无人/空视频/看不清 -> 空 events + summary 说明,不凑数
|
||
"""
|
||
from typing import Optional
|
||
|
||
|
||
def build_video_prompt(known_members: str, event_start_time: str,
|
||
camera_name: str = '') -> str:
|
||
"""构建整视频分析 prompt(gemini/nvidia 共用)。
|
||
|
||
Args:
|
||
known_members: get_known_members_context() 输出,每行 "- 真名(别名/标识:label)" 或 "- label"
|
||
event_start_time: 从文件名解析的视频开始时间(北京时间),仅用于 start_hint
|
||
camera_name: 摄像头名(注入提示,帮助模型理解画面位置语境)
|
||
"""
|
||
camera_hint = f"\n摄像头位置:{camera_name}。" if camera_name else ""
|
||
start_hint = ""
|
||
if event_start_time:
|
||
start_hint = (f"\n视频开始时间(北京时间)约为 {event_start_time},"
|
||
f"但 timestamp 字段仍填视频内相对时间(见下方格式说明)。")
|
||
|
||
members_block = known_members.strip() if known_members and known_members.strip() else "(暂无已知成员,所有人物用「人物A」「人物B」编号)"
|
||
|
||
return f"""你是家庭监控视频分析助手。请观看整段监控录像,提取结构化信息。
|
||
|
||
【输出格式 - 必须严格遵守】
|
||
- 只输出一个合法 JSON 对象,首字符必须是 {{,末字符必须是 }}。
|
||
- 禁止输出 markdown 代码块、思考过程、解释文字、前后缀。
|
||
- 空视频/画面看不清/无人出现时,events 填空数组,global_summary 说明情况。
|
||
|
||
【JSON 结构】
|
||
{{
|
||
"global_summary": "整个时段的客观摘要,简体中文,2-4 句,说明谁在做什么",
|
||
"events": [
|
||
{{
|
||
"timestamp": "HH:MM:SS",
|
||
"description": "该时刻画面的详细描述:人物身份、动作细节、位置移动、交互对象、姿态/手势/朝向、手中物品、周围环境",
|
||
"people": ["人物标识"],
|
||
"person_appearances": [
|
||
{{
|
||
"uid": "人物A",
|
||
"features": {{
|
||
"gender": "男",
|
||
"age_band": "中年",
|
||
"build": "瘦高",
|
||
"hair": "短发黑色",
|
||
"clothing": "红色卫衣+深色长裤",
|
||
"face": "蓄须",
|
||
"distinguishing": "左手戴手表"
|
||
}},
|
||
"action": "走向沙发坐下",
|
||
"bbox": [120, 340, 610, 900]
|
||
}}
|
||
],
|
||
"is_attention_event": false
|
||
}}
|
||
],
|
||
"people_mentioned": ["本视频出现的所有人物标识去重后的集合"]
|
||
}}
|
||
|
||
【字段规则】
|
||
1. timestamp: 视频内相对时间,格式 HH:MM:SS,从视频开头 00:00:00 算起。
|
||
示例:视频开始后 5 分 23 秒 → "00:05:23"。禁止输出绝对日期时间。
|
||
2. events 抽取密度(核心规则):
|
||
a) 有人出现在画面中时,每隔约 3 秒抽取一帧作为一个 event,时间戳对齐到 3 的倍数
|
||
(如 00:00:00、00:00:03、00:00:06、00:00:09……)。
|
||
示例:某人从 00:00:05 走入画面,00:00:30 离开 → 生成 00:00:06、00:00:09、
|
||
00:00:12、……、00:00:27 共约 8 条 event,每条描述该 3 秒窗口内的动作变化。
|
||
b) 同一人物持续在画面中且动作无明显变化时,仍按 3 秒一帧抽取,但 description 须描述
|
||
该 3 秒内的细微变化(姿态、位置、朝向、与谁交谈等),不要简单重复上一条。
|
||
c) 人物进入/离开画面的瞬间必须各成一条 event(时间戳取实际发生时刻,不必对齐 3 秒)。
|
||
d) 动作发生显著变化(如从走动变为坐下、从哭泣变为安静、拿取物品)的转折点必须成一条 event。
|
||
e) 无人出现的时段不要单独成 event。
|
||
3. description 必须尽量详细,每条至少覆盖以下维度(缺失的维度写"无"):
|
||
- 人物身份(用真名或「人物A」编号)+ 当前动作(走动/站立/坐下/蹲下/弯腰/奔跑等)
|
||
- 位置(如"客厅沙发左侧"/"厨房门口"/"走廊中部")+ 移动方向(向门口走/原地不动/朝镜头靠近)
|
||
- 姿态(站姿/蹲姿/坐姿)+ 朝向(面向镜头/背对镜头/侧身)
|
||
- 手部动作(手里拿着杯子/双手插兜/挥手/扶墙/抱孩子等)
|
||
- 交互对象(与谁交谈/喂食/搀扶/推搡/独处)
|
||
- 表情/情绪线索(如可辨认:微笑/皱眉/哭泣/平静)
|
||
- 周围环境与背景物品(电视开着/桌上水杯/地上有玩具等,辅助判断场景)
|
||
4. people: 该时刻出现的人物标识。已知成员用真名,未知人物用「人物A」「人物B」
|
||
本视频内连续编号(同一人保持同一编号)。只填标识本身,不要带括号注释
|
||
(如只写"人物A",不要写"人物A(别名/标识:人物B)")。
|
||
5. person_appearances: 该时刻出现的每个人物的结构化特征 + 动作。必填字段说明:
|
||
- uid: 与 people 数组里的标识完全一致(同一人同一 uid)。
|
||
- features: 客观可见特征,必须包含以下 7 个子字段,看不清的写 "unknown",绝不留空:
|
||
* gender: 性别(男/女/unknown)
|
||
* age_band: 年龄段(幼儿/儿童/少年/青年/中年/老年/unknown)
|
||
* build: 身材(如 瘦高/中等/偏胖/壮实/矮小/unknown)
|
||
* hair: 发型与颜色(如 短发黑色/长发棕色/秃顶/unknown)
|
||
* clothing: 当下衣着(如 红色卫衣+深色长裤/白色T恤+牛仔裤/unknown)
|
||
* face: 面部特征(如 蓄须/戴眼镜/圆脸/unknown)
|
||
* distinguishing: 辨识点(如 左手戴手表/右脸有痣/跛行/无)
|
||
- action: 该人物在本时刻的动作(与 description 里该人物动作一致,单独抽出便于检索)。
|
||
- bbox: 该人物在本帧画面中的包围框 [ymin,xmin,ymax,xmax],坐标为 0-1000 的归一化
|
||
整数(ymin/ymax 相对图片高度,xmin/xmax 相对图片宽度)——用于后续裁剪该人物的
|
||
缩略图/头像,不需要额外调用模型。看不清/无法定位时填 null,不要瞎猜坐标。
|
||
特征硬约束:
|
||
* 客观描述可见特征,不猜测、不推断、不编造(看不清的字段写 unknown,不要靠常识猜性别/年龄)。
|
||
* 同一 uid 在视频多个 event 出现时,features 字段保持一致(衣着变了再如实更新 clothing,
|
||
但 gender/age_band/build/face 必须稳定)。
|
||
6. people_mentioned: 必须等于 events 中所有 people 字段出现过的标识去重后的集合。
|
||
一致性强制:events 里出现的标识必须都在 people_mentioned 里,反之亦然。
|
||
7. is_attention_event: 跌倒、危险动作、异常哭闹、陌生人闯入、身体不适等需关注事件
|
||
填 true,否则 false。关注事件的 event 仍按上述密度规则抽取,但 description 须明确
|
||
说明"异常"点(如"张三在 00:01:15 跌坐在地,身体向右侧倾,双手撑地")。
|
||
8. global_summary: 客观描述,不猜测、不想象、不编造。须包含:谁在画面中、主要活动、
|
||
是否有关注事件、时段大致结构。
|
||
|
||
【已知家庭成员】
|
||
按特征匹配,匹配到用真名,匹配不到用「人物X」临时编号:
|
||
{members_block}{camera_hint}{start_hint}"""
|
||
|
||
|
||
def build_chat_prompt(context: str, members: str, question: str,
|
||
queried_person: str) -> str:
|
||
"""构建智能问答 system prompt(fam-core chat_handler 用)。
|
||
|
||
Args:
|
||
context: sync_events 格式化后的上下文文本(每行一条事件)
|
||
members: known_members_context 文本
|
||
question: 用户原始问题
|
||
queried_person: 用户指定的人物
|
||
"""
|
||
members_block = members.strip() if members and members.strip() else queried_person
|
||
context_block = context.strip() if context and context.strip() else "(今日无该人员的监控记录)"
|
||
|
||
return f"""你是家庭监控助手。仅根据下方监控数据回答用户问题。
|
||
|
||
【监控数据】(按时间顺序,每行一条事件)
|
||
{context_block}
|
||
|
||
【已知家庭成员】
|
||
{members_block}
|
||
|
||
【用户问题】
|
||
{question}
|
||
(用户关心的人物:{queried_person})
|
||
|
||
【回答要求】
|
||
1. 只基于上述监控数据,不编造、不补充数据外信息。
|
||
2. 按时间顺序组织回答,突出关键事件。
|
||
3. 若有关注事件(跌倒、哭闹、陌生人等),重点提示。
|
||
4. 若数据为空或当天未观察到 {queried_person},明确说"今天没有观察到{queried_person}"。
|
||
5. 用自然语言回答,不要输出 JSON 或列表格式。"""
|
||
|
||
|
||
def build_person_merge_prompt(unnamed_lines: str) -> str:
|
||
"""构建人物合并 prompt(person_service._llm_merge 用)。
|
||
|
||
Args:
|
||
unnamed_lines: 待合并人物的特征文本,每行 "- uid:特征=...; 特征=..."
|
||
(features_json 渲染而来的文本,供 LLM 判断是否同一人)
|
||
"""
|
||
return f"""你是家庭监控人物汇总助手。下面是若干人物标识及其结构化特征描述。
|
||
请判断哪些标识指向同一个人,并为每个人输出一个稳定的规范名。
|
||
|
||
【输出格式】
|
||
只输出一个合法 JSON 对象,首字符必须是 {{,禁止 markdown 和解释。
|
||
格式:{{"<原标识>": "<规范名>", ...}}
|
||
|
||
【命名规则】
|
||
1. 同一人的多个标识合并为同一个规范名。
|
||
2. 规范名用「人物A」「人物B」「人物C」这类占位(按出现频率/首次出现排序),
|
||
不要编造真实姓名。
|
||
3. 无法判断是否同一人的,保守不合并(各保留独立规范名)。
|
||
4. 规范名必须在输出中唯一:多个原标识可映射到同一规范名,但同一规范名只指向一个人。
|
||
|
||
【判断依据】
|
||
- 优先比对 gender / age_band / build / face 这四个稳定特征(不会在同一天内变化)。
|
||
- hair / clothing 会变化,仅作辅助;仅靠 clothing 相同不足以合并,仅靠 hair 不同不足以拆分。
|
||
- distinguishing 辨识点(如手表/痣/跛行)是强证据,一致时倾向合并。
|
||
- 任一稳定特征明确冲突(如一个写"男"一个写"女"),绝不合并。
|
||
- 任一关键特征缺失("unknown")时,其他特征一致性需更强才合并;拿不准保守不合并。
|
||
|
||
【待处理人物】
|
||
{unnamed_lines}"""
|