mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
feat(skills): product-launch-video skill + consolidate motion knowledge into hyperframes-animation (#1745)
* feat(skills): product-launch-video + consolidate motion knowledge into hyperframes-animation
- Add the product-launch-video skill: shot-sequence architecture where each
visual frame is a time-coded shot sequence picked from a blueprint menu and
paced to the voiceover (anti-PowerPoint). Includes the frame-worker sub-agent,
story/visual/motion-design references, and audio/captions/transitions/
stage-assets/assemble-index scripts.
- Consolidate motion knowledge in hyperframes-animation as the single source of
truth: promote the updated atomic rules (31 -> 36) and rename product-launch-
video's archetypes into hyperframes-animation blueprints (13 -> 15, replacing
the old set). product-launch-video, faceless-explainer, and pr-to-video now
reference them via ../hyperframes-animation/{rules-index,blueprints-index}.md
and the rules/blueprints dirs. Fixes the discrete-text-sequence broken links;
blueprints no longer ship per-id runnable examples, so example references in
the consumers were dropped.
- Default HeyGen TTS voice to Marcia (deterministic; was the API's first English
voice, which drifts on catalog re-sort). Override with --voice.
- assemble-index pre-assembly frame guards: auto-repair a sub-comp root missing
canvas dims; hard-fail on <video>/<audio> inside a sub-comp; hard-fail on a
timed non-root element missing class="clip" or overlapping same-track clips.
- Lint/CLI: lint media inside sub-compositions as an error; stop false-positive
caption layout/lint findings; contrast/layout-audit skip elements hidden by an
invisible ancestor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(skills): clear CodeQL alerts in assemble-index.mjs
- script/style blanking regex now matches closing tags with trailing
whitespace (</script >, </style >) — js/bad-tag-filter (high).
- drop the existsSync precheck before reading/repairing a frame file; read
directly and handle ENOENT, removing the check->write TOCTOU window —
js/file-system-race (high).
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:
co-authored by
Claude Opus 4.8
parent
413d8187fd
commit
05af482f22
@@ -50,18 +50,38 @@ export const UA_DEFAULT_COLORS = new Set(
|
||||
|
||||
// Pick the brand ACCENT — never by raw chroma alone, never a UA-default link color.
|
||||
// Priority:
|
||||
// 1) with capture colorStats → the colorful color used MOST as an interactive
|
||||
// background (buttons / pills). That is, by definition, the brand action color.
|
||||
// 1) with capture colorStats → the colorful color that RECURS across the UI. The brand
|
||||
// accent shows up in MANY roles (link text + icon + button + badge), whereas a one-off
|
||||
// CTA fill appears in just one. So rank chromatic (chroma>40) candidates by role
|
||||
// diversity first, then total prevalence, then interactive use, then chroma. This keeps
|
||||
// a pervasive brand color (e.g. an indigo used everywhere) ahead of a single bright
|
||||
// button fill (e.g. a lime used once) — the old "top interactiveBg" rule picked the
|
||||
// latter. Requiring interactiveBg>0 is dropped so a text/icon-only accent can still win.
|
||||
// 2) no stats → most chromatic color AFTER removing UA defaults + `exclude`.
|
||||
// A stray default link color (e.g. #0000EE) can win under neither path.
|
||||
export function pickAccent(stats, colors, exclude = []) {
|
||||
const ban = new Set([...exclude, ...UA_DEFAULT_COLORS].map((c) => String(c).toUpperCase()));
|
||||
const ok = (h) => /^#[0-9a-fA-F]{6}$/.test(String(h)) && !ban.has(String(h).toUpperCase());
|
||||
// Prominence rank from the (frequency-ordered) `colors` palette: index 0 = most used.
|
||||
// A saturated color sitting at the TAIL is almost always a one-off (a single CTA fill),
|
||||
// not the brand accent — capture colorStats counts are too sparse to tell these apart
|
||||
// (e.g. Linear's indigo and a lime CTA both register count≈1), but palette ORDER does.
|
||||
const rank = new Map((colors ?? []).map((h, i) => [String(h).toUpperCase(), i]));
|
||||
const prom = (h) => (rank.has(String(h).toUpperCase()) ? rank.get(String(h).toUpperCase()) : 1e9);
|
||||
if (Array.isArray(stats) && stats.length) {
|
||||
const roles = (s) =>
|
||||
((s.interactiveBg || 0) > 0 ? 1 : 0) +
|
||||
((s.textCount || 0) > 0 ? 1 : 0) +
|
||||
((s.bgCount || 0) > 0 ? 1 : 0);
|
||||
const a = stats
|
||||
.filter((s) => ok(s?.hex) && (s.interactiveBg || 0) > 0 && chroma(s.hex) > 40)
|
||||
.filter((s) => ok(s?.hex) && chroma(s.hex) > 40)
|
||||
.sort(
|
||||
(x, y) => (y.interactiveBg || 0) - (x.interactiveBg || 0) || chroma(y.hex) - chroma(x.hex),
|
||||
(x, y) =>
|
||||
roles(y) - roles(x) || // used in MORE roles (link+icon+button) = the brand accent
|
||||
prom(x.hex) - prom(y.hex) || // earlier in the palette = more prominent
|
||||
(y.count || 0) - (x.count || 0) ||
|
||||
(y.interactiveBg || 0) - (x.interactiveBg || 0) ||
|
||||
chroma(y.hex) - chroma(x.hex),
|
||||
);
|
||||
if (a.length) return a[0].hex;
|
||||
}
|
||||
@@ -77,7 +97,7 @@ export function pickAccent(stats, colors, exclude = []) {
|
||||
// are unusable, so the caller can fall back. canvas = the color painting the most real
|
||||
// background area (the page ground, dark or light); ink = the dominant text color that
|
||||
// actually contrasts with the canvas; accent via pickAccent.
|
||||
export function brandRolesFromStats(stats) {
|
||||
export function brandRolesFromStats(stats, colorsInOrder) {
|
||||
if (!Array.isArray(stats) || !stats.length) return null;
|
||||
const v = stats.filter((s) => /^#[0-9a-fA-F]{6}$/.test(s?.hex || ""));
|
||||
if (!v.length) return null;
|
||||
@@ -87,11 +107,9 @@ export function brandRolesFromStats(stats) {
|
||||
(b.maxArea || 0) - (a.maxArea || 0) ||
|
||||
(b.bgCount || 0) - (a.bgCount || 0),
|
||||
)[0]?.hex;
|
||||
const accent = pickAccent(
|
||||
v,
|
||||
v.map((s) => s.hex),
|
||||
[canvas],
|
||||
);
|
||||
// pass the frequency-ordered palette (tokens.colors) so pickAccent can use palette
|
||||
// PROMINENCE — colorStats counts alone are too sparse to rank rare accents.
|
||||
const accent = pickAccent(v, colorsInOrder ?? v.map((s) => s.hex), [canvas]);
|
||||
if (!canvas || !accent) return null;
|
||||
const cl = lum(canvas) ?? 0;
|
||||
const ink =
|
||||
@@ -170,5 +188,17 @@ export function parseFonts(md) {
|
||||
roles.title ??
|
||||
roles.hero ??
|
||||
body;
|
||||
return { display: q(display), body: q(body) };
|
||||
// the monospace / chrome family (code, tags, ticks, page numbers) — so the remix can
|
||||
// route a captured brand mono (Berkeley Mono, JetBrains Mono…) onto this role instead
|
||||
// of the reading body. null when the preset has no distinct mono role.
|
||||
const mono =
|
||||
roles.mono ??
|
||||
roles["mono-tag"] ??
|
||||
roles["mono-chrome"] ??
|
||||
roles["mono-tick"] ??
|
||||
roles.code ??
|
||||
roles.data ??
|
||||
roles.pagenum ??
|
||||
null;
|
||||
return { display: q(display), body: q(body), mono: q(mono) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user