"""在 motion_filter 找出的活动窗口内按固定间隔抽帧,调用本地 Ollama VLM 做限定词表活动分类,连续同标签的采样点合并成一条 activity 事件。 """ import json from datetime import datetime, timedelta import cv2 from analysis.config import ACTIVITY_SAMPLE_INTERVAL_SEC, ACTIVITY_MERGE_GAP_SEC, PROJECT_ROOT from analysis.db import get_all_videos, insert_event, update_video_status from analysis.ollama_client import classify_frame def _sample_labels(file_path, windows, fps): """在活动窗口内按固定秒数间隔抽帧分类,返回 [(t, label, raw_text), ...]。""" cap = cv2.VideoCapture(str(file_path)) if not cap.isOpened(): return [] results = [] for start_sec, end_sec in windows: t = start_sec while t <= end_sec: cap.set(cv2.CAP_PROP_POS_FRAMES, int(t * fps)) ret, frame = cap.read() if ret: label, raw_text = classify_frame(frame) results.append((t, label, raw_text)) t += ACTIVITY_SAMPLE_INTERVAL_SEC cap.release() return results def _merge_labeled_samples(samples): """连续同标签的采样点合并成一段 {start, end, label, raw_texts}。""" segments = [] for t, label, raw_text in samples: if (segments and segments[-1]["label"] == label and (t - segments[-1]["end"]) <= ACTIVITY_MERGE_GAP_SEC): segments[-1]["end"] = t segments[-1]["raw_texts"].append(raw_text) else: segments.append({"start": t, "end": t, "label": label, "raw_texts": [raw_text]}) return segments def process_video(video): """返回 (segment_count, failed)。""" try: windows = json.loads(video["motion_windows_json"] or "[]") except (json.JSONDecodeError, TypeError): windows = [] if not windows: update_video_status(video["id"], "activity_done") return 0, False file_path = PROJECT_ROOT / video["file_path"] if not file_path.exists(): update_video_status(video["id"], "activity_failed", error_message="文件不存在") return 0, True try: fps = video["fps"] or 15.0 samples = _sample_labels(file_path, windows, fps) except Exception as e: update_video_status(video["id"], "activity_failed", error_message=str(e)) return 0, True segments = _merge_labeled_samples(samples) start_time = None if video["start_time"]: try: start_time = datetime.strptime(video["start_time"], "%Y-%m-%d %H:%M:%S") except ValueError: pass # 只精确到天的粗粒度值,无法算出绝对时间戳,timestamp_abs留空 for seg in segments: timestamp_abs = (start_time + timedelta(seconds=seg["start"])).isoformat() if start_time else None insert_event({ "video_id": video["id"], "event_type": "activity", "label": seg["label"], "start_offset_sec": seg["start"], "end_offset_sec": seg["end"], "timestamp_abs": timestamp_abs, "confidence": None, "track_id": None, "thumbnail_path": None, "extra_json": json.dumps({"raw_texts": seg["raw_texts"]}, ensure_ascii=False), "created_at": datetime.now().isoformat(), }) update_video_status(video["id"], "activity_done") return len(segments), False def process_all(limit=None): videos = get_all_videos(status="fall_done") if limit: videos = videos[:limit] processed = 0 total_segments = 0 failed = 0 for video in videos: segments, is_failed = process_video(video) total_segments += segments if is_failed: failed += 1 processed += 1 return {"processed": processed, "total_segments": total_segments, "failed": failed} if __name__ == "__main__": print("===== 开始活动语义打标 =====") result = process_all() print(f"→ 处理 {result['processed']} 个视频,共标注 {result['total_segments']} 段活动") print("===== 打标完成 =====")