refactor(skills): rebuild faceless-explainer + pr-to-video on the shot-sequence architecture (#1778)

* refactor(skills): rebuild faceless-explainer + pr-to-video on the shot-sequence architecture

Move both skills onto the current shot-sequence authoring architecture and
the latest shared engine, then re-narrate to each domain. The engine fixes
(pre-assembly frame guards + BGM loop-extend in assemble-index, dark-ground
caption contrast, brand-accent selection + mono role in tokens, dark-mode
polarity invert / weight-clamp / icon-font filter in build-frame) had only
landed in one copy; both skills were a generation behind.

- Authoring model: visual-design now writes a time-coded shot sequence (Scene
  windows paced to the voiceover) instead of the older effects-id phased note;
  motion-language carries the move vocabulary + the tightened motion doctrine
  (smooth over bouncy) + the seek-safe core (fromTo entrances, no CSS-transition
  motion); add cut-catalog (within-frame velocity-matched seams); frame-worker
  and SKILL Step 4/5 move to blueprint instantiation + shot-sequence fidelity.
- faceless-explainer: fold the standalone composition.md into visual-design
  (inventing-the-visual / portrait / caption geometry); keep the explainer
  story doctrine; graft cue-segmented VO + candidate-blueprint-from-Step-3.
- pr-to-video: keep the ingest pipeline (fetch-pr / ingest / fetch-people-avatars)
  and code-vocabulary; preserve the code-beat (code-* block as focal, Scenes
  choreograph the surround) and mechanism-beat treatments under the new model.
- Drop stage-assets from both (no captured assets to stage); remove derivation
  references so each skill reads standalone.

bun run scripts/lint-skills.ts passes; all scripts node --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(skills): resync skills-manifest after format pass

The pre-commit skills-manifest hook hashed the skills before the format hook
reformatted cut-catalog.md, so the committed manifest lagged the on-disk
content and CI's "Skills: manifest in sync" check failed. Regenerated.

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-30 00:02:24 +08:00
committed by GitHub
co-authored by Claude Opus 4.8
parent c811a2750a
commit 7cb8386539
29 changed files with 2123 additions and 779 deletions
+20 -3
View File
@@ -36,7 +36,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from
import { dirname, join, resolve } from "node:path";
import { parseStoryboard } from "./lib/storyboard.mjs";
import { captionBand, parseFormat } from "./lib/dimensions.mjs";
import { parseColors, parseFonts, semanticColors } from "./lib/tokens.mjs";
import { parseColors, parseFonts, semanticColors, lum } from "./lib/tokens.mjs";
const flag = (argv, name, def) => {
const i = argv.indexOf(`--${name}`);
@@ -276,6 +276,20 @@ function buildFromSkin(skin, groups, total, W, H, tokens, die, faces = "", fonts
// overflow:hidden — nothing is clipped); zeroing it would need an airy line-height that
// balloons the pill, which is worse. Override only if a brand font genuinely clips.
out += "\n<style>\n .caption-line { line-height: 1.1 !important; }\n</style>";
// dark-ground caption contrast (auto). The preset caption skins are tuned for a LIGHT
// pill (cream); on a dark brand ground the pill goes near-black, so the skin's faint
// upcoming-word mix and light highlight block turn unreadable. When the resolved caption
// canvas is dark, override the three word states: a brighter muted upcoming color + an
// on-brand ACCENT highlight block with light text. Light grounds are left untouched.
const capCanvas = (tokens.match(/--cap-canvas:\s*(#[0-9a-fA-F]{6})/) || [])[1];
if (capCanvas && (lum(capCanvas) ?? 255) < 90) {
out +=
"\n<style>\n" +
" .caption-word { color: color-mix(in srgb, var(--cap-ink) 64%, var(--cap-canvas)); }\n" +
" .caption-word.is-active { color: var(--cap-ink); background: var(--cap-accent); box-shadow: 0 0 0 0.06em var(--cap-accent); }\n" +
" .caption-word.is-spoken { color: var(--cap-ink); background: transparent; box-shadow: none; }\n" +
"</style>";
}
return `<template id="captions-template" data-composition-id="captions" data-width="${W}" data-height="${H}">\n${out.trim()}\n</template>\n`;
}
@@ -291,8 +305,11 @@ function brandFontFaces(framePath, hyperframesDir) {
];
if (!families.length) return "";
const dirs = [
{ abs: join(hyperframesDir, "assets/fonts"), rel: "../assets/fonts" },
{ abs: join(hyperframesDir, "capture/assets/fonts"), rel: "../capture/assets/fonts" },
// ROOT-RELATIVE — compositions are served with the project root as their base URL, so a
// "../" prefix escapes the root (lint: invalid_parent_traversal_in_asset_path) and 404s in
// Studio/preview. Mirror what the frame workers use for images.
{ abs: join(hyperframesDir, "assets/fonts"), rel: "assets/fonts" },
{ abs: join(hyperframesDir, "capture/assets/fonts"), rel: "capture/assets/fonts" },
].filter((d) => existsSync(d.abs));
const weightOf = (n) => {
const s = n.toLowerCase();