Loading...
Loading...
pr-to-video workflow - a GitHub pull request (URL like github.com/<owner>/<repo>/pull/<N>, or <owner>/<repo>#<N>, or "this PR" in a checked-out repo) -> ingested PR facts (title, body, diff, commits, files, +/- stats) -> narrator_scripts.json + audio (voice + BGM) + section_plan.md -> code-diff / before-after / impact explainer video. Input is a CODE CHANGE. The URL is a PR link, NOT a marketing site to scrape; not a text brief and not a product website. For a non-PR input (product site, general website, topic text), see /hyperframes-read-first.
npx skill4agent add heygen-com/hyperframes pr-to-video<owner>/<repo>#<N>/general-videoghConfirm the route before Step 0. This skill explains a GitHub pull request (a code change read via). If the input is a marketing / product site →gh; a general website →/product-launch-video; a topic / article with no PR →/website-to-video; a whole-repo tour or multi-PR release →/faceless-explainer. Out of scope: live / at-render-time data — PR facts are read once at author time and baked in. Handed a non-PR input, or unsure? Read/general-videofirst./hyperframes-read-first
scripts/agents/phases/<SKILL_DIR>PROJECT_DIR = videos/<project-name>/<SKILL_DIR>/../hyperframes-core/references/subagent-dispatch.md| Phase | Execution | Primary artifact | Detailed flow |
|---|---|---|---|
| init | Bash | | Step 0 |
| ingest (own) | Bash ( | | Step 1 |
| design-system (shared) | Bash (no agent, deterministic | | Step 1b |
| story-design (own) | subagent | | |
| audio (shared) | | | |
| visual-design (shared) | subagent | | |
| prep (shared) | | | |
| captions (shared, det.) | | | |
| scenes (shared) | N x subagent (parallel) | | |
| finalize (shared) | Bash prelude (wait-bgm + assemble + inject/verify-transitions + hoist-videos + sfx-verify + preflight) -> finalize subagent (fix brief findings in place + one lean contact-sheet look + render) | | Step 7 / |
brew install python@3.11 node ffmpeg/usr/bin/python3pip installnpx hyperframes doctorscripts/check-overlap.mjspuppeteer-core--ensure-depspuppeteerghgh auth statushyperframes| Key / requirement | Used for | Default / fallback |
|---|---|---|
| Reading the PR (public or private) | required — fail fast with the auth hint |
| TTS (cloud, word-level timestamps) | voice: auto (first English starfish voice; override |
| TTS (cloud; needs | voice |
| neither, and not logged in | TTS | local Kokoro, voice |
| Lyria BGM | unset -> local MusicGen (first run downloads ~300 MB) |
/hyperframes-read-firstclaude/tmp/pr-video-...PROJECT_DIR = videos/<project-name>/<project-name>Use ./videos/retry-pr<repo>-pr-<N>widgets-pr-1187$PROJECT_DIR/hyperframes.jsonPROJECT_DIR="${PR_VIDEO_DIR:-videos/<project-name>}"
mkdir -p "$(dirname "$PROJECT_DIR")"
npx hyperframes init "$PROJECT_DIR" --non-interactive --skip-skills --example=blankdrops a generichyperframes init/AGENTS.mdintoCLAUDE.md; leave them in place — they are agent scaffolding for whoever opens the finished project later.$PROJECT_DIR
hyperframes initAGENTS.mdCLAUDE.mdhyperframes/PROJECT_DIR(cd "$PROJECT_DIR" && ...)cd$PROJECT_DIR/.envcontext.log$HEYGEN_API_KEY$HYPERFRAMES_API_KEY~/.heygen/credentialshyperframes auth login$PROJECT_DIR/.envKEY=value.envhyperframes auth loginghghingest.mjs# PR ref: a full URL, "<owner>/<repo>#<N>", or "<N>" inside a checked-out repo.
PR="<url | owner/repo#N | N>"
# Fail fast if gh is not authenticated.
gh auth status || { echo "gh not authenticated — run: gh auth login"; exit 1; }
(cd "$PROJECT_DIR" && mkdir -p capture/extracted capture/assets)
(cd "$PROJECT_DIR" && gh pr view "$PR" \
--json number,title,body,author,url,baseRefName,headRefName,commits,files,additions,deletions,changedFiles,labels,reviews,latestReviews,comments,assignees,reviewDecision,mergedBy \
> capture/pr.json)
(cd "$PROJECT_DIR" && gh pr diff "$PR" > capture/diff.patch)
# Fold pr.json + diff.patch into tokens.json (colors:[] → claude native palette) +
# visible-text.txt (the narrative brief) + people.json (PR author + commit authors w/ counts +
# reviewers / commenters / assignees, bot-filtered + deduped, each with a GitHub avatar URL).
# (The PR `author` is only the opener; commit authors from commits[].authors[] are tracked too.)
# ingest is OFFLINE.
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/ingest.mjs \
--pr-json ./capture/pr.json --diff ./capture/diff.patch --out-dir ./capture/extracted)
# Network step (the people front's only one — ingest stays offline): download each
# contributor's GitHub avatar to public/avatars/<login>.png for an optional credits /
# shipped-by close. Best-effort — a missing avatar or offline run never blocks (exit 0).
# Avatars + that close are the ONE place pr-to-video relaxes the faceless default.
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/fetch-people-avatars.mjs \
--people ./capture/extracted/people.json)[ -s "$PROJECT_DIR/capture/pr.json" ] && \
[ -s "$PROJECT_DIR/capture/diff.patch" ] && \
[ -s "$PROJECT_DIR/capture/extracted/tokens.json" ] && \
[ -s "$PROJECT_DIR/capture/extracted/visible-text.txt" ] && \
[ -s "$PROJECT_DIR/capture/extracted/people.json" ] && \
[ -d "$PROJECT_DIR/capture/assets" ] && echo ok || echo missing
# public/avatars/ is best-effort — its absence is NOT a failure (no avatars resolved / offline).ghingest.mjspr.jsonfetch-people-avatars.mjsdesign.htmlcode-windownumber-lockupstat-card(cd "$PROJECT_DIR" && node <SKILL_DIR>/phases/design-system/scripts/build-design.mjs ./design-system --no-emit --style claude)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/phases/design-system/scripts/build-design.mjs ./design-system --style claude)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/phases/design-system/scripts/emit-chunks.mjs ./design-system)[ -s "$PROJECT_DIR/design-system/inference.json" ] && \
[ -s "$PROJECT_DIR/design-system/design.html" ] && \
[ -s "$PROJECT_DIR/design-system/chunks/index.json" ] && echo ok || echo missingagents/story-design.md## Dispatch contextSKILL_DIR: <absolute path>
PROJECT_DIR: <video project root>
Schema validator: <SKILL_DIR>/scripts/validate-narrator.mjs
PR facts: ./capture/pr.json # title / body / commits / files / +/- stats — read first
Diff: ./capture/diff.patch # the actual change — pull 2-4 representative hunks
Brief: ./capture/extracted/visible-text.txt # the assembled narrative brief
People: ./capture/extracted/people.json # contributors (PR author + commit authors w/ commitCount + reviewers/commenters) + avatarFile; avatars in public/avatars/ — optional credits close
Design DNA: ./design-system/inference.json # Read site_dna once to set register (soft hint only)
Orientation: <landscape | portrait | square> # From the Step 0.0 aspect (16:9→landscape, 9:16→portrait, 1:1→square; default landscape). Emit VERBATIM as the top-level `orientation` field — dictated, not a choice; sets the canvas (portrait→1080×1920) for the whole pipeline.
Script style: concise, dev-facing — 1-2 sentences/scene, <=20 words; name the change, the why, the impactnarrativeArchetypechangelogfeature-revealfix-explainerrefactor-walkthrough"<outer> with <inner>"orientationnarrator_scripts.jsoncontinuitycontinuebreakbreakintentsharedMotifassetCandidates[]public/avatars/<login>.pngpeople.jsonnarrator_scripts.json(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/audio.mjs \
--narrator-scripts ./narrator_scripts.json \
--hyperframes . \
--out ./audio_meta.json \
--lyria-recipe <SKILL_DIR>/phases/audio/lyria-recipe.py)GEMINI_API_KEYGOOGLE_API_KEY--lyria-recipeimport google.genaipip install google-genaifacebook/musicgen-smallaudio.mjsaudio_meta.jsonbgm_logbgm_piddesign-system/chunks/index.jsonnarrator_scripts.jsonaudio_meta.json# Dispatch packets live in $PROJECT_DIR/.dispatch/ (transient; safe to delete after the run).
# NEVER use a fixed /tmp path: it persists across runs/projects, so a failed write silently
# reuses another project's stale packet and contaminates every worker.
mkdir -p "$PROJECT_DIR/.dispatch"
DP="$PROJECT_DIR/.dispatch/vd-dispatch.txt"
{
echo "## Design chunks"
(cd "$PROJECT_DIR" && cat design-system/chunks/index.json \
design-system/chunks/composition-hints.md design-system/chunks/voice.md \
design-system/chunks/tokens.css design-system/chunks/easings.js 2>/dev/null)
echo "## Effects catalog"; cat <SKILL_DIR>/phases/visual-design/effects-catalog.md
echo "## Design rules"; cat <SKILL_DIR>/phases/visual-design/rules/{typography,color-system,composition,motion-language}.md
echo "## SFX library"; cat <SKILL_DIR>/assets/sfx/manifest.json
echo "## Narrator scripts"; (cd "$PROJECT_DIR" && cat narrator_scripts.json)
echo "## Audio meta"; (cd "$PROJECT_DIR" && cat audio_meta.json 2>/dev/null) # Optional; overrides Duration if drift >10%
} > "$DP"
# Guard: a partially-failed build must fail LOUDLY here, not downstream in the subagent
grep -q '^## Narrator scripts' "$DP" || { echo "FATAL: vd-dispatch.txt incomplete — rebuild before dispatching"; }
# Captions planning hint (put it in the Captions: line of the dispatch below)
(cd "$PROJECT_DIR" && node -e 'try{const m=require("./audio_meta.json");process.stdout.write(Object.values(m.scenes||{}).some(s=>s.wordsPath)?"enabled":"disabled")}catch{process.stdout.write("enabled")}')agents/visual-design.md## Dispatch contextSKILL_DIR: <absolute path>
PROJECT_DIR: <video project root>
Schema validator: <SKILL_DIR>/scripts/validate-section.mjs
Canvas: <width>×<height> # default 1920×1080 (16:9 landscape); 1080×1920 (9:16 portrait) or 1080×1080 (1:1 square) if requested upstream (narrator_scripts.orientation/dimensions). Plan layouts for THIS aspect ratio — see composition.md "Portrait & Square".
Captions: <enabled | disabled> # Planning hint from the node -e above: enabled => leave the bottom ~17% of canvas height as caption territory in prose
Dispatch packet: <PROJECT_DIR>/.dispatch/vd-dispatch.txt # Step 0 reads it once for all inputs
Visuals: faceless code-change — every scene is a code-window / before-after split / file-tree / +/- counter / diagram / typography invented from the script + the featured diff hunk. assetCandidates is [] for most or all scenes; plan visuals from the script and diff, not from captured assets.section_plan.mdCaptions:group_spec.captions_enabledsection_plan.md(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/prep.mjs \
--section-plan ./section_plan.md \
--narrator-scripts ./narrator_scripts.json \
--audio-meta ./audio_meta.json \
--rules-dir <SKILL_DIR>/../hyperframes-animation/rules \
--capture ./capture \
--design-system ./design-system \
--hyperframes . \
--sfx-lib <SKILL_DIR>/assets/sfx \
--out ./group_spec.json)group_spec.jsonsection_planContinuityvisual_clips[]group_wN.htmltransitions[]capture/assets/prep.mjsis what carries each scene's--audio-meta ./audio_meta.json/voicePathand thewordsPathintobgm_path— and therefore into the assembledgroup_spec. Omitting it (or pointing it at a path whose wavs don't resolve underindex.html) silently blanks every voice / caption / BGM track and renders a SILENT, caption-less video while every gate stays green. prep now defaults this flag to--hyperframesand prints a./audio_meta.jsonbanner whenCRITICALlists voiced scenes but none get wired;audio_metare-asserts the same guard before render. Keep passing the flag explicitly anyway.assemble-index.mjs
context.log(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/captions.mjs group \
--group-spec ./group_spec.json --hyperframes . \
--tokens design-system/chunks/tokens.css --out ./caption_groups.json)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/captions.mjs html \
--hyperframes . --groups ./caption_groups.json \
--tokens design-system/chunks/tokens.css \
--inference design-system/inference.json \
--out compositions/captions.html)captions: skipped (<reason>)captions.htmlcaptions.mjs htmlcaption-skin.html--skin-filenpx hyperframes lintcaptions.htmlPROJECT_DIRnode_modules/node <SKILL_DIR>/scripts/check-overlap.mjs --ensure-deps
# Installs puppeteer-core (module only, no browser download) if not already resolvable; Chrome is
# reused from the hyperframes browser cache. Workers must NOT install it themselves (parallel npm race).group_spec.json.groups[]film directiontokenseasingsvoice# Same rule as Step 4: packets go in $PROJECT_DIR/.dispatch/, never a fixed /tmp path
# (a stale /tmp file from a previous project survives a failed write and silently
# poisons every worker with the wrong design system).
mkdir -p "$PROJECT_DIR/.dispatch/scene-dispatch"
# `## Film direction` = the film-level invariants from group_spec.film_direction
# (palette system / motion defaults + budget / ambient system / negative list);
# each scene's creative_brief carries only scene-specific deltas on top of it.
{
echo "## Film direction"
(cd "$PROJECT_DIR" && node -p 'JSON.parse(require("fs").readFileSync("group_spec.json","utf8")).film_direction || ""')
echo "## Tokens / easings / voice"
(cd "$PROJECT_DIR" && cat design-system/chunks/tokens.css design-system/chunks/easings.js design-system/chunks/voice.md 2>/dev/null)
} > "$PROJECT_DIR/.dispatch/scene-shared.txt"
# Guard BEFORE fan-out: the project's own brand token must be present; a contaminated
# packet here costs a full re-author round across every affected worker.
grep -q -- '--brand-primary' "$PROJECT_DIR/.dispatch/scene-shared.txt" || \
{ echo "FATAL: scene-shared.txt incomplete/stale — rebuild before dispatching workers"; }
# Then per worker: shared header + that worker's Scenes YAML -> $PROJECT_DIR/.dispatch/scene-dispatch/w<N>.txtagents/hyperframes-scene.md## Dispatch contextSKILL_DIRPROJECT_DIRWorker IDComposition widthComposition heightgroup_spec.widthgroup_spec.heightCaptions: <enabled|disabled>group_spec.captions_enabledDispatch packet: <PROJECT_DIR>/.dispatch/scene-dispatch/w<N>.txt## Film direction## Tokens / easings / voiceScenes:captions.mjs keepout --scenecheck-overlap.mjs --scenegroup_spec.json.groups[i]worker_idcomposition_idcomposition_fileduration_sscene_idsgroup_spec.jsonwidthheightCaptions: enabledCaption band top yheight − round(height × 0.1667)Foreground max yCaption band top y − 20Scenes:group_spec.json.groups[i].scenes[<sid>]scene_idlocal_start_seffectsrule_pathsassetCandidatesestimatedDuration_svoicePathdesign_chunkscreative_briefgroup_wN.htmlassetCandidates[]creative_briefdesign_chunks: null./design-system/design.htmlgroup_spec.visual_clips[]captions.html(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/check-compositions.mjs \
--hyperframes . \
--group-spec ./group_spec.json)compositions/scene_N.htmlgroup_wN.html(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/wait-bgm.mjs \
--audio-meta ./audio_meta.json \
--hyperframes . \
--timeout-ms 120000 \
--interval-ms 2000)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/assemble-index.mjs --group-spec ./group_spec.json --hyperframes .)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/transitions.mjs inject --group-spec ./group_spec.json --hyperframes .)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/transitions.mjs verify --group-spec ./group_spec.json --index ./index.html)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/hoist-videos.mjs --group-spec ./group_spec.json --hyperframes .)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/verify-output.mjs sfx --group-spec ./group_spec.json --index ./index.html)
(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/verify-output.mjs audio --hyperframes . --group-spec ./group_spec.json --index ./index.html)injectindex.htmldata-startdata-durationdata-track-indexhoist-videosdata-video-src<video class="clip"><video>data-durationtransitions[]data-video-srcEditnode <SKILL_DIR>/scripts/check-overlap.mjs --ensure-depsbgm.wavcaptions.htmlindex.htmlgroup_spec--audio-meta--audio-meta ./audio_meta.json⚠(cd "$PROJECT_DIR" && node <SKILL_DIR>/scripts/preflight-finalize.mjs --group-spec ./group_spec.json --hyperframes .)finalize_brief.jsonnpx hyperframes@<version>--tolerancedata-layout-allow-overflowcheck-overlap.mjsstatus: unavailablecaptions_enabledcaptions.mjs keepoutmargin-topmargin-bottomedit_oldedit_newpreflight_cleangates_cleangates.*bgm.*overlap.*caption_keepout.*anomalies[]snapshot_times_s[]npx_prefixscenes[]internal_seams[]preflight-finalize.mjsoverlap.violations[]caption_keepout.violations[]overlap.status: "unavailable"node <SKILL_DIR>/scripts/check-overlap.mjs --ensure-depsnpx hyperframes doctorgroup_wN.htmlagents/hyperframes-scene.md## Repair contextnpx_prefixfinalize_brief.jsonInspect at: <t1,t2,t3>midpoint_shigh_risk_extras_sstart_s + 0.5/0.75/0.9 × durationbrief.scenes[]Captions: enabled|disabledinspect --atcheck-overlap.mjs --sceneanomalies[]messageagents/hyperframes-finalize.md## Dispatch contextSKILL_DIR: <absolute path>
PROJECT_DIR: <video project root>
Render quality: high # Or draft / standard
Finalize brief: <PROJECT_DIR>/finalize_brief.json # Preflight has already written it; agent reads once for findings + npx_prefix + scene timings
Film direction: | # = group_spec.film_direction (film-level invariants the briefs assume)
<verbatim>
Visual clips: # One line per group_spec.visual_clips[] entry
- { id, file, kind, worker_id, scene_ids, start_s, duration_s }
Scenes: # One line per logical scene, copied verbatim from group_spec.json
- { scene_id, start_s, estimatedDuration_s, effects: [...], creative_brief: |
<Phase 3 prose for this scene> }index.htmloutput_tailoverlap.violations[]check-overlap --scenecaption_keepout.violations[]edit_oldedit_newdata-durationvisual_clips[].duration_sclaudehyperframes previewplay(cd "$PROJECT_DIR" && npx hyperframes preview) # Studio UI, e.g. http://localhost:3002/#project/<project-name>
# or a lightweight shareable player link instead:
(cd "$PROJECT_DIR" && npx hyperframes play) # plain http://localhost:<port>hyperframes-clireferences/preview-render.md$PROJECT_DIR/context.log| State | Continue from |
|---|---|
| log missing or empty | Full pipeline |
| Step 1 (ingest) |
ingest done, | Step 1b (three deterministic commands) |
| Step 2 (story-design). If the user supplied a final |
| Step 3 (audio) |
| Step 4 (visual-design) |
| Step 5 (prep) |
| Step 5.5+6 (run |
all | Step 7 (rerun assemble + sfx-verify + preflight, overwriting |
| Report completed and stop |
./ # workspace root
├── .claude/skills/
├── node_modules/ package.json
└── videos/<project-name>/ # PROJECT_DIR - HyperFrames project root
├── hyperframes.json context.log
├── capture/ # synthetic package (NOT a scrape) — kept for backend layout compatibility
│ ├── pr.json # gh pr view --json (now incl. reviews / comments / assignees / reviewDecision)
│ ├── diff.patch # gh pr diff (the full change; story-design pulls hunks from here)
│ ├── extracted/ # tokens.json (synthetic) + visible-text.txt (brief) + people.json (contributors)
│ └── assets/ # empty (faceless)
├── design-system/ # build-design outputs: inference.json / design.html / chunks/ / fonts/
├── narrator_scripts.json audio_meta.json section_plan.md group_spec.json
├── public/ assets/ compositions/ snapshots/ # public/avatars/<login>.png — contributor avatars
└── renders/video.mp4owner/repo#Ngithub.com/.../pull/N/general-video/product-launch-video/website-to-video/faceless-explainer/embedded-captions/general-video