refactor(skills): move product-launch / pr-to-video / faceless-explainer onto the script-driven architecture (#1635)

* 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>
This commit is contained in:
WaterrrForever
2026-06-22 22:49:42 +08:00
committed by GitHub
co-authored by Claude Opus 4.8
parent d0f0ec29e7
commit 1967901b57
630 changed files with 8846 additions and 87665 deletions
@@ -1,30 +1,10 @@
// dimensions.mjs — single source of truth for the video canvas size.
//
// ============================================================================
// THE A/B ↔ C SEAM (read this before wiring orientation in upstream)
// ============================================================================
// The whole faceless-explainer pipeline used to be hard-locked to landscape
// 1920×1080. It is now dimension-parametric: every deterministic script
// (assemble-index, transitions, captions, hoist-videos) and every
// scene worker reads the canvas size from ONE place — `group_spec.json`
// `width`/`height` — which prep.mjs stamps in.
//
// prep.mjs resolves the size with this precedence (see resolveDimensions):
// 1. explicit `--width N --height N` flags on prep (testing / override)
// 2. `narrator_scripts.json` → `dimensions: { width, height }` (explicit px)
// 3. `narrator_scripts.json` → `orientation: "landscape|portrait|square"`
// 4. default → landscape 1920×1080 (back-compat)
//
// >>> For whoever wires the intent→orientation entry (Step 0 / scriptwriting):
// do NOT thread pixels through every script. Just write ONE field into
// `narrator_scripts.json` — `orientation` (friendly) or `dimensions`
// (explicit px) — exactly the way `stylePreset` already lives there. prep
// picks it up and the rest of the pipeline follows automatically. If you
// write nothing, the video stays landscape (no behavior change).
// ============================================================================
// dimensions.mjs — canvas size + caption-band geometry for the product-launch
// pipeline. Single source of truth = the STORYBOARD frontmatter `format` global
// ("1920x1080" / "1080x1920" / "1080x1080", or a named orientation). Every
// script and the index assembler reads the size from here; none hardcodes it.
// Named orientation presets. Square/portrait are 1080-based so they share the
// long-edge pixel budget with landscape (same render cost ballpark).
// long-edge pixel budget with landscape (same render-cost ballpark).
export const ORIENTATION_PRESETS = {
landscape: { width: 1920, height: 1080 }, // 16:9 — default
portrait: { width: 1080, height: 1920 }, // 9:16 — reels / shorts / TikTok
@@ -37,47 +17,25 @@ function sane(w, h) {
return Number.isFinite(w) && Number.isFinite(h) && w >= 240 && h >= 240 && w <= 8192 && h <= 8192;
}
// Resolve canvas dims from (in priority order) explicit flags, an explicit
// `dimensions` object, a named `orientation`, else the landscape default.
// `flags` = { width, height } (strings or numbers, may be undefined).
// `narratorScripts` = the parsed narrator_scripts.json (may be null).
// Returns { width, height, source } — `source` is for logging/anomalies.
export function resolveDimensions(flags = {}, narratorScripts = null) {
const fw = flags.width != null ? parseInt(flags.width, 10) : NaN;
const fh = flags.height != null ? parseInt(flags.height, 10) : NaN;
if (sane(fw, fh)) return { width: fw, height: fh, source: "flags" };
const dim = narratorScripts && narratorScripts.dimensions;
if (dim) {
const w = parseInt(dim.width, 10);
const h = parseInt(dim.height, 10);
if (sane(w, h)) return { width: w, height: h, source: "narrator_scripts.dimensions" };
// Parse a STORYBOARD `format` global into { width, height, source }. Accepts
// "WxH" (e.g. "1920x1080"; `x` or `×`, any inner spacing) or a named orientation;
// falls back to landscape so a storyboard with a missing/garbled format still
// renders (no behavior change vs the old landscape lock).
export function parseFormat(format) {
const s = typeof format === "string" ? format.trim().toLowerCase() : "";
if (ORIENTATION_PRESETS[s]) return { ...ORIENTATION_PRESETS[s], source: `orientation=${s}` };
const m = s.match(/^(\d+)\s*[x×]\s*(\d+)$/);
if (m) {
const w = parseInt(m[1] ?? "", 10);
const h = parseInt(m[2] ?? "", 10);
if (sane(w, h)) return { width: w, height: h, source: "format" };
}
const orient =
narratorScripts && typeof narratorScripts.orientation === "string"
? narratorScripts.orientation.trim().toLowerCase()
: "";
if (orient && ORIENTATION_PRESETS[orient]) {
return { ...ORIENTATION_PRESETS[orient], source: `orientation=${orient}` };
}
return { ...DEFAULT_DIMENSIONS, source: "default(landscape)" };
}
// Read dims back out of a group_spec (downstream consumers). Falls back to the
// landscape default so a group_spec produced before this change still works.
export function readDims(groupSpec) {
const w = parseInt(groupSpec?.width, 10);
const h = parseInt(groupSpec?.height, 10);
if (sane(w, h)) return { width: w, height: h };
return { ...DEFAULT_DIMENSIONS };
}
// Caption band geometry, derived from canvas height. The band is the bottom
// ~16.67% of the canvas (matches the original landscape 180px band at h=1080:
// 1080 round(1080 × 0.1667) = 900). Foreground content must end `safetyPx`
// above the band top.
// Caption band geometry, derived from canvas height: the bottom ~16.67% (180px
// at h=1080). Frame content must end `safetyPx` above the band top. Holds even
// when captions are disabled (bottom-edge consistency).
export const CAPTION_BAND_FRACTION = 0.1667;
export function captionBand(height, safetyPx = 20) {
const h = Number.isFinite(height) ? height : DEFAULT_DIMENSIONS.height;