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
+28 -32
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}`);
@@ -243,32 +243,11 @@ function buildFromSkin(skin, groups, total, W, H, tokens, die, faces = "", fonts
prev = out;
out = out.replace(/<!--[\s\S]*?-->/g, "");
}
// brand :root tokens + font loading, both into the reserved hole. Prefer local
// @font-face files (offline-safe, lint-clean). When NONE exist (the common PR case:
// the project uses the preset's own default font and ships no woff2), fall back to a
// Google Fonts @import for the resolved families — otherwise the caption text silently
// drops to the skin's generic fallback (e.g. Fraunces → Georgia), since the renderer's
// auto-resolved font list does not cover preset display fonts. Matches how the frame
// compositions load fonts. @import is valid here because it precedes the :root tokens in
// this <style>. Assumes the family exposes wght 400/500 (true for the variable editorial
// fonts the presets default to); shipping a local woff2 (the faces path) avoids the
// network dependency and the google_fonts_import lint warning entirely.
let fontLoad = faces;
if (!fontLoad) {
const fams = [
...new Set(
[fonts.display, fonts.body].filter(Boolean).map((f) => f.replace(/^"|"$/g, "").trim()),
),
];
if (fams.length) {
const q = fams.map((f) => `family=${f.replace(/\s+/g, "+")}:wght@400;500`).join("&");
fontLoad = ` @import url("https://fonts.googleapis.com/css2?${q}&display=swap");`;
}
}
// brand :root tokens + @font-face for the brand fonts, both into the reserved hole
out = fillOnce(
out,
/<style data-brand-tokens>\s*<\/style>/,
`<style data-brand-tokens>\n${fontLoad ? fontLoad + "\n" : ""}${tokens}\n </style>`,
`<style data-brand-tokens>\n${faces ? faces + "\n" : ""}${tokens}\n </style>`,
"<style data-brand-tokens></style> hole",
);
// Resolve the skin's font-family var()s to the brand family LITERAL. Two reasons:
@@ -297,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`;
}
@@ -312,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();
@@ -370,19 +366,19 @@ function brandFontFaces(framePath, hyperframesDir) {
}
}
}
// Loud signal instead of a silent "". If frame.md named a brand font but no local file
// matched, the caller falls back to a Google Fonts @import (or, failing that, a generic
// font) — surface the cause here at build time rather than letting it surface 2 steps
// later as a font_family_without_font_face lint error disconnected from its root cause.
// Loud signal instead of a silent "". If frame.md named a brand font but no file
// matched, the caption text WILL fall back to a generic font in the render — surface
// the cause here (at build time) rather than letting it surface 2 steps later as a
// font_family_without_font_face lint error disconnected from its root cause.
if (!faces.length) {
const where = dirs.length
? dirs.map((d) => d.rel).join(" / ")
: "assets/fonts or capture/assets/fonts (neither exists)";
console.warn(
` ⚠ captions: frame.md names font ${families.map((f) => `"${f}"`).join(", ")} ` +
`but no matching .woff2/.woff/.ttf/.otf was found in ${where} — falling back to @import/generic ` +
`but no matching .woff2/.woff/.ttf/.otf was found in ${where} captions will fall back ` +
`(text may render in the wrong font). Stage a font file whose name starts with the family ` +
`(e.g. "TT Norms Pro" → TT_Norms_Pro_Bold.woff2) to ship it locally.`,
`(e.g. "TT Norms Pro" → TT_Norms_Pro_Bold.woff2) so it ships with the project.`,
);
}
return faces.join("\n");