mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
* refactor(product-launch-video): restructure onto script-driven architecture Move product-launch-video onto the shared script-driven authoring flow: build-frame remixes a hyperframes-creative preset onto brand tokens, audio routes through the shared hyperframes-media engine, per-preset caption skins, and every frame is authored as a directed shot. Removes the old bespoke scripts (captions/validate/prep/hoist/…) in favour of the shared lib. assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard (reject an empty or markup-less scene file at assembly, before emitting data-composition-src, and re-dispatch) carried onto the restructured reader. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(pr-to-video): restructure onto script-driven architecture Move pr-to-video onto the shared script-driven authoring flow: ingest.mjs folds the gh PR artifacts into the synthetic capture package the shared backend (build-frame / captions / assemble-index) reads, add the mechanism beat, route audio through hyperframes-media, and remix a hyperframes-creative preset onto brand tokens via the shared lib. - Fix skill name: pr-to-video-refactor -> pr-to-video (match directory). - Drop a stale faceless-explainer-refactor reference in an ingest.mjs comment. - assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(faceless-explainer): restructure onto script-driven architecture Move faceless-explainer onto the shared script-driven authoring flow: every visual is invented (typography / abstract graphics / diagram / data-viz) and authored through the shared backend (build-frame remixes a hyperframes-creative preset onto tokens, audio via hyperframes-media, assemble-index builds the standalone index.html) using the shared lib. - Fix skill name: faceless-explainer-refactor -> faceless-explainer (match directory). - assemble-index.mjs keeps upstream #1629's blank/partial scene-file guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(skills): refresh test-skills-fresh.sh workflow roster Update the install-and-verify harness to the current surface: 10 workflows (adds website-to-video, embedded-captions, graphic-overlays, slideshow; drops the removed footage-recut) and refreshed example prompts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(product-launch-video): oxfmt storyboard.mjs Run oxfmt over lib/storyboard.mjs — formatting only, no logic change. Fixes the Format / Preflight CI check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(studio): import commitGsapPositionFromDrag from its actual module The function was split out into gsapDragPositionCommit.ts in #1605, but the test kept importing it from ./gsapDragCommit, which no longer exports it — yielding 'is not a function' at runtime. Import from the correct module. Inherited main breakage (same fix as #1631); fixes the Test CI check on this branch independently of merge order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(hyperframes): refine router skill metadata tags Update the entry router's metadata tags (video / animation / router focus); oxfmt collapses the now-shorter metadata to a single line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): tighten caption comment-strip + document audio --only merge Review follow-ups (#1635): - captions.mjs (x3): the HTML-comment strip used a single global replace, which CodeQL flags as incomplete multi-character sanitization (a nested/partial pair can re-form a marker the single pass misses). Strip in a fixpoint loop instead. Input is preset-library content, not user-controlled, so this is lint- cleanliness, not XSS defense. - audio.mjs (x3): document that fetch-sfx (--only sfx) MERGES into the neutral audio_engine_meta.json sidecar — the engine reads prev and recomputes only the sfx section, so voices/bgm from the generate pass are preserved (review Q). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): remove existsSync->write TOCTOU in workflow scripts Clears the 9 js/file-system-race CodeQL alerts (captions/audio/transitions x3). Each was an existsSync precheck followed by a later write of the same path: - captions.mjs: caption-overrides shim -> atomic writeFileSync({ flag: 'wx' }). - audio.mjs (sync-durations) + transitions.mjs (inject): drop the existsSync precheck and read directly, surfacing the same friendly error from a try/catch on readFileSync — no check->write gap. Behavior is unchanged (same error messages); these are local single-process deterministic scripts so the race was never a real risk, but this clears the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): paint root composition ground color in assemble-index Per-frame roots carry data-start/data-duration and get clip-gated against the global timeline at render, so only the first frame's window overlaps global 0 — a frame's own full-bleed background can't serve as the video ground, and every frame after the first renders on the bare body color (black). Paint the ground on the always-present root composition using the project's frame.md canvas color (the same role the caption skin maps to --cap-canvas); fall back to the body letterbox color when frame.md is absent or has no resolvable ground. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(hyperframes): drop router-tag edit (moved to the foundation PR) The entry SKILL.md is rewritten wholesale by the frame-presets/media foundation PR (#1632); editing it here too guaranteed a merge conflict. Restore this file to main and let the router-tag tweak live with the rewrite in #1632, so the two PRs no longer both touch it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
165 lines
6.1 KiB
JavaScript
165 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
||
// Step 1 — PR fetch (deterministic; runs gh; large-PR-safe; NO scratch dir).
|
||
//
|
||
// Replaces a bare `gh pr view … > capture/pr.json` in the orchestrator. It folds
|
||
// the PR into the two artifacts ingest.mjs consumes:
|
||
// capture/pr.json the gh pr view core (title, body, author, refs, commits,
|
||
// reviews, comments, assignees, +/− stats, …) with its `files`
|
||
// list COMPLETED via a paginated `gh api .../pulls/N/files`
|
||
// call — `gh pr view --json files` truncates at ~100 files, so a
|
||
// big PR would otherwise lose the tail. Commits keep gh pr view's
|
||
// rich `authors[]` (co-authors) — only `files` needs the override.
|
||
// capture/diff.patch the full unified diff (`gh pr diff`).
|
||
//
|
||
// gh runs HERE so auth / not-found / private-repo errors surface with gh's own stderr
|
||
// and exit 1 (the orchestrator then stops). Intermediates are held in memory — this
|
||
// writes ONLY the two files above, so there is no `_ingest_tmp/` scratch to clean up
|
||
// (the previous "let the agent fetch in pieces" approach polluted videos/ and was
|
||
// non-deterministic). ingest.mjs stays a pure offline transform downstream.
|
||
//
|
||
// Usage:
|
||
// node fetch-pr.mjs --pr "<url | owner/repo#N | N>" [--out-dir ./capture]
|
||
//
|
||
// Exit 0 = capture/pr.json + capture/diff.patch written + summary on stdout.
|
||
// Exit 1 = gh not authenticated / PR not found / pr view failed.
|
||
|
||
import { execFileSync } from "node:child_process";
|
||
import { mkdirSync, writeFileSync } from "node:fs";
|
||
import { join, resolve } from "node:path";
|
||
|
||
const argv = process.argv.slice(2);
|
||
const flag = (name, def) => {
|
||
const i = argv.indexOf(`--${name}`);
|
||
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
|
||
};
|
||
function die(msg) {
|
||
console.error(`✗ fetch-pr.mjs: ${msg}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const prRef = flag("pr", null);
|
||
if (!prRef) die('--pr "<url | owner/repo#N | N>" is required');
|
||
const outDir = resolve(flag("out-dir", "./capture"));
|
||
|
||
// Run gh, capture stdout. Returns { ok, stdout, stderr } — never throws (callers
|
||
// decide whether a failure is fatal). 64 MB buffer covers large diffs / file lists.
|
||
function ghTry(args) {
|
||
try {
|
||
const stdout = execFileSync("gh", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
||
return { ok: true, stdout, stderr: "" };
|
||
} catch (e) {
|
||
return {
|
||
ok: false,
|
||
stdout: (e.stdout || "").toString(),
|
||
stderr: (e.stderr || e.message || "").toString().trim(),
|
||
};
|
||
}
|
||
}
|
||
|
||
// ── 0. auth — fail fast with gh's own hint ───────────────────────────────────
|
||
if (!ghTry(["auth", "status"]).ok) {
|
||
die("gh is not authenticated — run: gh auth login");
|
||
}
|
||
|
||
// ── 1. core PR object (gh pr view) ───────────────────────────────────────────
|
||
const FIELDS = [
|
||
"number",
|
||
"title",
|
||
"body",
|
||
"author",
|
||
"url",
|
||
"baseRefName",
|
||
"headRefName",
|
||
"commits",
|
||
"files",
|
||
"additions",
|
||
"deletions",
|
||
"changedFiles",
|
||
"labels",
|
||
"reviews",
|
||
"latestReviews",
|
||
"comments",
|
||
"assignees",
|
||
"reviewDecision",
|
||
"mergedBy",
|
||
].join(",");
|
||
|
||
const view = ghTry(["pr", "view", prRef, "--json", FIELDS]);
|
||
if (!view.ok) die(`gh pr view "${prRef}" failed (auth / not found / private?):\n${view.stderr}`);
|
||
|
||
let pr;
|
||
try {
|
||
pr = JSON.parse(view.stdout);
|
||
} catch (e) {
|
||
die(`gh pr view returned unparseable JSON (${e.message})`);
|
||
}
|
||
|
||
// ── 2. complete the files list via paginated gh api (the truncation fix) ──────
|
||
// gh pr view --json files caps at ~100 files; the REST endpoint paginates with no
|
||
// cap. --jq runs per page, so the output is NDJSON (one file object per line).
|
||
const number = pr.number;
|
||
const m = /github\.com\/([^/]+)\/([^/]+)\/pull\/\d+/.exec(pr.url || "");
|
||
const owner = m?.[1];
|
||
const repo = m?.[2];
|
||
let filesNote = `${Array.isArray(pr.files) ? pr.files.length : 0} (from pr view)`;
|
||
if (owner && repo && number != null) {
|
||
const apiFiles = ghTry([
|
||
"api",
|
||
"--paginate",
|
||
`repos/${owner}/${repo}/pulls/${number}/files`,
|
||
"--jq",
|
||
".[] | {path: .filename, additions, deletions, status}",
|
||
]);
|
||
if (apiFiles.ok) {
|
||
const files = apiFiles.stdout
|
||
.split("\n")
|
||
.filter(Boolean)
|
||
.map((l) => {
|
||
try {
|
||
return JSON.parse(l);
|
||
} catch {
|
||
return null;
|
||
}
|
||
})
|
||
.filter(Boolean);
|
||
if (files.length) {
|
||
pr.files = files;
|
||
if (pr.changedFiles == null || files.length > pr.changedFiles) pr.changedFiles = files.length;
|
||
filesNote = `${files.length} (completed via gh api)`;
|
||
}
|
||
} else {
|
||
console.error(
|
||
` (warn: gh api files failed — keeping pr view's files: ${apiFiles.stderr.split("\n")[0]})`,
|
||
);
|
||
}
|
||
} else {
|
||
console.error(" (warn: could not parse owner/repo from PR url — keeping pr view's files)");
|
||
}
|
||
|
||
// ── 3. write capture/pr.json + capture/diff.patch ────────────────────────────
|
||
mkdirSync(outDir, { recursive: true });
|
||
const prJsonPath = join(outDir, "pr.json");
|
||
writeFileSync(prJsonPath, JSON.stringify(pr, null, 2) + "\n");
|
||
|
||
const diff = ghTry(["pr", "diff", prRef]);
|
||
const diffPath = join(outDir, "diff.patch");
|
||
if (diff.ok) {
|
||
writeFileSync(diffPath, diff.stdout);
|
||
} else {
|
||
// The brief still builds without the diff (ingest treats it as optional), so this
|
||
// is a warning, not fatal — but surface it.
|
||
console.error(
|
||
` (warn: gh pr diff failed — brief builds without it: ${diff.stderr.split("\n")[0]})`,
|
||
);
|
||
}
|
||
|
||
// ── 4. summary ───────────────────────────────────────────────────────────────
|
||
const repoLabel = owner && repo ? `${owner}/${repo}` : "(repo?)";
|
||
console.log(
|
||
[
|
||
`✓ fetch-pr: ${repoLabel} PR #${number ?? "?"} — "${(pr.title || "").slice(0, 72)}"`,
|
||
` files: ${filesNote}; diff: ${diff.ok ? `${diff.stdout.length} chars` : "MISSING"}`,
|
||
` wrote ${prJsonPath}${diff.ok ? ` + ${diffPath}` : ""}`,
|
||
].join("\n"),
|
||
);
|