#!/usr/bin/env python3 """Create a non-destructive, longest-first media view from a render manifest.""" import argparse import csv from pathlib import Path TIERS = ( ("01_Long_Full_Motion_3sPlus", 3.0), ("02_Medium_Motion_1.5to2.99s", 1.5), ("03_Short_Bounce_Under1.5s", 0.0), ) def choose_manifest(source: Path, explicit: Path) -> Path: if explicit: return explicit.resolve() matches = sorted(source.glob("*render_manifest.csv")) if len(matches) != 1: raise SystemExit("Specify --manifest when zero or multiple render manifests exist") return matches[0] def main(): parser = argparse.ArgumentParser() parser.add_argument("source", type=Path) parser.add_argument("output", type=Path) parser.add_argument("--manifest", type=Path) parser.add_argument("--long-cycle", type=float, default=3.0) parser.add_argument("--medium-cycle", type=float, default=1.5) args = parser.parse_args() source = args.source.resolve() output = args.output.resolve() if not source.is_dir(): raise SystemExit(f"Source folder not found: {source}") output.mkdir(parents=True, exist_ok=True) if any(output.iterdir()): raise SystemExit(f"Output must be empty: {output}") manifest = choose_manifest(source, args.manifest) tiers = ( ("01_Long_Full_Motion_3sPlus", args.long_cycle), ("02_Medium_Motion_1.5to2.99s", args.medium_cycle), ("03_Short_Bounce_Under1.5s", 0.0), ) with manifest.open(newline="", encoding="utf-8") as handle: rows = [row for row in csv.DictReader(handle) if row.get("output_file")] grouped = {name: [] for name, _ in tiers} for row in rows: cycle = float(row.get("base_cycle_seconds") or 0) for name, threshold in tiers: if cycle >= threshold: grouped[name].append(row) break index_rows = [] for tier_number, (tier_name, _) in enumerate(tiers, 1): directory = output / tier_name directory.mkdir() ordered = sorted( grouped[tier_name], key=lambda row: ( -float(row.get("base_cycle_seconds") or 0), -int(row.get("source_frames") or 0), row["output_file"], ), ) for rank, row in enumerate(ordered, 1): source_file = source / row["output_file"] if not source_file.is_file(): raise FileNotFoundError(source_file) cycle = float(row.get("base_cycle_seconds") or 0) link_name = f"{rank:02d}_{cycle:05.2f}s_{source_file.name}" link = directory / link_name link.symlink_to(source_file) index_rows.append({ "tier": tier_number, "tier_name": tier_name, "rank_longest_first": rank, "unique_cycle_seconds": f"{cycle:.3f}", "source_frames": row.get("source_frames", ""), "finished_seconds": row.get("duration_seconds", ""), "original_file": source_file.name, "organized_link": str(link.relative_to(output)), }) index_path = output / "motion_length_index.csv" with index_path.open("w", newline="", encoding="utf-8") as handle: writer = csv.DictWriter(handle, fieldnames=index_rows[0]) writer.writeheader() writer.writerows(index_rows) readme = output / "README.md" readme.write_text( "# Motion-length review order\n\n" "These folders contain symbolic links to the verified preview media; " "the source clips were not moved or duplicated. Unique motion means one " "forward/back cycle before repetition.\n\n" f"- Long/full motion: {len(grouped[tiers[0][0]])} clips, " f"at least {args.long_cycle:g} seconds of unique motion\n" f"- Medium motion: {len(grouped[tiers[1][0]])} clips, " f"{args.medium_cycle:g} to {args.long_cycle - 0.01:.2f} seconds\n" f"- Short bounce: {len(grouped[tiers[2][0]])} clips, " f"under {args.medium_cycle:g} seconds\n\n" "Clips within each folder are numbered longest-first.\n", encoding="utf-8", ) for tier_name, _ in tiers: print(f"{tier_name}: {len(grouped[tier_name])}") print(f"Index: {index_path}") if __name__ == "__main__": main()