diff --git a/fam-core/src/fam_core/db_layer.py b/fam-core/src/fam_core/db_layer.py index c73b7b0..33b9187 100644 --- a/fam-core/src/fam_core/db_layer.py +++ b/fam-core/src/fam_core/db_layer.py @@ -103,32 +103,35 @@ def upsert_sync_videos(rows: List[Dict]) -> int: def get_sync_videos(limit=15, offset=0, date_filter=None) -> List[Dict]: """获取视频会话列表(已完成优先),支持日期筛选与分页。 - 排序按 COALESCE(processed_at, updated_at, created_at) 降序。 - date_filter 形如 '2026-08-21',匹配 processed_at 前缀。 + 排序/日期维度按视频实际录制时间(event_start_time,文件名解析), + 为空回退 processed_at/updated_at/created_at。 + date_filter 形如 '2026-08-21'。 """ conn = get_conn() try: cur = conn.cursor(pymysql.cursors.DictCursor) + # 录制时间优先,回退分析时间 + date_expr = "COALESCE(NULLIF(event_start_time,''), processed_at, updated_at, created_at)" if date_filter: cur.execute( - """SELECT id, filename, camera_name, event_start_time, status, + f"""SELECT id, filename, camera_name, event_start_time, status, summary_json, events_json, people_json, compute_provider, processed_at, updated_at, (SELECT COUNT(*) FROM sync_events se WHERE se.video_id = sync_videos.id) AS event_count FROM sync_videos - WHERE status='done' AND processed_at LIKE %s - ORDER BY COALESCE(processed_at, updated_at, created_at) DESC + WHERE status='done' AND {date_expr} LIKE %s + ORDER BY {date_expr} DESC LIMIT %s OFFSET %s""", (f'{date_filter}%', limit, offset)) else: cur.execute( - """SELECT id, filename, camera_name, event_start_time, status, + f"""SELECT id, filename, camera_name, event_start_time, status, summary_json, events_json, people_json, compute_provider, processed_at, updated_at, (SELECT COUNT(*) FROM sync_events se WHERE se.video_id = sync_videos.id) AS event_count FROM sync_videos WHERE status='done' - ORDER BY COALESCE(processed_at, updated_at, created_at) DESC + ORDER BY {date_expr} DESC LIMIT %s OFFSET %s""", (limit, offset)) return cur.fetchall() @@ -201,7 +204,8 @@ def query_sync_events_for_person_date(person: str, date_str: str) -> List[Dict]: """问答上下文:某人在某天的事件。 说明: Oracle 事件 ts 为视频内相对时间点(如 00:01:23),不是绝对日期, - 因此按所属视频的 processed_at 日期过滤,再按 person_list_json 命中人名。 + 因此按所属视频的录制日期(event_start_time,回退 processed_at)过滤, + 再按 person_list_json 命中人名。 person 可为真名或抽象标签(Oracle 回灌上下文用真名,但历史标签也保留)。 """ conn = get_conn() @@ -212,10 +216,10 @@ def query_sync_events_for_person_date(person: str, date_str: str) -> List[Dict]: v.camera_name, v.filename, v.event_start_time, v.processed_at FROM sync_events e JOIN sync_videos v ON e.video_id = v.id - WHERE v.processed_at LIKE %s + WHERE COALESCE(NULLIF(v.event_start_time,''), v.processed_at) LIKE %s AND e.person_list_json IS NOT NULL AND JSON_CONTAINS(e.person_list_json, JSON_QUOTE(%s), '$') - ORDER BY v.processed_at ASC, e.ts ASC""", + ORDER BY COALESCE(NULLIF(v.event_start_time,''), v.processed_at) ASC, e.ts ASC""", (f'{date_str}%', person)) return cur.fetchall() finally: @@ -414,18 +418,19 @@ def get_sync_stats(date_str: str = None) -> Dict: conn = get_conn() try: cur = conn.cursor(pymysql.cursors.DictCursor) - # 视频/事件/关注数 + # 视频/事件/关注数(日期维度=录制时间 event_start_time,回退 processed_at) + _D = "COALESCE(NULLIF(sv.event_start_time,''), sv.processed_at)" if date_str: cur.execute( """SELECT COUNT(*) AS videos, (SELECT COUNT(*) FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id - WHERE sv.processed_at LIKE %s) AS events, + WHERE {d} LIKE %s) AS events, (SELECT COALESCE(SUM(se.is_attention_event),0) FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id - WHERE sv.processed_at LIKE %s) AS attention - FROM sync_videos sv WHERE sv.processed_at LIKE %s""", + WHERE {d} LIKE %s) AS attention + FROM sync_videos sv WHERE {d} LIKE %s""".format(d=_D), (f'{date_str}%', f'{date_str}%', f'{date_str}%')) else: cur.execute( diff --git a/fam-ui/src/app.py b/fam-ui/src/app.py index 4327a9a..3181370 100644 --- a/fam-ui/src/app.py +++ b/fam-ui/src/app.py @@ -379,19 +379,22 @@ if page == "🕒 事件时间轴": conn = get_db_conn() try: cursor = conn.cursor() + # 日期维度统一用视频实际录制时间 event_start_time(文件名解析), + # 为空时回退 processed_at(分析时间),避免"处理时间=8/21"把旧视频全堆同一天 + _DATE_EXPR = "COALESCE(NULLIF(event_start_time,''), processed_at)" if date_str: cursor.execute( - "SELECT COUNT(*) v FROM sync_videos WHERE status='done' AND processed_at LIKE %s", + f"SELECT COUNT(*) v FROM sync_videos WHERE status='done' AND {_DATE_EXPR} LIKE %s", (f'{date_str}%',)) v = cursor.fetchone().get('v', 0) cursor.execute( - "SELECT COUNT(*) e FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id WHERE sv.processed_at LIKE %s", + f"SELECT COUNT(*) e FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id WHERE {_DATE_EXPR} LIKE %s", (f'{date_str}%',)) e = cursor.fetchone().get('e', 0) cursor.execute( - """SELECT COALESCE(SUM(se.is_attention_event),0) AS att + f"""SELECT COALESCE(SUM(se.is_attention_event),0) AS att FROM sync_events se JOIN sync_videos sv ON se.video_id=sv.id - WHERE sv.processed_at LIKE %s""", (f'{date_str}%',)) + WHERE {_DATE_EXPR} LIKE %s""", (f'{date_str}%',)) att = cursor.fetchone().get('att', 0) else: cursor.execute("SELECT COUNT(*) v FROM sync_videos WHERE status='done'") @@ -435,22 +438,22 @@ if page == "🕒 事件时间轴": cursor = conn.cursor() if date_str: cursor.execute( - """SELECT id, filename, camera_name, event_start_time, summary_json, + f"""SELECT id, filename, camera_name, event_start_time, summary_json, compute_provider, processed_at, (SELECT COUNT(*) FROM sync_events se WHERE se.video_id=sync_videos.id) AS event_count FROM sync_videos - WHERE status='done' AND processed_at LIKE %s - ORDER BY COALESCE(processed_at, updated_at, created_at) DESC + WHERE status='done' AND {_DATE_EXPR} LIKE %s + ORDER BY {_DATE_EXPR} DESC LIMIT %s OFFSET %s""", (f'{date_str}%', page_size, offset)) else: cursor.execute( - """SELECT id, filename, camera_name, event_start_time, summary_json, + f"""SELECT id, filename, camera_name, event_start_time, summary_json, compute_provider, processed_at, (SELECT COUNT(*) FROM sync_events se WHERE se.video_id=sync_videos.id) AS event_count FROM sync_videos WHERE status='done' - ORDER BY COALESCE(processed_at, updated_at, created_at) DESC + ORDER BY {_DATE_EXPR} DESC LIMIT %s OFFSET %s""", (page_size, offset)) videos = cursor.fetchall() @@ -479,10 +482,10 @@ if page == "🕒 事件时间轴": f'margin-bottom:10px;">视频会话 · {len(videos)} 条', unsafe_allow_html=True) for vid in videos: - proc = vid.get('processed_at') - ptime = parse_ts(proc) - time_label = ptime.strftime('%m-%d %H:%M') if ptime else '--:--' - date_label = ptime.strftime('%Y-%m-%d') if ptime else '' + # 日期标签/时间用视频实际录制时间(event_start_time),为空回退处理时间 + ev_time = parse_ts(vid.get('event_start_time')) or parse_ts(vid.get('processed_at')) + time_label = ev_time.strftime('%m-%d %H:%M') if ev_time else '--:--' + date_label = ev_time.strftime('%Y-%m-%d') if ev_time else '' summary = (vid.get('summary_json') or '').strip() summary_short = summary if len(summary) <= 28 else summary[:28] + '…' if not summary_short: @@ -515,13 +518,15 @@ if page == "🕒 事件时间轴": with col_detail: vid = next(v for v in videos if v['id'] == selected_id) - proc = parse_ts(vid.get('processed_at')) start = parse_ts(vid.get('event_start_time')) + proc = parse_ts(vid.get('processed_at')) + # 主时间 = 录制时间(event_start_time),回退分析时间(processed_at) + main_t = start or proc range_str = '' - if proc: - range_str = proc.strftime('%Y-%m-%d %H:%M:%S') - if start: - range_str += f"(起始 {start.strftime('%H:%M:%S')})" + if main_t: + range_str = main_t.strftime('%Y-%m-%d %H:%M:%S') + if start and proc and (proc - start).total_seconds() > 60: + range_str += f"(分析于 {proc.strftime('%m-%d %H:%M')})" provider = vid.get('compute_provider') or '' model_badges = '' @@ -899,17 +904,18 @@ elif page == "📈 统计图表": st.markdown('