Files
sentinel-home-ai/fam-edge/src/fam_edge/ai_orchestrator/prompts.py
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

189 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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:
"""构建整视频分析 promptgemini/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 promptfam-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:
"""构建人物合并 promptperson_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}"""