pr-to-video
Turn a GitHub pull request (a PR URL, owner/repo#N, or 'this PR' in a checked-out repo) into a code-change explainer video — changelog, feature reveal, fix, or refactor walkthrough built from the diff, commits, and files: the input is a code change, not a website. Not a product promo (/product-launch-video) or a no-PR topic explainer (/faceless-explainer). Unclear → /hyperframes.
1How It Works & Mechanism
Orchestrator pattern: PR-to-video workflow is broken down into 7 sequential steps (Step 0 setup → 1 ingest → 2 design system → 3 storyboard/script → 3.1 audio → 4 visual design → 5 frames → 6 finalize), all executed sequentially by the orchestrator itself except Step 5 which dispatches frame workers, with explicit gates at every step.
User-gated steps are Step 0, Step 3, and Step 6.
Input is strictly defined as a 'code change', not a website: reads PR via gh, contains no capture step, and utilizes no real-world media assets other than contributor avatars.
The input is a **code change** (read via `gh`), not a website — there is **no capture step and no real assets** beyond the contributors' avatars.
Step 1 is deterministic ingestion: fetch-pr.mjs runs gh to fetch core PR objects and uses paginated gh api calls to complete the files list (gh pr view caps at ~100 files), writing only capture/pr.json and capture/diff.patch without temporary directories; for MERGED PRs, it attempts to resolve a real shipped_version.
// gh pr view --json files caps at ~100 files; the REST endpoint paginates with no
Projects are placed in a resolved external directory, never creating videos/ inside the caller's repository: resolves PR reference → parsed by project-dir.mjs (overridable via PR_TO_VIDEO_PROJECT_DIR) → preflight CLI capability checks → runs init only if hyperframes.json is missing.
Never create `videos/` in the caller repository:
Style is fixed to code-editorial without user prompting: build-frame.mjs copies the preset FRAME.md and blends in brand tokens (PR lacks branding → colors:[]/fonts:[]), then duplicates the caption skin and self-validates.
The style is fixed — **code-editorial** (warm editorial; a navy code surface built for diffs).
Narrative discipline in Step 3: sequence stems from narrative design rather than the diff's file order, quantified via 'word budget' (TTS ≈2.2 words/sec, soft target ≤19 words/≤9s per frame, hard cap >26 words/>12s, full video sweet spot ~30–90s).
Scene order comes from narrative design, not from the diff's file order or the commit list.
Code beats are delegated to registry code-* primitives (code-diff / code-morph / code-typing / code-highlight, etc.), requiring inspection of the live catalog before writing manually; non-code 'mechanism beats' require invented animations/flowcharts/data charts.
**Reach for one of these first** for any code beat; fall back to hand-authored composition only when none fits.
Step 5 uses bounded parallelism: frame packets are generated by frame-packets.mjs (hard failure for code frames missing ### Source excerpt, with strict byte limits), dispatching to a maximum of 3 workers where each worker reads only its assigned packet and frame.md; frame failures trigger at most one retry per frame with concrete findings attached.
Dispatch **at most three workers total**, balanced across the packet paths
Validation phase is restrained: lint + check + snapshot contact sheet, with documentation pre-identifying a known false positive (caption highlight text_box_overflow 1–4px) that must not be pursued; rendering strictly requires user approval.
**Known false-positive — do not chase it.**
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| cli | GitHub CLI(gh):pr view / pr diff / api 分页 / users/<login> 取显示名——摄取 PR 的唯一通道 |
| api | GitHub 用户 API(解析 null 显示名,供 credits 帧旁白念姓名) |
| network | 贡献者头像下载(白名单仅 GitHub 头像主机;github.com/<login>.png 302 到 avatars.githubusercontent.com) |
| cli | 官方 hyperframes CLI:init / add / catalog / lint / check / snapshot / preview / render(--skill=pr-to-video) |
| api | HeyGen 音频后端(BGM 检索;TTS 由同一个共享音频引擎承担,signed in 时走 HeyGen、离线走本地引擎) |
| package | 兄弟 skill 依赖(被指名调用):/media-use(BGM/SFX/图像/logo 来源与音频引擎)、/hyperframes、/hyperframes-core、/hyperframes-animation(rules/blueprints)、/hyperframes-creative(帧 preset) |
| network | CDN 资源(产物期):组装的 index.html/captions.html 引用 GSAP,带 SRI 与 crossorigin |
| package | 共享音频引擎路径可用环境变量覆盖(HF_MEDIA_ENGINE),默认指向 /media-use 的 audio.mjs |
4Recommended Governance & Evidence
- scripts/: Execute local script or command: node <SKILL_DIR>/scripts/project-dir.mjs --pr "$PR" (resolves project directory; configurable via PR_TO_VIDEO_P
- 网络端点: Declare external network access: GitHub: all PR reads go through gh (github.com + api.github.com), gh handles auth and retries; large PR file lists use paginated API calls
- 本地工作目录: File I/O and artifact output: Within PROJECT_DIR (never in caller repo): capture/{pr.json,diff.patch,extracted/{tokens.json,
5Second-Pass Verification
- [ok] gh 凭证是否真被使用(橙档证据之一) — fetch-pr.mjs 顶部注释写明「gh runs HERE so auth / not-found / private-repo errors surface with gh's own stderr and exit 1」,实现里第一步就是 `if (!ghTry(["auth", "status"]).ok) { die("gh is not authenticated — run: gh auth login"); }`,随后 pr view / api 分页 / pr diff 三处都用同一 execFileSync 包装。调用点真实存在。
- [ok] HeyGen 凭证与 TTS/BGM 实现到底在谁身上(避免把别家行为算进档位) — 本 skill 的 scripts/audio.mjs 头注释自述「The TTS / BGM / SFX implementation no longer lives here: it is the shared engine at ../../media-use/audio/scripts/audio.mjs」,实现只做 SCRIPT/STORYBOARD → audio_request.json 的映射与回写;凭证读取发生在被指名的姊妹 skill 内。按范式「他人 skill 被指名调用只写进 reason、不作档位依据」,橙档依据只用本目录自身可取证的两条:SKILL.md 明写音频共用 ~/.heygen 凭证,以及 fetch-pr.mjs 的 gh 凭证依赖。
- [ok] 头像下载的防护是否真实(是否只是口号) — fetch-people-avatars.mjs 里有 AVATAR_HOSTS 白名单 + isAllowedAvatarUrl(拒绝非 GitHub 主机)与 isUnderProject(写入路径必须在项目目录内,防 ../../ 逃逸),并以 softExit 保证任何失败都 exit 0;注释解释了 github.com/<login>.png 302 到 avatars.githubusercontent.com 属同主控重定向。防护与注释一致,非表面文章。
- [discrepancy] 「无 asset-staging 步骤」的声明与随包代码是否一致 — SKILL.md Step 4 明写 `There is **no asset-staging step** — the only real assets are the credits avatars, already in `assets/`。`,但 scripts/lib/assets.mjs 的头注释仍写着「Shared by stage-assets.mjs (Step 4 close, BEFORE the frame workers run) and assemble-index.mjs (Step 5, idempotent backstop)」,而 **pr-to-video 目录内并不存在 stage-assets.mjs**(该脚本只存在于同仓 product-launch-video 与 music-to-video),assemble-index.mjs 第 521 行也仍在注释里引用它。结论:这是复用姊妹 skill 时残留的过期注释(audio.mjs 自述该适配层在复用者之间「intentionally identical」,可解释来源);实际行为与 SKILL.md 一致(只有 assemble-index 作为幂等兜底暂存头像),但读者若去找 Step 4 的 stage-assets 会找不到——对 pin commit 有效的真实出入。
- [ok] 是否漏掉未声明的网络调用 — 全目录 URL 命中仅 5 处:两处 gsap CDN(assemble-index.mjs 与 captions.mjs,产物期)、一处 ingest.mjs 的 `https://github.com/${login}.png?size=200` 头像 URL、两处测试文件里的 PR fixture URL;fetch( 仅出现在 fetch-people-avatars.mjs(带白名单);无其它隐藏端点。
- [ok] 「Step 1 无 capture、只读 gh」与脚本能力是否相符 — fetch-pr.mjs 只写 capture/pr.json 与 capture/diff.patch(头注释明确 no scratch dir),ingest.mjs 是纯离线变换,两者都不抓网页;Quick Reference 亦写明「no Step 1 capture」。与 description「the input is a code change, not a website」一致,无夸大。
6Verdict & Scenarios
8f260d47a576b3e2…