mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(skills): unify bgm-to-video flows into music-to-video
Replace bgm-to-video, bgm-to-video-new, bgm-to-video-refactor, and the standalone beat-sync/montage skills with a single music-to-video skill. 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
0dcf914e14
commit
a34d3dba4d
@@ -37,6 +37,32 @@ function isCompositionRootOrMount(rawTag: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
// Top-level CSS selectors (comma-split) in a stylesheet, skipping at-rule headers
|
||||
// (@media/@keyframes/...) and keyframe stops. Heuristic — the lint layer has no
|
||||
// full CSS parser, and rules elsewhere in this file scan CSS the same way.
|
||||
function extractCssSelectors(css: string): string[] {
|
||||
const out: string[] = [];
|
||||
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
const ruleHeader = /([^{}]+)\{/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = ruleHeader.exec(noComments)) !== null) {
|
||||
const header = (m[1] ?? "").trim();
|
||||
if (!header || header.startsWith("@")) continue;
|
||||
for (const sel of header.split(",")) {
|
||||
const s = sel.trim();
|
||||
if (s) out.push(s);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Class tokens in a selector's leftmost compound (before the first descendant /
|
||||
// child / sibling combinator). `.frame .title` → ["frame"]; `.a.b > .c` → ["a","b"].
|
||||
function leftmostCompoundClasses(selector: string): string[] {
|
||||
const leftmost = selector.trim().split(/[\s>+~]+/)[0] ?? "";
|
||||
return (leftmost.match(/\.([\w-]+)/g) ?? []).map((c) => c.slice(1));
|
||||
}
|
||||
|
||||
export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
// invalid_capture_path — catches ../capture/ in src/href attributes and scripts.
|
||||
// Sub-compositions live in compositions/ but are served relative to the project
|
||||
@@ -576,4 +602,50 @@ export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
|
||||
// subcomposition_root_styled_by_class
|
||||
// A sub-composition's <style> is scoped at render time to
|
||||
// `[data-composition-id="<id>"] <selector>` so scenes inlined into one document
|
||||
// can't leak styles into each other. A rule whose LEFTMOST selector is the ROOT
|
||||
// element's own class (e.g. `.frame { ... }` on the same element that carries
|
||||
// data-composition-id) therefore becomes a DESCENDANT selector that can never
|
||||
// match the root — the whole scene renders unstyled (tiny text top-left, images
|
||||
// at natural size). lint/validate/inspect evaluate the file in isolation (no
|
||||
// scoping) and Studio previews each scene in its own iframe (no scoping), so the
|
||||
// break is invisible until the composited MP4 render. Style the root via `#root`
|
||||
// (the scoper special-cases the root id) and descendants via plain selectors,
|
||||
// like the registry blocks — the runtime already scopes each scene by id, so a
|
||||
// class namespace on the root is redundant.
|
||||
({ rootTag, rootCompositionId, styles, options }) => {
|
||||
if (!options.isSubComposition) return [];
|
||||
if (isRegistrySourceFile(options.filePath)) return [];
|
||||
if (!rootTag || !rootCompositionId) return [];
|
||||
|
||||
const rootClasses = (readAttr(rootTag.raw, "class") || "").split(/\s+/).filter(Boolean);
|
||||
if (rootClasses.length === 0) return [];
|
||||
|
||||
const offenders: string[] = [];
|
||||
for (const style of styles) {
|
||||
for (const selector of extractCssSelectors(style.content)) {
|
||||
const hitsRoot = leftmostCompoundClasses(selector).some((c) => rootClasses.includes(c));
|
||||
if (hitsRoot && !offenders.includes(selector)) offenders.push(selector);
|
||||
}
|
||||
}
|
||||
if (offenders.length === 0) return [];
|
||||
|
||||
const example = offenders.slice(0, 3).join(", ");
|
||||
return [
|
||||
{
|
||||
code: "subcomposition_root_styled_by_class",
|
||||
severity: "error",
|
||||
message:
|
||||
`Root element has class="${rootClasses.join(" ")}" and is styled by ${offenders.length} rule(s) keyed off that class (e.g. ${example}). ` +
|
||||
`At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the root — the scene renders unstyled (tiny text top-left, full-size images). ` +
|
||||
`lint/validate/inspect and Studio's per-frame iframe preview do not scope, so this passes every static check and looks correct in preview.`,
|
||||
selector: example,
|
||||
fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`) — the runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant and breaks under scoping.`,
|
||||
snippet: truncateSnippet(rootTag.raw),
|
||||
},
|
||||
];
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user