feat: video analysis switched to push mode (upload whole video, sync response)
Rationale: Oracle cannot reach NAS (Tailscale userspace mode on NAS, no TUN), the old pull+webhook design requires Edge to download video from NAS and callback to NAS - both blocked. New design is one-way NAS -> Oracle: - FAM-Edge: new POST /api/edge/video/push endpoint accepts multipart video upload, reuses existing OpenCV scene-change keyframe selection, analyzes synchronously and returns the result payload directly in the HTTP response (no webhook callback). Old /api/edge/video/analyze kept for compatibility. - FAM-Edge: VideoPreprocessor.save_upload() saves the uploaded file - FAM-Edge: AIOrchestrator.process_push_task() runs the full pipeline (health check -> extract -> select -> compress -> VLM -> fusion) and returns callback-style payload dict - FAM-Core: Dispatcher rewritten to push mode - reads local video file, uploads with task metadata (camera_name, event_start_time from file mtime, known_members_context), applies the result to DB via shared event_receiver.apply_success_event() - FAM-Core: event_receiver success logic extracted into reusable apply_success_event() (used by both webhook route and dispatcher) - config: edge_url -> /api/edge/video/push, push_timeout 1800s, gunicorn Edge timeout raised to 1800s for long synchronous analysis
This commit is contained in:
@@ -246,7 +246,7 @@ class AIOrchestrator:
|
||||
logger.error(f"[task_id={task_id}] 失败回调也失败: {e}")
|
||||
|
||||
def process_task(self, task_data: dict):
|
||||
"""端到端处理任务"""
|
||||
"""端到端处理任务(拉取模式,webhook 回调)"""
|
||||
task_id = task_data.get('task_id')
|
||||
video_url = task_data.get('video_url')
|
||||
webhook_url = task_data.get('webhook_url')
|
||||
@@ -325,3 +325,82 @@ class AIOrchestrator:
|
||||
preprocessor.cleanup()
|
||||
|
||||
return 200
|
||||
|
||||
def process_push_task(self, task_data: dict, video_path: str,
|
||||
preprocessor: 'VideoPreprocessor') -> dict:
|
||||
"""推送模式:同步处理上传的视频,结果直接返回(无 webhook 回调)
|
||||
|
||||
返回 payload 结构与原 webhook 回调一致:
|
||||
- 成功: {task_id, status: "success", event_start_time, ..., frame_details, ...}
|
||||
- 失败: {task_id, status: "failed", failure_stage, error_message}
|
||||
"""
|
||||
task_id = task_data.get('task_id')
|
||||
known_members = task_data.get('known_members_context', '')
|
||||
event_start_time = task_data.get('event_start_time', '')
|
||||
|
||||
logger.info(f"[task_id={task_id}] ====== 开始处理推送任务 ======")
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 1. 健康检查
|
||||
healthy_adapters = self.health_check_all()
|
||||
if not healthy_adapters:
|
||||
logger.error(f"[task_id={task_id}] 所有模型不健康")
|
||||
return {
|
||||
"task_id": task_id, "status": "failed",
|
||||
"failure_stage": "vlm_visual",
|
||||
"error_message": "All models unhealthy"
|
||||
}
|
||||
|
||||
# 2. 抽帧(视频已由调用方保存到本地,无需下载)
|
||||
candidate_frames = preprocessor.extract_candidate_frames(video_path)
|
||||
if not candidate_frames:
|
||||
raise Exception("抽帧失败,无候选帧")
|
||||
|
||||
key_frames = preprocessor.select_key_frames(candidate_frames)
|
||||
compressed_frames = preprocessor.compress_frames(key_frames)
|
||||
if not compressed_frames:
|
||||
raise Exception("压缩后无可用帧")
|
||||
|
||||
frame_timestamps = preprocessor.compute_timestamps(
|
||||
video_path, len(compressed_frames), event_start_time
|
||||
)
|
||||
|
||||
# 3. 并行视觉分析
|
||||
model_outputs = self.run_visual_analysis(
|
||||
healthy_adapters, compressed_frames, frame_timestamps, known_members
|
||||
)
|
||||
if not model_outputs:
|
||||
raise Exception('All models failed in visual analysis')
|
||||
|
||||
# 4. 文本融合
|
||||
fusion_result = self.run_text_fusion(model_outputs, known_members, task_id)
|
||||
|
||||
total_ms = int((time.time() - start_time) * 1000)
|
||||
log_task(logger, task_id, 'overall', '推送任务完成', duration_ms=total_ms)
|
||||
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"status": "success",
|
||||
"event_start_time": event_start_time,
|
||||
"event_end_time": task_data.get('event_end_time', ''),
|
||||
"camera_name": task_data.get('camera_name', ''),
|
||||
"global_summary": fusion_result.get('global_summary', ''),
|
||||
"entities_json": fusion_result.get('entities_json', []),
|
||||
"frame_details": fusion_result.get('frame_details', []),
|
||||
"compute_provider": fusion_result.get('compute_provider', []),
|
||||
"error_message": None
|
||||
}
|
||||
|
||||
except VLMOutputInvalidError as e:
|
||||
logger.error(f"[task_id={task_id}] VLM 输出解析失败: {e}")
|
||||
return {
|
||||
"task_id": task_id, "status": "failed",
|
||||
"failure_stage": "vlm_fusion", "error_message": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"[task_id={task_id}] 推送任务处理失败: {e}", exc_info=True)
|
||||
return {
|
||||
"task_id": task_id, "status": "failed",
|
||||
"failure_stage": "process", "error_message": str(e)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user