fix: 出现人物统计去重 + 导航移至页面顶部

- person 字段是自由文本,一帧可为多人组合且带括号特征描述
  ('张三 (红色T恤), 汤圆, 人物3'),COUNT(DISTINCT person) 把每种
  组合字符串当一个人物,全局误报 20 人。新增 resolve_persons:
  去括号描述 -> 按中英逗号/顿号/斜杠拆分 -> family_members
  抽象标签映射真名,去重后全局实际 6 人
- 导航从侧边栏 radio 改为顶部 segmented_control:窄屏下
  Streamlit 侧边栏自动收起,页面入口藏进汉堡菜单导致
  "成员命名/问答界面不见了";放主区域顶部任何屏宽可见
This commit is contained in:
ericwyuan
2026-08-21 00:22:24 +08:00
parent 5d574029b0
commit 3fb86ee449

View File

@@ -6,6 +6,7 @@ FAM-UI - 家庭多模态智能监控系统前端 v2
- AI 对话 / 对话历史 / 成员命名 / 统计图表 - AI 对话 / 对话历史 / 成员命名 / 统计图表
""" """
import os import os
import re
import sys import sys
import json import json
import base64 import base64
@@ -59,6 +60,22 @@ def parse_ts(ts):
return None return None
def resolve_persons(person_str, alias_map):
"""解析一帧 person 字段为真实人物集合
person 是自由文本,可能为多人组合且带括号特征描述,如
'张三 (红色T恤, 白色裤子), 汤圆 (蓝色上衣), 人物3'
统计真实人数需: 去括号描述 -> 按分隔符拆分 -> 抽象标签映射真名。
"""
s = re.sub(r'[(][^()]*[)]', '', person_str or '')
out = set()
for n in re.split(r'[,,、/]+', s):
n = n.strip()
if n and n != '无人':
out.add(alias_map.get(n, n))
return out
def load_frame_b64(event_id, frame_index): def load_frame_b64(event_id, frame_index):
"""读取落盘关键帧为 base64不存在返回 None""" """读取落盘关键帧为 base64不存在返回 None"""
path = os.path.join(FRAME_DIR, f'event_{event_id}', f'frame_{frame_index}.jpg') path = os.path.join(FRAME_DIR, f'event_{event_id}', f'frame_{frame_index}.jpg')
@@ -383,13 +400,13 @@ st.sidebar.markdown(
'<div style="font-size:11px;color:#64748b;margin-top:3px;">SENTINEL HOME AI</div>' '<div style="font-size:11px;color:#64748b;margin-top:3px;">SENTINEL HOME AI</div>'
'</div>', unsafe_allow_html=True) '</div>', unsafe_allow_html=True)
page = st.sidebar.radio("功能页面", [ # 顶部横向导航(窄屏下侧边栏自动收起,放主区域保证任何屏宽都可见)
"🕒 事件时间轴", page = st.segmented_control(
"💬 AI 对话", "功能页面",
"📝 对话历史", ["🕒 事件时间轴", "💬 AI 对话", "📝 对话历史", "👤 成员命名", "📈 统计图表"],
"👤 成员命名", default="🕒 事件时间轴",
"📈 统计图表" label_visibility="collapsed"
], label_visibility="collapsed") ) or "🕒 事件时间轴"
# 侧边栏底部:任务队列状态 # 侧边栏底部:任务队列状态
try: try:
@@ -433,26 +450,41 @@ if page == "🕒 事件时间轴":
conn = get_db_conn() conn = get_db_conn()
try: try:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(
"SELECT abstract_label, real_name FROM family_members "
"WHERE real_name IS NOT NULL AND is_active = TRUE")
alias_map = {r['abstract_label']: r['real_name'] for r in cursor.fetchall()}
if date_str: if date_str:
cursor.execute(""" cursor.execute("""
SELECT COUNT(DISTINCT me.event_id) AS ev, SELECT COUNT(DISTINCT me.event_id) AS ev,
COUNT(ed.detail_id) AS fr, COUNT(ed.detail_id) AS fr,
COALESCE(SUM(ed.is_attention_event), 0) AS att, COALESCE(SUM(ed.is_attention_event), 0) AS att
COUNT(DISTINCT CASE WHEN ed.person <> '无人' THEN ed.person END) AS ps
FROM monitor_events me FROM monitor_events me
LEFT JOIN event_details ed ON ed.event_id = me.event_id LEFT JOIN event_details ed ON ed.event_id = me.event_id
WHERE DATE(me.event_start_time) = %s WHERE DATE(me.event_start_time) = %s
""", (date_str,)) """, (date_str,))
s = cursor.fetchone() or {}
cursor.execute(
"""SELECT ed.person FROM event_details ed
JOIN monitor_events me ON me.event_id = ed.event_id
WHERE DATE(me.event_start_time) = %s""", (date_str,))
else: else:
cursor.execute(""" cursor.execute("""
SELECT COUNT(DISTINCT me.event_id) AS ev, SELECT COUNT(DISTINCT me.event_id) AS ev,
COUNT(ed.detail_id) AS fr, COUNT(ed.detail_id) AS fr,
COALESCE(SUM(ed.is_attention_event), 0) AS att, COALESCE(SUM(ed.is_attention_event), 0) AS att
COUNT(DISTINCT CASE WHEN ed.person <> '无人' THEN ed.person END) AS ps
FROM monitor_events me FROM monitor_events me
LEFT JOIN event_details ed ON ed.event_id = me.event_id LEFT JOIN event_details ed ON ed.event_id = me.event_id
""") """)
s = cursor.fetchone() or {} s = cursor.fetchone() or {}
cursor.execute(
"SELECT ed.person FROM event_details ed "
"JOIN monitor_events me ON me.event_id = ed.event_id")
persons = set()
for r in cursor.fetchall():
persons |= resolve_persons(r['person'], alias_map)
s['ps'] = len(persons)
finally: finally:
conn.close() conn.close()