#!/usr/bin/env python3 """ Seed a scenario checkpoint from a video chunk. Extracts frames via ffmpeg, uploads to MinIO, creates a StageCheckpoint in Postgres marked as a scenario. No pipeline, no Redis, no SSE. Prerequisites: - Postgres reachable (port-forward or local) - MinIO reachable (port-forward or local) Usage: # With K8s port-forwards: kubectl port-forward svc/postgres 5432:5432 & kubectl port-forward svc/minio 9000:9000 & python tests/detect/manual/seed_scenario.py # Custom video: python tests/detect/manual/seed_scenario.py --video media/mpr/out/chunks/.../chunk_0001.mp4 Then open: http://mpr.local.ar/detection/?job=&stage=filter_scenes&editor=true """ from __future__ import annotations import argparse import logging import os import sys import uuid parser = argparse.ArgumentParser(description="Seed a scenario checkpoint") parser.add_argument("--video", default="media/mpr/out/chunks/95043d50-4df6-4ac8-bbd5-2ba873117c6e/chunk_0000.mp4") parser.add_argument("--label", default="chelsea_edges_default", help="Scenario label for bookmarking") parser.add_argument("--fps", type=float, default=2.0, help="Frames per second to extract") parser.add_argument("--max-frames", type=int, default=20, help="Max frames to extract") parser.add_argument("--db-url", default=os.environ.get("DATABASE_URL", "postgresql://mpr:mpr@localhost:5432/mpr")) parser.add_argument("--s3-url", default=os.environ.get("S3_ENDPOINT_URL", "http://localhost:9000")) args = parser.parse_args() # Set env before imports os.environ["DATABASE_URL"] = args.db_url os.environ["S3_ENDPOINT_URL"] = args.s3_url os.environ.setdefault("AWS_ACCESS_KEY_ID", "minioadmin") os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "minioadmin") sys.path.insert(0, ".") logging.basicConfig(level=logging.INFO, format="%(levelname)-7s %(name)s — %(message)s") logger = logging.getLogger(__name__) def extract_frames_ffmpeg(video_path: str, fps: float, max_frames: int): """Extract frames using ffmpeg subprocess — no pipeline dependencies.""" import subprocess import tempfile from pathlib import Path import numpy as np from PIL import Image from detect.models import Frame tmpdir = tempfile.mkdtemp(prefix="scenario_") pattern = os.path.join(tmpdir, "frame_%04d.jpg") cmd = [ "ffmpeg", "-i", video_path, "-vf", f"fps={fps}", "-frames:v", str(max_frames), "-q:v", "2", pattern, "-y", "-loglevel", "error", ] subprocess.run(cmd, check=True) frames = [] for jpg in sorted(Path(tmpdir).glob("frame_*.jpg")): seq = int(jpg.stem.split("_")[1]) - 1 # 0-indexed img = Image.open(jpg).convert("RGB") image_array = np.array(img) frame = Frame( sequence=seq, chunk_id=0, timestamp=seq / fps, image=image_array, ) frames.append(frame) jpg.unlink() Path(tmpdir).rmdir() return frames def main(): job_id = str(uuid.uuid4()) video_path = args.video if not os.path.exists(video_path): logger.error("Video not found: %s", video_path) sys.exit(1) logger.info("Video: %s", video_path) logger.info("Job ID: %s", job_id) logger.info("Label: %s", args.label) # Ensure DB tables exist from core.db.connection import create_tables create_tables() # Extract frames logger.info("Extracting frames (fps=%.1f, max=%d)...", args.fps, args.max_frames) frames = extract_frames_ffmpeg(video_path, args.fps, args.max_frames) logger.info("Extracted %d frames", len(frames)) # Upload frames to MinIO from detect.checkpoint.frames import save_frames logger.info("Uploading frames to MinIO...") manifest = save_frames(job_id, frames) logger.info("Uploaded %d frames", len(manifest)) # Build frame metadata frames_meta = [ { "sequence": f.sequence, "chunk_id": f.chunk_id, "timestamp": f.timestamp, "perceptual_hash": "", } for f in frames ] # All frames are "filtered" (no scene filter ran) filtered_sequences = [f.sequence for f in frames] # Save checkpoint as scenario from core.db.detect import save_stage_checkpoint from detect.checkpoint.frames import CHECKPOINT_PREFIX checkpoint = save_stage_checkpoint( job_id=job_id, stage="filter_scenes", stage_index=1, frames_prefix=f"{CHECKPOINT_PREFIX}/{job_id}/frames/", frames_manifest={str(k): v for k, v in manifest.items()}, frames_meta=frames_meta, filtered_frame_sequences=filtered_sequences, stage_output_key="", stats={"frames_extracted": len(frames), "frames_after_scene_filter": len(frames)}, config_snapshot={}, config_overrides={}, video_path=video_path, profile_name="soccer_broadcast", is_scenario=True, scenario_label=args.label, ) logger.info("") logger.info("Scenario created:") logger.info(" ID: %s", checkpoint.id) logger.info(" Job: %s", job_id) logger.info(" Label: %s", args.label) logger.info(" Frames: %d", len(frames)) logger.info("") logger.info("Open in editor:") logger.info(" http://mpr.local.ar/detection/?job=%s#/editor/detect_edges", job_id) if __name__ == "__main__": main()