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:
@@ -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: 该人物的结构化特征 dict(gender/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 覆盖 llm;llm 不覆盖 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' 且非空时覆盖 old(old 为 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')
|
||||
|
||||
Reference in New Issue
Block a user