"""Run FreeMoCap's stable headless pipeline for one uploaded reference video.""" from __future__ import annotations import argparse import json import shutil import subprocess from pathlib import Path def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--input", required=True, type=Path) parser.add_argument("--recording", required=True, type=Path) parser.add_argument("--output", required=True, type=Path) parser.add_argument("--metadata", required=True, type=Path) parser.add_argument("--fps", required=True, type=float) return parser.parse_args() def prepare_mp4(source: Path, destination: Path) -> None: destination.parent.mkdir(parents=True, exist_ok=True) if source.suffix.lower() == ".mp4": shutil.copyfile(source, destination) return subprocess.run( [ "ffmpeg", "-y", "-i", str(source), "-an", "-c:v", "libx264", "-pix_fmt", "yuv420p", str(destination), ], check=True, ) def main() -> None: args = parse_args() synchronized = args.recording / "synchronized_videos" prepared_video = synchronized / "camera_0.mp4" prepare_mp4(args.input, prepared_video) # FreeMoCap 1.8.x exposes this stable headless entry point. Blender and the # notebook are deliberately disabled: Tilt only needs its filtered body XYZ. from freemocap.core_processes.process_motion_capture_videos.process_recording_headless import ( process_recording_headless, ) from freemocap.data_layer.recording_models.post_processing_parameter_models import ( ProcessingParameterModel, ) import cv2 capture = cv2.VideoCapture(str(prepared_video)) detected_fps = float(capture.get(cv2.CAP_PROP_FPS)) capture.release() source_fps = detected_fps if detected_fps > 0 else args.fps parameters = ProcessingParameterModel() parameters.post_processing_parameters_model.framerate = source_fps parameters.post_processing_parameters_model.butterworth_filter_parameters.sampling_rate = source_fps # Preserve MediaPipe's monocular depth estimate; the default flattened mode # throws that axis away before FreeMoCap's interpolation and rigid-bone pass. parameters.anipose_triangulate_3d_parameters_model.flatten_single_camera_data = False process_recording_headless( recording_path=args.recording, recording_processing_parameter_model=parameters, run_blender=False, make_jupyter_notebook=False, use_tqdm=False, ) result = args.recording / f"{args.recording.name}_by_frame.csv" if not result.is_file(): matches = sorted(args.recording.glob("*_by_frame.csv")) if not matches: raise FileNotFoundError("FreeMoCap completed without producing a by-frame CSV") result = matches[0] args.output.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(result, args.output) args.metadata.write_text(json.dumps({"fps": source_fps}), encoding="utf-8") if __name__ == "__main__": main()