[功能] 事件-片段一致性对账 - /api/oracle/activity 加 segment.consistency(事件总数/已结束/已分割/缺口+明细/素材覆盖范围), 服务状态页展示一致性数字
This commit is contained in:
@@ -163,6 +163,8 @@ def activity():
|
|||||||
segment['clips_files'] = len(os.listdir(seg_dir)) if os.path.isdir(seg_dir) else 0
|
segment['clips_files'] = len(os.listdir(seg_dir)) if os.path.isdir(seg_dir) else 0
|
||||||
except Exception:
|
except Exception:
|
||||||
segment['clips_files'] = 0
|
segment['clips_files'] = 0
|
||||||
|
# 事件↔片段一致性对账(数量可验证)
|
||||||
|
segment['consistency'] = db.get_segment_consistency()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"queue": q_status,
|
"queue": q_status,
|
||||||
"db": db.get_queue_status(),
|
"db": db.get_queue_status(),
|
||||||
|
|||||||
@@ -398,6 +398,50 @@ class OracleDB:
|
|||||||
"last": dict(last) if last else None,
|
"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,
|
def set_video_file_status(self, video_id: int, valid: bool,
|
||||||
error: str = '', media_meta: dict = None):
|
error: str = '', media_meta: dict = None):
|
||||||
"""登记/更新文件校验结果:valid / file_error / media_meta_json"""
|
"""登记/更新文件校验结果:valid / file_error / media_meta_json"""
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const qs = computed(() => queue.value.stats || {})
|
|||||||
const rclone = computed(() => oracle.value.rclone)
|
const rclone = computed(() => oracle.value.rclone)
|
||||||
const person = computed(() => oracle.value.person)
|
const person = computed(() => oracle.value.person)
|
||||||
const segment = computed(() => oracle.value.segment)
|
const segment = computed(() => oracle.value.segment)
|
||||||
|
const segCons = computed(() => segment.value?.consistency || {})
|
||||||
const nasSync = computed(() => data.value?.nas_sync)
|
const nasSync = computed(() => data.value?.nas_sync)
|
||||||
const modelCalls = computed(() => oracle.value.model_calls || [])
|
const modelCalls = computed(() => oracle.value.model_calls || [])
|
||||||
const activities = computed(() => oracle.value.activities || [])
|
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}` : '暂无片段' }}
|
{{ segment && segment.total ? `运动片段 ${segment.done ?? 0}/${segment.total} · 文件 ${segment.clips_files ?? 0}` : '暂无片段' }}
|
||||||
<template #sub v-if="segment && segment.total">
|
<template #sub v-if="segment && segment.total">
|
||||||
待处理 {{ segment.pending ?? 0 }} · 失败 {{ segment.failed ?? 0 }} · 运动事件 {{ segment.motion_events ?? 0 }}
|
待处理 {{ 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">
|
<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) }}
|
{{ (segment.last.ts || '').slice(0, 19) }} · {{ (segment.last.detail || '').slice(0, 42) }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user