Merge remote-tracking branch 'origin/main' into feat/lint-gsap-non-transform-motion

# Conflicts:
#	skills-manifest.json
This commit is contained in:
Vance Ingalls
2026-07-08 01:36:50 -07:00
913 changed files with 74664 additions and 8626 deletions
+4 -2
View File
@@ -1,6 +1,6 @@
---
name: music-to-video
description: "Use when the user has a music track (an audio file, or a video to pull audio from) and wants a beat-synced HyperFrames video, calm to hard-hitting. The music drives everything: one analyzer reads it once, the orchestrator lays out the frames and fills a per-frame plan, and one sub-agent builds each frame. Typography and templates are the floor — a complete video needs zero assets — but any images or videos the user supplies are cut into the frames on the same beat grid (beat-cut / ken-burns). The genre (lyric video, slideshow, kinetic promo) falls out of the per-frame choices; the pipeline never branches on it."
description: "Turn a music track (an audio file, a video to pull audio from, or a track generated from a mood brief) into a beat-synced video — lyric video, slideshow, or kinetic promo. The music drives all pacing; any user-supplied images/videos are cut onto the same beat grid, and a complete video needs zero assets. Narrated pieces the input-matched workflow (see /hyperframes). Unclear → /hyperframes."
---
# music-to-video — one music-grounded, beat-synced video workflow
@@ -24,7 +24,9 @@ Workflow: Step 0 setup → `hyperframes.json` + `assets/bgm.mp3`; Step 1 analyze
Goal: Establish the music source, create the HyperFrames project, and note any user-supplied media.
The **music is the spine** — establish one track before anything else. This skill is tuned for **fast, high-energy BGM**: a strong beat grid drives the cuts (calm tracks work, but pace by phrase rather than beat). If the user gave you audio — a music file, or a video to pull the audio from — use it. If not, generate one: choose the mood from the user's description (e.g. "driving synthwave", "trap beat", "upbeat corporate") and produce a track via `/hyperframes-media` (`references/bgm.md` — HeyGen retrieval when credentialed, else local Lyria / MusicGen; ElevenLabs or another generator also works). Before generating, run `npx hyperframes auth status` and **relay its output verbatim (don't paraphrase or rewrite it)** — it shows whether BGM comes from HeyGen or local MusicGen and, if not signed in, how to sign in. **If not signed in, STOP and wait for the user to choose — sign in, or continue offline with local MusicGen — before generating the track**; don't write keys into a per-repo `.env`. (In autonomous mode, note the status and continue offline.) See `/hyperframes-media` → Preflight for the canonical guidance. Either way the track lands at `assets/bgm.mp3`. Stage any user-supplied images or videos so frames can weave them in on the beat grid; otherwise typography carries the whole video.
The **music is the spine** — establish one track before anything else. This skill is tuned for **fast, high-energy BGM**: a strong beat grid drives the cuts (calm tracks work, but pace by phrase rather than beat). If the user gave you audio — a music file, or a video to pull the audio from — use it. If not, generate one: choose the mood from the user's description (e.g. "driving synthwave", "trap beat", "upbeat corporate") and produce a track via `/media-use` (`references/bgm.md` — HeyGen retrieval when credentialed, else local Lyria / MusicGen; ElevenLabs or another generator also works). Before generating, run `npx hyperframes auth status` and **relay its output verbatim (don't paraphrase or rewrite it)** — it shows whether BGM comes from HeyGen or local MusicGen and, if not signed in, how to sign in. **If not signed in, STOP and wait for the user to choose — sign in, or continue offline with local MusicGen — before generating the track**; don't write keys into a per-repo `.env`. (In autonomous mode, note the status and continue offline.) See `/media-use` → Preflight for the canonical guidance. Either way the track lands at `assets/bgm.mp3`. Stage any user-supplied images or videos so frames can weave them in on the beat grid; otherwise typography carries the whole video.
**Lyric videos:** for lyrics synced to the vocals, get word/line timing by transcribing the track via `/media-use`, or ask the user for the lyrics text and place lines on the beat grid.
Initialize only if `hyperframes.json` is missing. Name `<project>` from the brief in kebab-case, such as `midnight-drive-loop` — never a timestamp. `init` checks the installed skills against the latest on GitHub and updates the global set if any are out of date.
@@ -236,6 +236,104 @@
"</svg>"
);
}
// Insert SVG markup as SANITIZED parsed nodes — never innerHTML, and never
// a raw appendChild. DOMParser builds the tree, then cleanSvg() hard-strips
// everything that isn't inert drawing: only allow-listed shape elements +
// presentation attributes survive, so <script>, <image>/<use>/<foreignObject>,
// javascript: hrefs and every on* handler are removed before the nodes ever
// enter the live document. A custom icon SVG therefore can't smuggle active
// content (CWE-79 DOM-XSS). Non-SVG / malformed input falls back to text.
var SVG_OK_TAGS = {
svg: 1,
g: 1,
path: 1,
line: 1,
polyline: 1,
polygon: 1,
rect: 1,
circle: 1,
ellipse: 1,
defs: 1,
lineargradient: 1,
radialgradient: 1,
stop: 1,
clippath: 1,
title: 1,
desc: 1,
text: 1,
tspan: 1,
};
var SVG_OK_ATTRS = {
viewbox: 1,
xmlns: 1,
width: 1,
height: 1,
fill: 1,
"fill-rule": 1,
"fill-opacity": 1,
stroke: 1,
"stroke-width": 1,
"stroke-linecap": 1,
"stroke-linejoin": 1,
"stroke-dasharray": 1,
"stroke-dashoffset": 1,
"stroke-opacity": 1,
opacity: 1,
d: 1,
x: 1,
y: 1,
x1: 1,
y1: 1,
x2: 1,
y2: 1,
cx: 1,
cy: 1,
r: 1,
rx: 1,
ry: 1,
points: 1,
transform: 1,
offset: 1,
"stop-color": 1,
"stop-opacity": 1,
gradientunits: 1,
gradienttransform: 1,
"clip-path": 1,
"clip-rule": 1,
class: 1,
id: 1,
};
function cleanSvg(node) {
var attrs = Array.prototype.slice.call(node.attributes || []);
for (var a = 0; a < attrs.length; a++) {
if (!SVG_OK_ATTRS[attrs[a].name.toLowerCase()]) node.removeAttribute(attrs[a].name);
}
var kids = Array.prototype.slice.call(node.childNodes);
for (var k = 0; k < kids.length; k++) {
var c = kids[k];
if (c.nodeType === 1) {
if (SVG_OK_TAGS[(c.localName || c.nodeName).toLowerCase()]) cleanSvg(c);
else node.removeChild(c);
} else if (c.nodeType !== 3) {
node.removeChild(c);
}
}
}
function setSvg(el, markup) {
el.textContent = "";
var doc = new DOMParser().parseFromString(String(markup), "image/svg+xml");
var root = doc.documentElement;
if (
!root ||
root.nodeName.toLowerCase() !== "svg" ||
doc.getElementsByTagName("parsererror").length
) {
el.textContent = String(markup);
return;
}
cleanSvg(root);
el.appendChild(document.importNode(root, true));
}
// ── 4. Default program (reversed from act0-intro-bell) ──
var DEFAULT_PHRASES = [
@@ -381,9 +479,9 @@
glyph.style.height = iconPx + "px";
glyph.style.color = TH.ink;
if (ICONS[iconSpec]) {
glyph.innerHTML = iconSVG(iconSpec); // library icon
setSvg(glyph, iconSVG(iconSpec)); // library icon
} else if (iconSpec.slice(0, 4).toLowerCase() === "<svg") {
glyph.innerHTML = iconSpec; // custom inline SVG
setSvg(glyph, iconSpec); // custom inline SVG
} else {
glyph.style.fontSize = Math.round(iconPx * 0.86) + "px"; // emoji / text glyph
glyph.textContent = iconSpec;
@@ -361,10 +361,109 @@
document.documentElement.style.setProperty("--mark", vars.markColor);
document.documentElement.style.setProperty("--text", vars.textColor);
// Insert SVG markup as SANITIZED parsed nodes — never innerHTML, and never
// a raw appendChild. DOMParser builds the tree, then cleanSvg() hard-strips
// everything that isn't inert drawing: only allow-listed shape elements +
// presentation attributes survive, so <script>, <image>/<use>/<foreignObject>,
// javascript: hrefs and every on* handler are removed before the nodes ever
// enter the live document. A custom mark SVG therefore can't smuggle active
// content (CWE-79 DOM-XSS). Non-SVG / malformed input falls back to text.
var SVG_OK_TAGS = {
svg: 1,
g: 1,
path: 1,
line: 1,
polyline: 1,
polygon: 1,
rect: 1,
circle: 1,
ellipse: 1,
defs: 1,
lineargradient: 1,
radialgradient: 1,
stop: 1,
clippath: 1,
title: 1,
desc: 1,
text: 1,
tspan: 1,
};
var SVG_OK_ATTRS = {
viewbox: 1,
xmlns: 1,
width: 1,
height: 1,
fill: 1,
"fill-rule": 1,
"fill-opacity": 1,
stroke: 1,
"stroke-width": 1,
"stroke-linecap": 1,
"stroke-linejoin": 1,
"stroke-dasharray": 1,
"stroke-dashoffset": 1,
"stroke-opacity": 1,
opacity: 1,
d: 1,
x: 1,
y: 1,
x1: 1,
y1: 1,
x2: 1,
y2: 1,
cx: 1,
cy: 1,
r: 1,
rx: 1,
ry: 1,
points: 1,
transform: 1,
offset: 1,
"stop-color": 1,
"stop-opacity": 1,
gradientunits: 1,
gradienttransform: 1,
"clip-path": 1,
"clip-rule": 1,
class: 1,
id: 1,
};
function cleanSvg(node) {
var attrs = Array.prototype.slice.call(node.attributes || []);
for (var a = 0; a < attrs.length; a++) {
if (!SVG_OK_ATTRS[attrs[a].name.toLowerCase()]) node.removeAttribute(attrs[a].name);
}
var kids = Array.prototype.slice.call(node.childNodes);
for (var k = 0; k < kids.length; k++) {
var c = kids[k];
if (c.nodeType === 1) {
if (SVG_OK_TAGS[(c.localName || c.nodeName).toLowerCase()]) cleanSvg(c);
else node.removeChild(c);
} else if (c.nodeType !== 3) {
node.removeChild(c);
}
}
}
function setSvg(el, markup) {
el.textContent = "";
var doc = new DOMParser().parseFromString(String(markup), "image/svg+xml");
var root = doc.documentElement;
if (
!root ||
root.nodeName.toLowerCase() !== "svg" ||
doc.getElementsByTagName("parsererror").length
) {
el.textContent = String(markup);
return;
}
cleanSvg(root);
el.appendChild(document.importNode(root, true));
}
var markLeft = document.getElementById("markLeft");
var markRight = document.getElementById("markRight");
markLeft.innerHTML = (vars.leftMark && vars.leftMark.trim()) || DEFAULT_LEFT;
markRight.innerHTML = (vars.rightMark && vars.rightMark.trim()) || DEFAULT_RIGHT;
setSvg(markLeft, (vars.leftMark && vars.leftMark.trim()) || DEFAULT_LEFT);
setSvg(markRight, (vars.rightMark && vars.rightMark.trim()) || DEFAULT_RIGHT);
var wordsEl = document.getElementById("words");
var wordList = [vars.word1, vars.word2, vars.word3, vars.word4].filter(function (w) {
@@ -180,7 +180,7 @@ const html = `<!doctype html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=${WIDTH}, height=${HEIGHT}" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js" integrity="sha384-sG0Hv1tP1lZCk9KQmrIbY/XNwi+OY84GQqhMscbnsoBFqAz8KNCil1kvfL3Hbbk2" crossorigin="anonymous"></script>
<style>
${headStyle}
</style>