Spaces:
Sleeping
Sleeping
| """Adaptive frame sampling for video ingest. Worker-only (uses OpenCV).""" | |
| import logging | |
| log = logging.getLogger("mmap.video.frames") | |
| def probe_video_duration(path: str) -> float: | |
| """Read just the container metadata (no frame decode) and return the | |
| duration in seconds. Returns 0.0 when fps/frame-count are unreadable β | |
| callers should treat 0.0 as "unknown", not "empty".""" | |
| import cv2 # type: ignore[import-not-found] | |
| video = cv2.VideoCapture(path) | |
| if not video.isOpened(): | |
| raise RuntimeError(f"could not open video at {path!r}") | |
| try: | |
| fps = float(video.get(cv2.CAP_PROP_FPS) or 0.0) | |
| total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT) or 0) | |
| if fps <= 0 or total_frames <= 0: | |
| return 0.0 | |
| return total_frames / fps | |
| finally: | |
| video.release() | |
| def extract_adaptive_frames( | |
| path: str, | |
| *, | |
| max_frame_budget: int = 30, | |
| target_width: int = 720, | |
| jpeg_quality: int = 85, | |
| ) -> list[bytes]: | |
| """Sample frames from `path` with a duration-aware cadence, downscale to | |
| `target_width`, and JPEG-encode. Returns a list of frame bytes. Assumes | |
| duration β€ 300s β callers enforce the cap upstream.""" | |
| # Lazy import: cv2 (opencv-python-headless) lives in the worker image only. | |
| import cv2 # type: ignore[import-not-found] | |
| video = cv2.VideoCapture(path) | |
| if not video.isOpened(): | |
| raise RuntimeError(f"could not open video at {path!r}") | |
| try: | |
| fps = float(video.get(cv2.CAP_PROP_FPS) or 0.0) | |
| total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT) or 0) | |
| if fps <= 0 or total_frames <= 0: | |
| log.info("video frames: empty stream (fps=%.2f frames=%d)", fps, total_frames) | |
| return [] | |
| duration = total_frames / fps | |
| # Adaptive bucket sampling tuned to keep token cost bounded while | |
| # preserving temporal resolution on short clips. The β€300s tier is | |
| # the terminal branch β duration is bounded upstream by the | |
| # `video_max_duration_sec` cap. | |
| if duration <= 60: | |
| step = max(1, int(fps)) | |
| elif duration <= 120: | |
| step = max(1, int(fps * 2)) | |
| else: | |
| step = max(1, int(fps * 5)) | |
| indices = list(range(0, total_frames, step)) | |
| # Hard ceiling β downsample uniformly if the cadence still overshoots. | |
| if len(indices) > max_frame_budget: | |
| stride = len(indices) / max_frame_budget | |
| indices = [indices[int(i * stride)] for i in range(max_frame_budget)] | |
| log.info( | |
| "video frames: duration=%.1fs fps=%.1f step=%d sampled=%d", | |
| duration, | |
| fps, | |
| step, | |
| len(indices), | |
| ) | |
| out: list[bytes] = [] | |
| encode_params = [int(cv2.IMWRITE_JPEG_QUALITY), jpeg_quality] | |
| for idx in indices: | |
| video.set(cv2.CAP_PROP_POS_FRAMES, idx) | |
| ok, frame = video.read() | |
| if not ok: | |
| continue | |
| h, w = frame.shape[:2] | |
| if w > target_width: | |
| scale = target_width / w | |
| frame = cv2.resize( | |
| frame, | |
| (target_width, int(h * scale)), | |
| interpolation=cv2.INTER_AREA, | |
| ) | |
| ok, buf = cv2.imencode(".jpg", frame, encode_params) | |
| if ok: | |
| out.append(bytes(buf)) | |
| return out | |
| finally: | |
| video.release() | |