feat(fam-edge): 队列健壮性 - ①失败重试间隔 retry_interval_sec=1h(last_fail_at 记录,配额类故障等恢复再试);②max_retries 2→10;③入队前 OpenCV 文件校验(可解码/大小/元数据,失败标 invalid 不入队)+ mtime 稳定窗口防 rclone 半成品 + 处理前二次确认

This commit is contained in:
ericwyuan
2026-08-21 14:09:38 +08:00
parent 92ef9fe163
commit 6afdda582b
4 changed files with 131 additions and 11 deletions

View File

@@ -86,6 +86,46 @@ def _parse_event_ts(ts: str, start_dt):
return ts, 0.0
def validate_video(path: str) -> tuple:
"""校验视频文件是否为正常可解码视频。
返回 (ok: bool, error: str, meta: dict|None)
- meta: {fps, frames, duration_sec, width, height}
用 OpenCV 打开并读取至少 1 帧(不校验会导致空/半成品文件浪费云端配额)。
"""
meta = None
try:
if not path or not os.path.isfile(path):
return False, "file_missing", None
if os.path.getsize(path) == 0:
return False, "file_empty", None
try:
import cv2
except ImportError:
return True, "", None # 无 cv2 时跳过深度校验(仅大小检查)
cap = cv2.VideoCapture(path)
try:
if not cap.isOpened():
return False, "cannot_open", None
ok, frame = cap.read()
if not ok or frame is None:
return False, "no_decodable_frame", None
fps = float(cap.get(cv2.CAP_PROP_FPS) or 0)
frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
meta = {
"fps": round(fps, 2),
"frames": frames,
"duration_sec": round(frames / max(fps, 0.01), 1),
"width": int(cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 0),
"height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 0),
}
finally:
cap.release()
return True, "", meta
except Exception as e:
return False, f"validate_exc: {e}", None
class VideoProcessor:
def __init__(self, db: oracle_db.OracleDB):
self.config = load_config()
@@ -93,6 +133,8 @@ class VideoProcessor:
self.vision_order = self.config.get('video_processing', {}).get(
'vision_order', ['gemini', 'nvidia'])
self.vision_timeout = self.config.get('video_processing', {}).get('timeout', 900)
self.file_validate = bool(self.config.get('video_processing', {}).get(
'file_validate', True))
self.parse_start = self.config.get('gdrive_sync', {}).get(
'parse_start_from_filename', True)
adapters = build_adapters(self.config.get('models', []))
@@ -122,6 +164,17 @@ class VideoProcessor:
self.db.mark_video_failed(video_id, "file_missing")
return False
# 处理前二次确认文件有效性(防止登记后文件被破坏/截断;校验结果落库)
if self.file_validate:
ok, verr, vmeta = validate_video(local_path)
if not ok:
logger.error(f"[video_id={video_id}] 文件校验失败({verr}),标记 failed: {local_path}")
self.db.set_video_file_status(video_id, False, verr)
self.db.mark_video_failed(video_id, f"invalid_file:{verr}")
return False
if vmeta:
self.db.set_video_file_status(video_id, True, '', vmeta)
camera_name = self.db.get_video_by_filename(filename)['camera_name'] or ''
event_start = ''
if self.parse_start: