From 1dd9df34c235e081ed7ff90653efb8f3c0fc182d Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Sat, 22 Aug 2026 13:40:42 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=8A=9F=E8=83=BD]=20=E4=BA=8B=E4=BB=B6-?= =?UTF-8?q?=E7=89=87=E6=AE=B5=E4=B8=80=E8=87=B4=E6=80=A7=E5=AF=B9=E8=B4=A6?= =?UTF-8?q?=20-=20/api/oracle/activity=20=E5=8A=A0=20segment.consistency(?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E6=80=BB=E6=95=B0/=E5=B7=B2=E7=BB=93?= =?UTF-8?q?=E6=9D=9F/=E5=B7=B2=E5=88=86=E5=89=B2/=E7=BC=BA=E5=8F=A3+?= =?UTF-8?q?=E6=98=8E=E7=BB=86/=E7=B4=A0=E6=9D=90=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E8=8C=83=E5=9B=B4),=20=E6=9C=8D=E5=8A=A1=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E9=A1=B5=E5=B1=95=E7=A4=BA=E4=B8=80=E8=87=B4=E6=80=A7=E6=95=B0?= =?UTF-8?q?=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/fam_edge/api_gateway/api_gateway.py | 2 + fam-edge/src/fam_edge/oracle_db.py | 44 +++++++++++++++++++ fam-ui/src/views/ServiceStatus.vue | 7 +++ 3 files changed, 53 insertions(+) diff --git a/fam-edge/src/fam_edge/api_gateway/api_gateway.py b/fam-edge/src/fam_edge/api_gateway/api_gateway.py index ae94e57..96b91f8 100644 --- a/fam-edge/src/fam_edge/api_gateway/api_gateway.py +++ b/fam-edge/src/fam_edge/api_gateway/api_gateway.py @@ -163,6 +163,8 @@ def activity(): segment['clips_files'] = len(os.listdir(seg_dir)) if os.path.isdir(seg_dir) else 0 except Exception: segment['clips_files'] = 0 + # 事件↔片段一致性对账(数量可验证) + segment['consistency'] = db.get_segment_consistency() return jsonify({ "queue": q_status, "db": db.get_queue_status(), diff --git a/fam-edge/src/fam_edge/oracle_db.py b/fam-edge/src/fam_edge/oracle_db.py index 5e42cef..e8c049d 100644 --- a/fam-edge/src/fam_edge/oracle_db.py +++ b/fam-edge/src/fam_edge/oracle_db.py @@ -398,6 +398,50 @@ class OracleDB: "last": dict(last) if last else None, } + def get_segment_consistency(self, gap_limit: int = 20) -> Dict: + """事件↔片段一致性对账(数量可验证)。 + + - event_total : ss_motion_events 全部运动事件(SS 已回灌的) + - finished : 已结束且 duration>0(具备分割条件)的事件数 + - segmented : 已生成片段的去重事件数(videos.motion_event_id) + - gap_count : 已结束但未生成片段(缺口 = finished - segmented 的下界) + - gaps : 缺口明细(event_id/start_time/duration,北京时间) + - material_range: 素材文件覆盖的 event_start_time 范围(判断缺口是否因素材缺失) + """ + now_ts = int(datetime.now(timezone(timedelta(hours=8))).timestamp()) + fin = ("event_type = 10 AND COALESCE(duration,0) > 0 " + "AND (start_time + COALESCE(duration,0)) <= ?") + event_total = self._conn.execute( + "SELECT COUNT(*) c FROM ss_motion_events WHERE event_type=10" + ).fetchone()['c'] + finished = self._conn.execute( + f"SELECT COUNT(*) c FROM ss_motion_events WHERE {fin}", (now_ts,) + ).fetchone()['c'] + segmented = self._conn.execute( + "SELECT COUNT(DISTINCT motion_event_id) c FROM videos " + "WHERE motion_event_id IS NOT NULL" + ).fetchone()['c'] + gaps = self._conn.execute( + f"""SELECT me.event_id, me.camera_id, me.start_time, me.duration + FROM ss_motion_events me + WHERE {fin} + AND NOT EXISTS (SELECT 1 FROM videos v + WHERE v.motion_event_id = me.event_id) + ORDER BY me.start_time DESC LIMIT ?""", + (now_ts, int(gap_limit))).fetchall() + mrange = self._conn.execute( + "SELECT MIN(event_start_time) mn, MAX(event_start_time) mx " + "FROM videos WHERE motion_event_id IS NULL AND event_start_time != ''" + ).fetchone() + return { + "event_total": event_total, + "finished": finished, + "segmented": segmented, + "gap_count": max(0, finished - segmented), + "gaps": [dict(r) for r in gaps], + "material_range": {"min": mrange['mn'], "max": mrange['mx']} if mrange and mrange['mn'] else None, + } + def set_video_file_status(self, video_id: int, valid: bool, error: str = '', media_meta: dict = None): """登记/更新文件校验结果:valid / file_error / media_meta_json""" diff --git a/fam-ui/src/views/ServiceStatus.vue b/fam-ui/src/views/ServiceStatus.vue index c46f03a..4379f2e 100644 --- a/fam-ui/src/views/ServiceStatus.vue +++ b/fam-ui/src/views/ServiceStatus.vue @@ -39,6 +39,7 @@ const qs = computed(() => queue.value.stats || {}) const rclone = computed(() => oracle.value.rclone) const person = computed(() => oracle.value.person) const segment = computed(() => oracle.value.segment) +const segCons = computed(() => segment.value?.consistency || {}) const nasSync = computed(() => data.value?.nas_sync) const modelCalls = computed(() => oracle.value.model_calls || []) const activities = computed(() => oracle.value.activities || []) @@ -106,6 +107,12 @@ const SVC_BADGE = { {{ segment && segment.total ? `运动片段 ${segment.done ?? 0}/${segment.total} · 文件 ${segment.clips_files ?? 0}` : '暂无片段' }}