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 对话 / 对话历史 / 成员命名 / 统计图表
"""
import os
import re
import sys
import json
import base64
@@ -59,6 +60,22 @@ def parse_ts(ts):
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):
"""读取落盘关键帧为 base64不存在返回 None"""
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>', unsafe_allow_html=True)
page = st.sidebar.radio("功能页面", [
"🕒 事件时间轴",
"💬 AI 对话",
"📝 对话历史",
"👤 成员命名",
"📈 统计图表"
], label_visibility="collapsed")
# 顶部横向导航(窄屏下侧边栏自动收起,放主区域保证任何屏宽都可见)
page = st.segmented_control(
"功能页面",
["🕒 事件时间轴", "💬 AI 对话", "📝 对话历史", "👤 成员命名", "📈 统计图表"],
default="🕒 事件时间轴",
label_visibility="collapsed"
) or "🕒 事件时间轴"
# 侧边栏底部:任务队列状态
try:
@@ -433,26 +450,41 @@ if page == "🕒 事件时间轴":
conn = get_db_conn()
try:
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:
cursor.execute("""
SELECT COUNT(DISTINCT me.event_id) AS ev,
COUNT(ed.detail_id) AS fr,
COALESCE(SUM(ed.is_attention_event), 0) AS att,
COUNT(DISTINCT CASE WHEN ed.person <> '无人' THEN ed.person END) AS ps
COALESCE(SUM(ed.is_attention_event), 0) AS att
FROM monitor_events me
LEFT JOIN event_details ed ON ed.event_id = me.event_id
WHERE DATE(me.event_start_time) = %s
""", (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:
cursor.execute("""
SELECT COUNT(DISTINCT me.event_id) AS ev,
COUNT(ed.detail_id) AS fr,
COALESCE(SUM(ed.is_attention_event), 0) AS att,
COUNT(DISTINCT CASE WHEN ed.person <> '无人' THEN ed.person END) AS ps
COALESCE(SUM(ed.is_attention_event), 0) AS att
FROM monitor_events me
LEFT JOIN event_details ed ON ed.event_id = me.event_id
""")
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:
conn.close()