#!/usr/bin/env python3 """Build a public-safe mock demo video draft from committed screenshots.""" from __future__ import annotations import argparse import json from pathlib import Path import shutil import subprocess import tempfile from typing import Any, Callable ROOT = Path(__file__).resolve().parents[1] SCHEMA = "proofframe.demo_video_draft.v1" DEFAULT_JSON = ROOT / "docs" / "assets" / "demo-video-draft.json" DEFAULT_MD = ROOT / "docs" / "assets" / "demo-video-draft.md" DEFAULT_VIDEO = ROOT / "docs" / "assets" / "proofframe-demo-draft.mp4" PUBLIC_VIDEO_DRAFT_URL = ( "https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/resolve/main/" "docs/assets/proofframe-demo-draft.mp4" ) MAX_SECONDS = 180 TARGET_WIDTH = 1280 TARGET_HEIGHT = 720 SLIDES = [ { "path": "docs/assets/proofframe-hf-public-smoke.png", "seconds": 10, "title": "Judge-mode public demo", }, { "path": "docs/assets/proofframe-sponsor-model-smoke.png", "seconds": 12, "title": "Sponsor evidence model", }, { "path": "docs/assets/proofframe-local-ui-smoke.png", "seconds": 12, "title": "Campaign and asset ledger", }, { "path": "docs/assets/proofframe-review-console-smoke.png", "seconds": 12, "title": "Review console", }, { "path": "docs/assets/proofframe-sponsor-model-mobile-smoke.png", "seconds": 8, "title": "Mobile-safe judge proof", }, ] ProbeFn = Callable[[Path], dict[str, Any]] def slide_records(root: Path) -> list[dict[str, Any]]: records = [] for slide in SLIDES: path = root / slide["path"] records.append( { "path": slide["path"], "title": slide["title"], "seconds": slide["seconds"], "present": path.exists(), "bytes": path.stat().st_size if path.exists() else 0, } ) return records def total_seconds() -> int: return sum(int(slide["seconds"]) for slide in SLIDES) def shell_quote_for_concat(path: Path) -> str: return str(path).replace("'", "'\\''") def build_concat_file(root: Path, concat_path: Path) -> None: lines: list[str] = [] for slide in SLIDES: image_path = (root / slide["path"]).resolve() lines.append(f"file '{shell_quote_for_concat(image_path)}'") lines.append(f"duration {int(slide['seconds'])}") last_image = (root / SLIDES[-1]["path"]).resolve() lines.append(f"file '{shell_quote_for_concat(last_image)}'") concat_path.write_text("\n".join(lines) + "\n", encoding="utf-8") def build_video(root: Path, output: Path, *, ffmpeg_bin: str | None = None) -> dict[str, Any]: ffmpeg = ffmpeg_bin or shutil.which("ffmpeg") if not ffmpeg: return {"ok": False, "error": "ffmpeg_not_found", "command": None} missing = [slide["path"] for slide in slide_records(root) if not slide["present"]] if missing: return {"ok": False, "error": "missing_slides", "missing": missing, "command": None} output.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix="proofframe-video-") as tmp_dir: concat_path = Path(tmp_dir) / "slides.txt" build_concat_file(root, concat_path) vf = ( f"scale={TARGET_WIDTH}:{TARGET_HEIGHT}:force_original_aspect_ratio=decrease," f"pad={TARGET_WIDTH}:{TARGET_HEIGHT}:(ow-iw)/2:(oh-ih)/2:color=0x171714," "format=yuv420p" ) command = [ ffmpeg, "-y", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", str(concat_path), "-vf", vf, "-r", "30", "-movflags", "+faststart", str(output), ] completed = subprocess.run(command, cwd=root, text=True, capture_output=True, check=False) return { "ok": completed.returncode == 0, "returncode": completed.returncode, "stderr": completed.stderr.strip(), "command": " ".join(command[:1] + ["..."] + command[-1:]), } def probe_video(path: Path, *, ffprobe_bin: str | None = None) -> dict[str, Any]: if not path.exists(): return {"checked": False, "ok": False, "error": "video_missing"} ffprobe = ffprobe_bin or shutil.which("ffprobe") if not ffprobe: return {"checked": False, "ok": False, "error": "ffprobe_not_found"} command = [ ffprobe, "-v", "error", "-show_entries", "format=duration,size:stream=width,height", "-of", "json", str(path), ] completed = subprocess.run(command, text=True, capture_output=True, check=False) if completed.returncode != 0: return { "checked": True, "ok": False, "error": completed.stderr.strip() or "ffprobe_failed", } try: data = json.loads(completed.stdout) except json.JSONDecodeError: return {"checked": True, "ok": False, "error": "ffprobe_invalid_json"} streams = data.get("streams") or [] first_stream = streams[0] if streams else {} duration = float((data.get("format") or {}).get("duration") or 0) size = int((data.get("format") or {}).get("size") or path.stat().st_size) return { "checked": True, "ok": duration > 0 and size > 0, "duration_seconds": round(duration, 2), "bytes": size, "width": first_stream.get("width"), "height": first_stream.get("height"), "error": None, } def build_report( root: Path = ROOT, *, video_path: Path = DEFAULT_VIDEO, public_url: str = PUBLIC_VIDEO_DRAFT_URL, prober: ProbeFn = probe_video, ) -> dict[str, Any]: root = root.resolve() video_path = video_path.resolve() slides = slide_records(root) missing_slides = [slide["path"] for slide in slides if not slide["present"]] probe = prober(video_path) expected_seconds = total_seconds() duration = probe.get("duration_seconds") duration_ok = isinstance(duration, (int, float)) and 0 < float(duration) <= MAX_SECONDS video_present = video_path.exists() draft_ready = bool(not missing_slides and video_present and probe.get("ok") and duration_ok) if draft_ready: mode = "mock_video_draft_ready" elif missing_slides: mode = "missing_draft_inputs" elif not video_present: mode = "draft_video_missing" elif not probe.get("ok"): mode = "draft_video_unverified" else: mode = "draft_video_too_long" return { "schema": SCHEMA, "mode": mode, "ok": draft_ready, "safe_to_submit": False, "final_video_ready": False, "public_video_draft_url": public_url, "video_path": str(video_path.relative_to(root) if video_path.is_relative_to(root) else video_path), "expected_seconds": expected_seconds, "max_seconds": MAX_SECONDS, "slides": slides, "missing_slides": missing_slides, "video_probe": probe, "checks": [ { "id": "draft_inputs_present", "ok": not missing_slides, "detail": f"{len(slides) - len(missing_slides)} / {len(slides)} slide inputs present.", }, { "id": "draft_video_present", "ok": video_present, "detail": f"Video path is {video_path}.", }, { "id": "draft_under_time_limit", "ok": duration_ok, "detail": f"Duration is {duration}; max is {MAX_SECONDS}.", }, ], "claim_boundary": ( "This MP4 is a mock recording draft for rehearsal and public review. It is not the " "final Devpost video and does not prove live B2 or Genblaze execution." ), "next_actions": [ "Upload or sync this draft only as a mock video reference.", "After B2 and Genblaze live proof, record the final narrated video and set PROOFFRAME_PUBLIC_VIDEO_URL.", "Run scripts/public_video_check.py with --verify-url --strict-final on the final public video URL.", ], } def render_markdown(report: dict[str, Any]) -> str: lines = [ "# ProofFrame Demo Video Draft", "", f"Mode: `{report['mode']}`", f"OK: `{str(report['ok']).lower()}`", f"Safe to submit: `{str(report['safe_to_submit']).lower()}`", f"Final video ready: `{str(report['final_video_ready']).lower()}`", f"Draft URL: {report['public_video_draft_url']}", f"Expected duration: `{report['expected_seconds']}s / {report['max_seconds']}s max`", "", "## Claim Boundary", "", report["claim_boundary"], "", "## Slides", "", ] for slide in report["slides"]: marker = "OK" if slide["present"] else "MISSING" lines.append(f"- {marker} `{slide['path']}` ({slide['seconds']}s): {slide['title']}") probe = report["video_probe"] lines.extend( [ "", "## Video Probe", "", f"- Checked: `{str(probe.get('checked')).lower()}`", f"- OK: `{str(probe.get('ok')).lower()}`", f"- Duration: `{probe.get('duration_seconds')}`", f"- Bytes: `{probe.get('bytes')}`", f"- Size: `{probe.get('width')}x{probe.get('height')}`", f"- Error: `{probe.get('error')}`", "", "## Checks", "", ] ) for check in report["checks"]: marker = "OK" if check["ok"] else "BLOCKED" lines.append(f"- {marker} `{check['id']}`: {check['detail']}") lines.extend(["", "## Next Actions", ""]) lines.extend(f"- {action}" for action in report["next_actions"]) return "\n".join(lines) + "\n" def write_outputs(report: dict[str, Any], json_path: Path, markdown_path: Path) -> None: json_path.parent.mkdir(parents=True, exist_ok=True) markdown_path.parent.mkdir(parents=True, exist_ok=True) json_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") markdown_path.write_text(render_markdown(report), encoding="utf-8") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Build a ProofFrame mock demo video draft report.") parser.add_argument("--root", type=Path, default=ROOT) parser.add_argument("--video-out", type=Path, default=DEFAULT_VIDEO) parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON) parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MD) parser.add_argument("--public-url", default=PUBLIC_VIDEO_DRAFT_URL) parser.add_argument("--build-video", action="store_true", help="Create the MP4 from screenshot slides.") parser.add_argument("--strict", action="store_true", help="Fail unless the draft video is ready.") return parser def main() -> None: args = build_parser().parse_args() if args.build_video: build_result = build_video(args.root.resolve(), args.video_out.resolve()) if not build_result["ok"]: print(json.dumps({"ok": False, "mode": "video_build_failed", **build_result}, indent=2)) raise SystemExit(2) report = build_report( args.root, video_path=args.video_out, public_url=args.public_url, ) write_outputs(report, args.json_out, args.markdown_out) print( json.dumps( { "ok": report["ok"], "mode": report["mode"], "video": str(args.video_out), "json": str(args.json_out), "markdown": str(args.markdown_out), "public_url": report["public_video_draft_url"], }, indent=2, ) ) raise SystemExit(0 if report["ok"] or not args.strict else 2) if __name__ == "__main__": main()