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 特征卡替代头像、事件时间线人物特征块

This commit is contained in:
ericwyuan
2026-08-21 18:06:21 +08:00
parent 2ba3478291
commit ff01d14c79
12 changed files with 463 additions and 257 deletions

View File

@@ -71,6 +71,7 @@ class OracleDB:
ts TEXT,
description TEXT,
person_list_json TEXT,
person_appearances_json TEXT,
is_attention_event INTEGER DEFAULT 0,
FOREIGN KEY(video_id) REFERENCES videos(id)
);
@@ -81,6 +82,8 @@ class OracleDB:
first_seen TEXT,
appearances INTEGER DEFAULT 0,
source TEXT DEFAULT 'llm',
features_json TEXT,
display_uid TEXT,
updated_at TEXT
);
CREATE TABLE IF NOT EXISTS sync_cursor (
@@ -122,6 +125,18 @@ class OracleDB:
]:
if col not in cols:
c.execute(ddl)
# 兼容旧库events 表补 person_appearances_json新架构 v3 加)
ev_cols = [r[1] for r in c.execute("PRAGMA table_info(events)").fetchall()]
if 'person_appearances_json' not in ev_cols:
c.execute("ALTER TABLE events ADD COLUMN person_appearances_json TEXT")
# 兼容旧库people 表补 features_json / display_uid新架构 v3 加)
pe_cols = [r[1] for r in c.execute("PRAGMA table_info(people)").fetchall()]
for col, ddl in [
('features_json', "ALTER TABLE people ADD COLUMN features_json TEXT"),
('display_uid', "ALTER TABLE people ADD COLUMN display_uid TEXT"),
]:
if col not in pe_cols:
c.execute(ddl)
self._conn.commit()
# ------------------------------------------------------------------
@@ -238,7 +253,11 @@ class OracleDB:
def mark_video_processed(self, video_id: int, summary: str, events: List[dict],
people: List[str], compute_provider: str) -> List[int]:
"""落库视频结果;返回新插入事件的 id 列表(与 events 参数一一对应,供事件截图用)"""
"""落库视频结果;返回新插入事件的 id 列表(与 events 参数一一对应)。
events 内每条可含 person_appearances[{uid, features, action}]
原样存到 events.person_appearances_json供 person_service 聚合特征。
"""
with self._write_lock:
now = _now_iso()
self._conn.execute(
@@ -250,11 +269,13 @@ class OracleDB:
self._conn.execute("DELETE FROM events WHERE video_id=?", (video_id,))
event_ids: List[int] = []
for ev in events:
pa = ev.get('person_appearances')
cur = self._conn.execute(
"INSERT INTO events (video_id, ts, description, person_list_json, "
"is_attention_event) VALUES (?,?,?,?,?)",
"person_appearances_json, is_attention_event) VALUES (?,?,?,?,?,?)",
(video_id, ev.get('timestamp', ''), ev.get('description', ''),
json.dumps(ev.get('people', []), ensure_ascii=False),
json.dumps(pa, ensure_ascii=False) if pa else None,
1 if ev.get('is_attention_event') else 0))
event_ids.append(cur.lastrowid)
self._conn.commit()
@@ -276,27 +297,71 @@ class OracleDB:
# people
# ------------------------------------------------------------------
def upsert_person(self, label: str, canonical_name: str = '', source: str = 'llm',
first_seen: str = ''):
first_seen: str = '', features: dict = None,
display_uid: str = ''):
"""登记/更新人物。
features: 该人物的结构化特征 dictgender/age_band/build/hair/clothing/face/
distinguishing。与已有 features_json 合并(已有非 unknown 字段不被
覆盖,新非 unknown 字段补齐。None 时不更新特征列。
display_uid: 大模型给的人物 UID"人物A"。label 本身就是 UID 时可省略。
"""
now = _now_iso()
row = self._conn.execute("SELECT * FROM people WHERE label=?", (label,)).fetchone()
# 特征合并(在已有 features_json 基础上)
merged_features = self._merge_features(
row['features_json'] if row else None, features) if row else (
self._merge_features(None, features))
if row:
# manual 覆盖 llmllm 不覆盖 manual
if source == 'manual' or row['source'] != 'manual':
self._conn.execute(
"UPDATE people SET canonical_name=?, source=?, appearances=appearances+1, "
"updated_at=? WHERE label=?",
(canonical_name or row['canonical_name'], source, now, label))
"features_json=?, display_uid=?, updated_at=? WHERE label=?",
(canonical_name or row['canonical_name'], source,
merged_features, display_uid or row['display_uid'] or label, now, label))
else:
self._conn.execute(
"UPDATE people SET appearances=appearances+1, updated_at=? WHERE label=?",
(now, label))
"UPDATE people SET appearances=appearances+1, features_json=?, "
"display_uid=?, updated_at=? WHERE label=?",
(merged_features, display_uid or row['display_uid'] or label, now, label))
else:
self._conn.execute(
"INSERT INTO people (label, canonical_name, first_seen, appearances, "
"source, updated_at) VALUES (?,?,?,1,?,?)",
(label, canonical_name, first_seen or now, source, now))
"source, features_json, display_uid, updated_at) "
"VALUES (?,?,?,1,?,?,?,?)",
(label, canonical_name, first_seen or now, source,
merged_features, display_uid or label, now))
self._conn.commit()
@staticmethod
def _merge_features(old_json: Optional[str], new_features: Optional[dict]) -> Optional[str]:
"""合并人物特征:已有非 unknown 字段不被覆盖;新字段在 old 为空/unknown 时补齐。
- old_json 为 None / 空 -> 直接用 new_features
- new_features 为 None / 空 -> 不变
- 字段级new 值非 'unknown' 且非空时覆盖 oldold 为 unknown/空);
new 值为 'unknown' 时保留 old哪怕 old 也是 unknown
"""
if not new_features:
return old_json
try:
old = json.loads(old_json) if old_json else {}
except (ValueError, TypeError):
old = {}
if not isinstance(old, dict):
old = {}
merged = dict(old)
for k, v in new_features.items():
v_str = str(v).strip() if v is not None else ''
if v_str and v_str.lower() != 'unknown':
# 新值是有效特征,覆盖(无论 old 是什么)
merged[k] = v_str
elif k not in merged:
# 新值 unknown 且 old 没该字段,至少把字段占位(写 unknown
merged[k] = v_str or 'unknown'
return json.dumps(merged, ensure_ascii=False)
def set_canonical(self, label: str, canonical_name: str, source: str = 'manual'):
"""手动命名设置规范名label 可视为别名)。"""
self.upsert_person(label, canonical_name, source='manual')