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