[功能] 事件-片段一致性对账 - /api/oracle/activity 加 segment.consistency(事件总数/已结束/已分割/缺口+明细/素材覆盖范围), 服务状态页展示一致性数字

This commit is contained in:
ericwyuan
2026-08-22 13:40:42 +08:00
parent 177c2a83b7
commit 1dd9df34c2
3 changed files with 53 additions and 0 deletions

View File

@@ -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(),

View File

@@ -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"""

View File

@@ -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}` : '暂无片段' }}
<template #sub v-if="segment && segment.total">
待处理 {{ segment.pending ?? 0 }} · 失败 {{ segment.failed ?? 0 }} · 运动事件 {{ segment.motion_events ?? 0 }}
<span v-if="segCons.event_total" class="mt-0.5 block">
一致性事件 {{ segCons.event_total }} · 已结束 {{ segCons.finished }} · 已分割 {{ segCons.segmented }}
<span :class="(segCons.gap_count || 0) > 0 ? 'text-warn' : 'text-text-mute'">
· 缺口 {{ segCons.gap_count ?? 0 }}
</span>
</span>
<span v-if="segment.last" class="mt-0.5 block text-text-mute">
{{ (segment.last.ts || '').slice(0, 19) }} · {{ (segment.last.detail || '').slice(0, 42) }}
</span>