mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +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
@@ -72,6 +72,26 @@ describe("caption rules", () => {
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not warn on a content frame that only mentions karaoke in a comment", async () => {
|
||||
const html = `<template id="06-one-platform-template">
|
||||
<div id="root" data-composition-id="06-one-platform" data-width="1920" data-height="1080">
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
// "Minutes, not weeks" lands with a karaoke-style keyword glow
|
||||
SCREENS.forEach(function (s, i) {
|
||||
var el = document.getElementById("screen-" + i);
|
||||
tl.to(el, { y: -40, opacity: 0, duration: 0.3 }, i * 1.3);
|
||||
});
|
||||
window.__timelines["06-one-platform"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>`;
|
||||
const result = await lintHyperframeHtml(html, { isSubComposition: true });
|
||||
const finding = result.findings.find((f) => f.code === "caption_exit_missing_hard_kill");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("warns when caption group has nowrap without max-width", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
|
||||
@@ -30,8 +30,17 @@ function extractArrayLiteral(src: string, varMatch: RegExpExecArray): string | n
|
||||
|
||||
export const captionRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
// caption_exit_missing_hard_kill
|
||||
({ scripts }) => {
|
||||
({ scripts, styles, options, rootCompositionId }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
// Only the ACTUAL captions composition. A content frame that merely mentions
|
||||
// "karaoke" / "caption-*" in a comment (or uses an unrelated forEach + opacity:0
|
||||
// screen-swap) is NOT captions — gating here prevents the false positive that fired
|
||||
// on a content frame whose only caption signal was a descriptive comment.
|
||||
const isCaptionComposition =
|
||||
Boolean(options.filePath && /caption/i.test(options.filePath)) ||
|
||||
rootCompositionId === "captions" ||
|
||||
styles.some((s) => /\.caption[-_]?(?:group|word|line|block)\b|\.cg-/.test(s.content));
|
||||
if (!isCaptionComposition) return findings;
|
||||
for (const script of scripts) {
|
||||
const content = script.content;
|
||||
const hasExitTween = /\.to\s*\([^,]+,\s*\{[^}]*opacity\s*:\s*0/.test(content);
|
||||
|
||||
@@ -220,4 +220,32 @@ describe("media rules", () => {
|
||||
const finding = result.findings.find((f) => f.code === "imperative_media_control");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("flags <video> inside a sub-composition (media must be a host-root child)", async () => {
|
||||
const html = `<template id="scene-template">
|
||||
<div id="root" data-composition-id="scene" data-width="1920" data-height="1080">
|
||||
<video id="v1" src="clip.mp4" data-start="0" data-duration="5" muted playsinline></video>
|
||||
<script>window.__timelines = window.__timelines || {}; window.__timelines["scene"] = gsap.timeline({ paused: true });</script>
|
||||
</div>
|
||||
</template>`;
|
||||
const result = await lintHyperframeHtml(html, { isSubComposition: true });
|
||||
const finding = result.findings.find((f) => f.code === "media_in_subcomposition");
|
||||
expect(finding).toBeDefined();
|
||||
expect(finding?.severity).toBe("error");
|
||||
expect(finding?.elementId).toBe("v1");
|
||||
expect(finding?.message).toContain("sub-composition");
|
||||
});
|
||||
|
||||
it("does not flag media in a host-root (non-sub) composition", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<video id="v1" src="clip.mp4" data-start="0" data-duration="5" muted playsinline></video>
|
||||
</div>
|
||||
<script>window.__timelines = window.__timelines || {}; window.__timelines["c1"] = gsap.timeline({ paused: true });</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "media_in_subcomposition");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -342,6 +342,29 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
|
||||
return findings;
|
||||
},
|
||||
|
||||
// media_in_subcomposition — <video>/<audio> only render as a DIRECT child of the host
|
||||
// root (index.html). Inside a sub-composition <template> the runtime never seeks/decodes
|
||||
// them, so they render BLANK/black in preview and renders — and the other lint/validate
|
||||
// passes otherwise miss it (only a per-frame snapshot reveals the blank panel).
|
||||
({ tags, options }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
if (!options.isSubComposition) return findings;
|
||||
for (const tag of tags) {
|
||||
if (tag.name !== "video" && tag.name !== "audio") continue;
|
||||
const elementId = readAttr(tag.raw, "id") || undefined;
|
||||
findings.push({
|
||||
code: "media_in_subcomposition",
|
||||
severity: "error",
|
||||
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> is inside a sub-composition. The runtime only drives media that is a DIRECT child of the host root (index.html); media inside a sub-comp <template> is never seeked/decoded and renders BLANK/black in preview and renders.`,
|
||||
elementId,
|
||||
fixHint:
|
||||
"Move the media OUT of the sub-composition: place the <video>/<audio> as a direct child of #root in index.html, positioned over the scene, and drive any per-scene motion on the MAIN timeline at global time (a sub-comp timeline cannot reach host elements). See composition-patterns.md archetype B.",
|
||||
snippet: truncateSnippet(tag.raw),
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
|
||||
// self_closing_media_tag
|
||||
({ source }) => {
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
|
||||
Reference in New Issue
Block a user