#!/usr/bin/env python3 """Build a public-safe final video publishing kit for Devpost submission.""" from __future__ import annotations import argparse import json from datetime import datetime, timezone from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] SCHEMA = "proofframe.final_video_publish_kit.v1" DEFAULT_JSON = ROOT / "docs" / "assets" / "final-video-publish-kit.json" DEFAULT_MD = ROOT / "docs" / "assets" / "final-video-publish-kit.md" PUBLIC_DEMO_URL = "https://adjcjh-backblaze-proofframe.hf.space/?judge=1" REPOSITORY_URL = "https://github.com/adjcjh777/backblaze-proofframe" ALLOWED_HOSTS = ["YouTube", "Vimeo", "Youku"] def utc_now() -> str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") def load_json(path: Path) -> dict[str, Any]: try: data = json.loads(path.read_text(encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError): return {} return data if isinstance(data, dict) else {} def source_summary(root: Path, relative_path: str, schema: str) -> dict[str, Any]: data = load_json(root / relative_path) return { "path": relative_path, "present": bool(data), "schema": data.get("schema"), "schema_ok": data.get("schema") == schema, "mode": data.get("mode"), "ok": data.get("ok"), "safe_to_submit": data.get("safe_to_submit"), } def build_chapters(storyboard: dict[str, Any]) -> list[dict[str, Any]]: shots = storyboard.get("shots") if not isinstance(shots, list) or not shots: shots = [ {"id": "opening", "title": "ProofFrame judge-mode slate", "seconds": 20}, {"id": "workflow", "title": "Generate and review a provenance packet", "seconds": 55}, {"id": "evidence", "title": "Manifest, checksum, storage route, and evidence ZIP", "seconds": 35}, {"id": "gates", "title": "Final B2, Genblaze, video, and audit gates", "seconds": 25}, ] chapters: list[dict[str, Any]] = [] cursor = 0 for shot in shots: if not isinstance(shot, dict): continue seconds = int(shot.get("seconds") or shot.get("duration_seconds") or 0) title = str(shot.get("title") or shot.get("screen") or shot.get("id") or "ProofFrame section") chapters.append( { "timecode": f"{cursor // 60}:{cursor % 60:02d}", "title": title, "duration_seconds": seconds, } ) cursor += max(0, seconds) return chapters def description_text(*, public_demo_url: str, repository_url: str, claim_boundary: str) -> str: return "\n".join( [ "ProofFrame is a provenance-first media operations desk for generated campaign assets.", "", "This demo walks through one creator workflow: open the judge-mode demo, create a media packet, inspect prompt/provider/model/storage/checksum metadata, approve an asset, and download the evidence ZIP.", "", f"Public demo: {public_demo_url}", f"Repository: {repository_url}", "", "What to look for:", "- Generated media is reviewed as an evidence packet, not a loose image.", "- Prompt, provider/model metadata, storage pointer, checksum, approval state, and risk note stay together.", "- Optional Backblaze B2 and Genblaze paths are implemented and fail closed until live proof is captured.", "", f"Claim boundary: {claim_boundary}", ] ) def build_upload_checklist(public_video_check: dict[str, Any]) -> list[dict[str, Any]]: url_analysis = public_video_check.get("url_analysis") or {} return [ { "id": "host_family", "label": "Upload to an official public video host", "ok": bool(url_analysis.get("official_host")), "detail": "Allowed host families: YouTube, Vimeo, or Youku.", }, { "id": "public_visibility", "label": "Use public or unlisted visibility that Devpost judges can access", "ok": bool(public_video_check.get("safe_to_submit")), "detail": "The final URL must be reachable without login, cookies, tokens, or signed query parameters.", }, { "id": "duration", "label": "Keep the final video under the event time limit", "ok": True, "detail": "Use the generated storyboard and chapter plan; rerun public_video_check after upload.", }, { "id": "devpost_field", "label": "Paste the final video URL into Devpost", "ok": bool(public_video_check.get("safe_to_submit")), "detail": "After upload, set PROOFFRAME_PUBLIC_VIDEO_URL and regenerate final reports.", }, ] def source_is_final_ready(source: dict[str, Any]) -> bool: return bool(source["schema_ok"] and source.get("ok") is True and source.get("safe_to_submit") is True) def final_control_is_ready(source: dict[str, Any]) -> bool: return bool(source["schema_ok"] and source.get("ok") is True and source.get("safe_to_submit") is True) def build_kit(root: Path = ROOT) -> dict[str, Any]: root = root.resolve() packet = load_json(root / "docs" / "assets" / "devpost-submission-packet.json") storyboard = load_json(root / "docs" / "assets" / "demo-storyboard.json") draft = load_json(root / "docs" / "assets" / "demo-video-draft.json") public_video_check = load_json(root / "docs" / "assets" / "public-video-check.json") evidence_index = load_json(root / "docs" / "assets" / "judge-evidence-index.json") title = "ProofFrame: Provenance-first generated media vault" claim_boundary = ( "Use this title and description for final upload only after live B2 and Genblaze proof are recorded; " "until then, the public demo remains local/mock and safe_to_submit=false." ) sources = { "storyboard": source_summary(root, "docs/assets/demo-storyboard.json", "proofframe.demo_storyboard.v1"), "draft_video": source_summary(root, "docs/assets/demo-video-draft.json", "proofframe.demo_video_draft.v1"), "public_video_check": source_summary(root, "docs/assets/public-video-check.json", "proofframe.public_video_check.v1"), "final_control": source_summary(root, "docs/assets/final-submission-control.json", "proofframe.final_submission_control.v1"), "evidence_index": source_summary(root, "docs/assets/judge-evidence-index.json", "proofframe.judge_evidence_index.v1"), } ok = all(source["schema_ok"] for source in sources.values()) and bool(evidence_index.get("ok")) video_ready = source_is_final_ready(sources["public_video_check"]) control_ready = final_control_is_ready(sources["final_control"]) safe_to_submit = bool(ok and video_ready and control_ready) return { "schema": SCHEMA, "created_at": utc_now(), "ok": ok, "mode": "public_video_ready" if video_ready else "ready_for_final_upload", "safe_to_share": ok, "safe_to_submit": safe_to_submit, "final_video_ready": video_ready, "allowed_hosts": ALLOWED_HOSTS, "video_url": public_video_check.get("video_url") or packet.get("video_url"), "title": title, "description": description_text( public_demo_url=packet.get("demo_url") or PUBLIC_DEMO_URL, repository_url=packet.get("repository_url") or REPOSITORY_URL, claim_boundary=claim_boundary, ), "chapters": build_chapters(storyboard), "devpost_field": { "field_id": "video_url", "value": public_video_check.get("video_url") if video_ready else "TBD after final upload.", "ready": video_ready, "source": "docs/assets/public-video-check.json", }, "upload_checklist": build_upload_checklist(public_video_check), "source_reports": sources, "draft_video": { "path": draft.get("video_path"), "public_url": draft.get("public_video_draft_url"), "safe_to_submit": draft.get("safe_to_submit"), "final_video_ready": draft.get("final_video_ready"), }, "evidence_index_url": ( "https://huggingface.co/spaces/ADJCJH/backblaze-proofframe/raw/main/" "docs/assets/judge-evidence-index.md" ), "claim_boundary": claim_boundary, "next_actions": [ "Record the final narrated video after live B2 and Genblaze proof is visible.", "Upload to YouTube, Vimeo, or Youku with public or unlisted judge-accessible visibility.", "Set PROOFFRAME_PUBLIC_VIDEO_URL to the final URL and run scripts/public_video_check.py --verify-url --strict-final.", "Regenerate Devpost form kit, final control, submission audit, preview, and bundle before submit.", ], } def render_markdown(kit: dict[str, Any]) -> str: lines = [ "# ProofFrame Final Video Publish Kit", "", f"Mode: `{kit['mode']}`", f"OK: `{str(kit['ok']).lower()}`", f"Safe to submit: `{str(kit['safe_to_submit']).lower()}`", f"Final video ready: `{str(kit['final_video_ready']).lower()}`", f"Created: `{kit['created_at']}`", "", "## Upload Copy", "", f"Title: {kit['title']}", "", "Description:", "", "```text", kit["description"], "```", "", "## Chapters", "", ] lines.extend( f"- `{chapter['timecode']}` {chapter['title']} ({chapter['duration_seconds']}s)" for chapter in kit["chapters"] ) lines.extend(["", "## Upload Checklist", ""]) for item in kit["upload_checklist"]: status = "OK" if item["ok"] else "PENDING" lines.append(f"- {status} `{item['id']}`: {item['label']} - {item['detail']}") lines.extend( [ "", "## Devpost Field", "", f"- Field: `{kit['devpost_field']['field_id']}`", f"- Ready: `{str(kit['devpost_field']['ready']).lower()}`", f"- Value: `{kit['devpost_field']['value']}`", "", "## Claim Boundary", "", kit["claim_boundary"], "", "## Next Actions", "", ] ) lines.extend(f"- {action}" for action in kit["next_actions"]) lines.append("") return "\n".join(lines) def write_outputs(kit: 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(kit, indent=2) + "\n", encoding="utf-8") markdown_path.write_text(render_markdown(kit), encoding="utf-8") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Build the ProofFrame final video publishing kit.") parser.add_argument("--root", type=Path, default=ROOT) parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON) parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MD) parser.add_argument("--strict-final", action="store_true", help="Fail unless the final video URL is ready.") return parser def main() -> None: args = build_parser().parse_args() kit = build_kit(args.root) write_outputs(kit, args.json_out, args.markdown_out) print( json.dumps( { "ok": kit["safe_to_submit"] if args.strict_final else kit["ok"], "mode": kit["mode"], "safe_to_submit": kit["safe_to_submit"], "final_video_ready": kit["final_video_ready"], "json": str(args.json_out), "markdown": str(args.markdown_out), }, indent=2, ) ) if args.strict_final and not kit["safe_to_submit"]: raise SystemExit(2) if not kit["ok"]: raise SystemExit(1) if __name__ == "__main__": main()