""" IngestAgent — decodes video, normalizes FPS, samples frames. Input: video file path (str) Output: IngestResult(frames, fps, duration, n_people, width, height) Failure: returns IngestResult with confidence=0.0 and notes explaining the error. Params: 0 (no model — pure OpenCV). License: n/a. Gated: no. """ from __future__ import annotations import cv2 from pathlib import Path from formscout.types import IngestResult from formscout import config class IngestAgent: """Deterministic video ingestion — no model, just OpenCV decode + frame sampling.""" def run(self, video_path: str) -> IngestResult: p = Path(video_path) if not p.exists(): return IngestResult( frames=[], fps=0.0, duration=0.0, n_people=0, width=0, height=0, confidence=0.0, notes=f"video not found: {video_path}", ) try: cap = cv2.VideoCapture(str(p)) except Exception as e: return IngestResult( frames=[], fps=0.0, duration=0.0, n_people=0, width=0, height=0, confidence=0.0, notes=f"failed to open video: {e}", ) if not cap.isOpened(): return IngestResult( frames=[], fps=0.0, duration=0.0, n_people=0, width=0, height=0, confidence=0.0, notes=f"could not open video: {video_path}", ) fps = cap.get(cv2.CAP_PROP_FPS) or config.TARGET_FPS total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) duration = total / fps if fps > 0 else 0.0 notes_parts: list[str] = [] if duration > config.MAX_DURATION_SEC: notes_parts.append( f"video is {duration:.1f}s (>{config.MAX_DURATION_SEC}s) — capping frames" ) # Sample frames evenly, capped at MAX_FRAMES step = max(1, total // config.MAX_FRAMES) frames: list = [] idx = 0 while True: ret, frame = cap.read() if not ret: break if idx % step == 0: frames.append(frame) idx += 1 if len(frames) >= config.MAX_FRAMES: break cap.release() if not frames: return IngestResult( frames=[], fps=fps, duration=duration, n_people=0, width=w, height=h, confidence=0.0, notes="no frames decoded", ) return IngestResult( frames=frames, fps=fps, duration=duration, n_people=-1, # unknown until segmentation/pose width=w, height=h, confidence=1.0, notes="; ".join(notes_parts) if notes_parts else "", )