diff --git a/fam-ui/src/app.py b/fam-ui/src/app.py
index 2e72e44..76cef5c 100644
--- a/fam-ui/src/app.py
+++ b/fam-ui/src/app.py
@@ -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(
'
SENTINEL HOME AI
'
'', 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 {}
+ 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()