feat(人物管理): 头像优先选有人脸帧 + 人物合并到操作
1. person_photo 读 event 的 meta.json(face_count),优先挑人脸数最多的帧做头像,无 meta 时按时间序第一张兜底 2. 人物卡片新增「合并到」下拉框+按钮,调 /api/member/merge(source=当前人物,target=所选人物),展示替换明细数 3. 至此设计文档 8 项需求全部落地(Edge 画框/mark_frames 端点/meta.json/backfill/merge_member/merge 路由/UI 头像+合并)
This commit is contained in:
@@ -810,7 +810,31 @@ elif page == "👤 人物管理":
|
|||||||
f'<span style="color:#64748b;">(与事件时间轴"出现人物"统计一致)</span></div>',
|
f'<span style="color:#64748b;">(与事件时间轴"出现人物"统计一致)</span></div>',
|
||||||
unsafe_allow_html=True)
|
unsafe_allow_html=True)
|
||||||
|
|
||||||
|
_meta_cache = {}
|
||||||
|
|
||||||
|
def _load_meta(event_id):
|
||||||
|
"""读 event 的 meta.json(frame_index -> face_count),缺失返回空 dict"""
|
||||||
|
if event_id not in _meta_cache:
|
||||||
|
mpath = os.path.join(FRAME_DIR, f'event_{event_id}', 'meta.json')
|
||||||
|
try:
|
||||||
|
with open(mpath) as f:
|
||||||
|
_meta_cache[event_id] = json.load(f) or {}
|
||||||
|
except (OSError, ValueError):
|
||||||
|
_meta_cache[event_id] = {}
|
||||||
|
return _meta_cache[event_id]
|
||||||
|
|
||||||
def person_photo(frames):
|
def person_photo(frames):
|
||||||
|
"""头像优先挑有人脸的帧(face_count 最大),其次按时间序第一张"""
|
||||||
|
best, best_faces = None, -1
|
||||||
|
for eid, fidx, _ in frames:
|
||||||
|
meta = _load_meta(eid)
|
||||||
|
faces = int(meta.get(str(fidx), 0) or 0)
|
||||||
|
if faces > best_faces:
|
||||||
|
b64 = load_frame_b64(eid, fidx)
|
||||||
|
if b64:
|
||||||
|
best, best_faces = b64, faces
|
||||||
|
if best:
|
||||||
|
return best
|
||||||
for eid, fidx, _ in frames:
|
for eid, fidx, _ in frames:
|
||||||
b64 = load_frame_b64(eid, fidx)
|
b64 = load_frame_b64(eid, fidx)
|
||||||
if b64:
|
if b64:
|
||||||
@@ -835,6 +859,25 @@ elif page == "👤 人物管理":
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"异常: {e}")
|
st.error(f"异常: {e}")
|
||||||
|
|
||||||
|
def do_merge(src_key, tgt_key):
|
||||||
|
"""把 src 并入 tgt(判断是同一人时)"""
|
||||||
|
try:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{_core_url}/api/member/merge",
|
||||||
|
json={"source": src_key, "target": tgt_key},
|
||||||
|
timeout=15)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
result = resp.json()
|
||||||
|
st.success(
|
||||||
|
f"已合并:{src_key} → {result.get('target', tgt_key)},"
|
||||||
|
f"明细 {result.get('updated_event_details_count', 0)} 条 / "
|
||||||
|
f"事件 {result.get('updated_monitor_events_count', 0)} 条")
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
st.error(f"合并失败: {resp.status_code} {resp.text[:150]}")
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"合并异常: {e}")
|
||||||
|
|
||||||
if not person_frames:
|
if not person_frames:
|
||||||
st.markdown(
|
st.markdown(
|
||||||
'<div class="fam-empty"><span class="fe-ico">👤</span>'
|
'<div class="fam-empty"><span class="fe-ico">👤</span>'
|
||||||
@@ -892,6 +935,16 @@ elif page == "👤 人物管理":
|
|||||||
do_name(label, new_name.strip())
|
do_name(label, new_name.strip())
|
||||||
else:
|
else:
|
||||||
st.warning("请输入名字")
|
st.warning("请输入名字")
|
||||||
|
merge_targets = [k for k in person_frames if k != key]
|
||||||
|
if merge_targets:
|
||||||
|
merge_to = st.selectbox(
|
||||||
|
"合并到", options=[""] + merge_targets,
|
||||||
|
key=f"mg_{label}", label_visibility="collapsed",
|
||||||
|
placeholder="合并到其他人物…")
|
||||||
|
if merge_to:
|
||||||
|
if st.button("合并", key=f"mbtn_{label}",
|
||||||
|
use_container_width=True):
|
||||||
|
do_merge(key, merge_to)
|
||||||
st.markdown('<div style="height:10px"></div>', unsafe_allow_html=True)
|
st.markdown('<div style="height:10px"></div>', unsafe_allow_html=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user