"""Interactive review walk: `python -m tools.review_cli`. Thin shell over the tested review core (tools.review). Walks each proposed record showing its mechanical fields, the proposed coordinates + rationale, and a best-effort preview frame, then prompts accept/edit/skip/quit. Approvals are persisted (full rewrite) after each one, so an interrupted session keeps its progress. """ from __future__ import annotations import argparse import os import platform import shutil import subprocess import sys import tempfile from datetime import datetime, timezone from pathlib import Path from hef.catalog import load_catalog, save_catalog, validate_catalog from tools.mediatools import extract_frame from tools.review import approve, proposed_records DEFAULT_CATALOG = "catalog/library.jsonl" DEFAULT_MEDIA_ROOT = "./media" def _now() -> str: return datetime.now(timezone.utc).isoformat() def _build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="tools.review_cli", description="Walk proposed records and approve/correct their coordinates.", ) p.add_argument("--catalog", default=DEFAULT_CATALOG) p.add_argument( "--media-root", default=os.environ.get("HEF_MEDIA_ROOT", DEFAULT_MEDIA_ROOT), ) p.add_argument("--no-preview", action="store_true", help="skip preview rendering") return p def _show(rec, out) -> None: print(f"\n{'=' * 60}", file=out) print(f" id {rec.id}", file=out) print(f" title {rec.title}", file=out) print(f" source {rec.source_archive} {rec.source_url}", file=out) attr = f" ({rec.attribution})" if rec.attribution else "" print(f" license {rec.license}{attr}", file=out) print(f" media mode={rec.mode} {rec.duration_s}s {rec.resolution}", file=out) if rec.dominant_color: print(f" color {rec.dominant_color}", file=out) print( f" proposed left={rec.left} right={rec.right} dark={rec.dark} light={rec.light}", file=out, ) print(f" rationale {rec.rationale}", file=out) if rec.notes: print(f" notes {rec.notes}", file=out) def _open_cmd(): return "open" if platform.system() == "Darwin" else "xdg-open" def _preview(rec, media_root, out) -> None: """Best-effort preview; never fatal. Requires ffmpeg for video frames.""" if not shutil.which("ffmpeg"): return src = Path(media_root) / rec.file_path if not src.exists(): print(f" (preview: media not found at {src})", file=out) return try: midpoint = rec.duration_s / 2 if rec.duration_s else 0.0 if rec.mode in ("video", "av"): dest = Path(tempfile.gettempdir()) / f"hef-preview-{rec.id}.png" extract_frame(src, dest, midpoint_s=midpoint) else: # audio -> waveform thumbnail dest = Path(tempfile.gettempdir()) / f"hef-preview-{rec.id}.png" subprocess.run( [ "ffmpeg", "-v", "quiet", "-y", "-i", str(src), "-filter_complex", "showwavespic=s=640x240", "-frames:v", "1", str(dest), ], check=True, ) opener = shutil.which(_open_cmd()) if opener: subprocess.run([opener, str(dest)], check=False) except Exception as exc: # noqa: BLE001 - preview is best-effort print(f" (preview failed: {exc})", file=out) def _prompt_coords(rec, input_fn, out): from hef.selection import Coordinate def _ask(axis, current): raw = input_fn(f" {axis} [{current}]: ").strip() if not raw: return current try: return max(0, min(4, int(raw))) except ValueError: print(f" (not an int, keeping {current})", file=out) return current return Coordinate( _ask("left", rec.left), _ask("right", rec.right), _ask("dark", rec.dark), _ask("light", rec.light), ) def main(argv=None, *, input_fn=input, now_fn=_now, out=sys.stdout) -> int: args = _build_parser().parse_args(argv) records = load_catalog(args.catalog) validate_catalog(records) pending = proposed_records(records) if not pending: print("no proposed records to review", file=out) return 0 pos = {r.id: i for i, r in enumerate(records)} print(f"{len(pending)} proposed record(s) to review", file=out) for rec in pending: _show(rec, out) if not args.no_preview: _preview(rec, args.media_root, out) choice = input_fn("[a]ccept / [e]dit / [s]kip / [q]uit > ").strip().lower() if choice == "q": break if choice == "s" or choice not in ("a", "e"): continue coordinate = _prompt_coords(rec, input_fn, out) if choice == "e" else None approved = approve(records[pos[rec.id]], reviewed_at=now_fn(), coordinate=coordinate) records[pos[rec.id]] = approved save_catalog(records, args.catalog) print(f" approved {rec.id}", file=out) return 0 if __name__ == "__main__": # pragma: no cover raise SystemExit(main())