#!/usr/bin/env python3 from __future__ import annotations import argparse import json import shutil import subprocess from pathlib import Path def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Summarize a prepared or completed poetry compare run.") parser.add_argument("run_root", nargs="?", type=Path, help="Compare run directory. Defaults to the latest run under outputs/compare_poetry.") parser.add_argument("--compare-root", type=Path, default=Path(__file__).resolve().parents[2] / "outputs" / "compare_poetry") parser.add_argument("--json", action="store_true", help="Print JSON instead of a text table.") parser.add_argument("--ffprobe-timeout", type=float, default=8.0) return parser.parse_args() def find_latest_run(compare_root: Path) -> Path: candidates = [path for path in compare_root.iterdir() if path.is_dir()] if not candidates: raise FileNotFoundError(f"No compare runs found under {compare_root}") return max(candidates, key=lambda path: path.stat().st_mtime) def load_json(path: Path) -> dict: return json.loads(path.read_text(encoding="utf-8")) def human_size(num_bytes: int | None) -> str: if num_bytes is None: return "-" value = float(num_bytes) for unit in ["B", "KB", "MB", "GB", "TB"]: if value < 1024.0 or unit == "TB": return f"{value:.1f}{unit}" value /= 1024.0 return f"{num_bytes}B" def probe_duration(path: Path, timeout: float) -> float | None: if not path.is_file() or shutil.which("ffprobe") is None: return None command = [ "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", str(path), ] try: result = subprocess.run(command, capture_output=True, text=True, check=True, timeout=timeout) except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): return None text = result.stdout.strip() if not text: return None try: return float(text) except ValueError: return None def collect_row(run_dir: Path, ffprobe_timeout: float) -> dict: spec_path = run_dir / "spec.json" spec = load_json(spec_path) if spec_path.is_file() else {} planner = str(spec.get("planner") or run_dir.name.split("__", 1)[0]) backend = str(spec.get("backend") or run_dir.name.split("__", 1)[-1]) shared_plan_json = Path(spec.get("shared_plan_json")) if spec.get("shared_plan_json") else None plan_path = run_dir / "plan.json" final_video = run_dir / "final_poetry_teaching.mp4" subtitled_video = run_dir / "final_poetry_teaching_subtitled.mp4" launch_path = run_dir / "launch.sh" segment_videos = sorted((run_dir / "videos").glob("segment_*.mp4")) plan_segments = None if plan_path.is_file(): try: plan_segments = len(load_json(plan_path).get("segments", [])) except Exception: plan_segments = None if final_video.is_file(): status = "done" elif plan_path.is_file() or segment_videos: status = "partial" elif launch_path.is_file(): status = "prepared" else: status = "missing" return { "planner": planner, "image_backend": str(spec.get("image_backend") or "-"), "backend": backend, "status": status, "shared_plan_ready": bool(shared_plan_json and shared_plan_json.is_file()), "plan_ready": plan_path.is_file(), "plan_segments": plan_segments, "segment_videos": len(segment_videos), "final_video": str(final_video) if final_video.is_file() else None, "final_video_size_bytes": final_video.stat().st_size if final_video.is_file() else None, "final_video_duration_seconds": probe_duration(final_video, ffprobe_timeout), "subtitled_video": str(subtitled_video) if subtitled_video.is_file() else None, "run_dir": str(run_dir), } def print_table(rows: list[dict], run_root: Path, manifest: dict | None) -> None: print(f"Compare run: {run_root}") if manifest: print(f"Experiment JSON: {manifest.get('experiment_json', '-')}") print(f"Requested backends: {', '.join(manifest.get('backends_requested', []))}") print(f"Planner variants: {', '.join(manifest.get('planner_variants', []))}") print(f"Image backend: {manifest.get('image_backend', '-')}") print(f"Persistent video backends: {manifest.get('persistent_video_backends')}") print(f"Dry run: {manifest.get('dry_run')}") headers = ["planner", "backend", "image", "status", "shared_plan", "plan", "segments", "duration", "size"] table = [] for row in rows: duration = row["final_video_duration_seconds"] duration_text = f"{duration:.1f}s" if duration is not None else "-" table.append([ row["planner"], row["backend"], row["image_backend"], row["status"], "yes" if row["shared_plan_ready"] else "no", "yes" if row["plan_ready"] else "no", str(row["segment_videos"]), duration_text, human_size(row["final_video_size_bytes"]), ]) widths = [len(header) for header in headers] for line in table: for idx, cell in enumerate(line): widths[idx] = max(widths[idx], len(cell)) header_line = " ".join(header.ljust(widths[idx]) for idx, header in enumerate(headers)) print(header_line) print(" ".join("-" * width for width in widths)) for line, row in zip(table, rows): print(" ".join(cell.ljust(widths[idx]) for idx, cell in enumerate(line))) if row["final_video"]: print(f" final: {row['final_video']}") else: print(f" run: {row['run_dir']}") def main() -> None: args = parse_args() run_root = args.run_root.resolve() if args.run_root else find_latest_run(args.compare_root.resolve()) manifest_path = run_root / "manifest.json" manifest = load_json(manifest_path) if manifest_path.is_file() else None rows = [] for run_dir in sorted(path for path in run_root.iterdir() if path.is_dir() and path.name != "shared_plan"): rows.append(collect_row(run_dir, args.ffprobe_timeout)) if args.json: print(json.dumps({"run_root": str(run_root), "manifest": manifest, "rows": rows}, ensure_ascii=False, indent=2)) else: print_table(rows, run_root, manifest) if __name__ == "__main__": main()