diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c472c1282f..89f596646f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -21,6 +21,7 @@ "./skills/hyperframes-creative", "./skills/hyperframes-keyframes", "./skills/hyperframes-registry", + "./skills/hyperframes-studio", "./skills/media-use" ] }, diff --git a/AGENTS.md b/AGENTS.md index 2e77575bef..2d421d404b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This repo ships AI agent skills via [vercel-labs/skills](https://github.com/verc ```bash npx hyperframes skills update # default: installs/refreshes the core set — workflows install on demand -npx hyperframes skills # all 20 published skills at once +npx hyperframes skills # all 21 published skills at once npx skills add heygen-com/hyperframes # interactive picker (terminal only; repo-internal skills are excluded by default) ``` diff --git a/CLAUDE.md b/CLAUDE.md index c2934f5eff..64c77db284 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,11 +4,11 @@ Open-source video rendering framework: write HTML, render video. ## Skills -This repo ships 20 AI agent skills via [vercel-labs/skills](https://github.com/vercel-labs/skills). Install them before writing compositions — they encode framework-specific patterns that generic docs don't cover. **Default to the core set**: the `/hyperframes` router installs each creation workflow on demand; install all 20 only when the user explicitly asks for the full set. +This repo ships 21 AI agent skills via [vercel-labs/skills](https://github.com/vercel-labs/skills). Install them before writing compositions — they encode framework-specific patterns that generic docs don't cover. **Default to the core set**: the `/hyperframes` router installs each creation workflow on demand; install all 21 only when the user explicitly asks for the full set. ```bash npx hyperframes skills update # default: installs/refreshes the core set — workflows install on demand -npx hyperframes skills # all 20 published skills at once — only on explicit request +npx hyperframes skills # all 21 published skills at once — only on explicit request npx skills add heygen-com/hyperframes # interactive picker (terminal only; --all also pulls the 6 repo-internal skills under .claude/skills) npx skills add heygen-com/hyperframes --skill # just one (bare name, no leading slash) ``` diff --git a/README.md b/README.md index fa9805507d..58fa6d4e56 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Install the HyperFrames skills, then describe the video you want: npx skills add heygen-com/hyperframes ``` -> The picker opens with nothing pre-selected — the **Core Skills** group is all you need: the `/hyperframes` router installs each creation workflow on demand. Agents and non-interactive runs should use `npx hyperframes skills update` instead — it installs exactly the core set, whereas `skills add --all` installs all 20 published skills. The six repo-internal skills under `.claude/skills` / `.agents/skills` are excluded by default. For the full published set use `npx hyperframes skills`. +> The picker opens with nothing pre-selected — the **Core Skills** group is all you need: the `/hyperframes` router installs each creation workflow on demand. Agents and non-interactive runs should use `npx hyperframes skills update` instead — it installs exactly the core set, whereas `skills add --all` installs all 21 published skills. The six repo-internal skills under `.claude/skills` / `.agents/skills` are excluded by default. For the full published set use `npx hyperframes skills`. > > `skills add` resolves the skills.sh registry blob, which can lag `main` by hours. `npx hyperframes skills update` installs from the current `main`, so reach for it when you need the newest copy of a skill. @@ -53,9 +53,9 @@ The skills teach agents the HyperFrames production loop: plan the video, write v ## Skills -HyperFrames ships 20 skills agents load on demand. Read `/hyperframes` first — it's the router and capability map; it picks a workflow for any "make me a…" request — video, deck, or composition port — and points to the domain skills below. +HyperFrames ships 21 skills agents load on demand. Read `/hyperframes` first — it's the router and capability map; it picks a workflow for any "make me a…" request — video, deck, or composition port — and points to the domain skills below. -Default to the **core set** — the router installs each creation workflow on demand. `npx hyperframes skills update` installs exactly that from anywhere; the interactive picker (`npx skills add heygen-com/hyperframes`) lists it as the "Core Skills" group, nothing pre-selected. The picker is interactive-only — a non-interactive or agent run without `--skill` installs all 20. Use `npx skills add heygen-com/hyperframes --all` to install all 20 deliberately (skips the picker), or `npx skills add heygen-com/hyperframes --skill ` for just one (bare name, no leading `/`). +Default to the **core set** — the router installs each creation workflow on demand. `npx hyperframes skills update` installs exactly that from anywhere; the interactive picker (`npx skills add heygen-com/hyperframes`) lists it as the "Core Skills" group, nothing pre-selected. The picker is interactive-only — a non-interactive or agent run without `--skill` installs all 21. Use `npx skills add heygen-com/hyperframes --all` to install all 20 deliberately (skips the picker), or `npx skills add heygen-com/hyperframes --skill ` for just one (bare name, no leading `/`). Installs stay lean after that: `npx hyperframes init` keeps the **core set** fresh (the router, the `hyperframes-*` domain skills, and `media-use` — plus whatever is already installed; `/figma` stays on demand) and never expands a partial install; the creation workflows install **on demand** — the router runs `npx hyperframes skills update ` before entering one. Nothing re-pulls the full set behind your back. diff --git a/package.json b/package.json index 9553f6a213..a1a6f8a6b4 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "player:perf": "bun run --filter @hyperframes/player perf", "format:check": "oxfmt --check .", "knip": "knip", - "test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts && vitest run scripts/catalog/", + "test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts && vitest run scripts/catalog/", "typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json", "test:skills": "node --test 'skills/**/*.test.mjs'", "generate:previews": "tsx scripts/generate-template-previews.ts", diff --git a/packages/cli/src/docs/data-attributes.md b/packages/cli/src/docs/data-attributes.md index 9ee928bc3f..848ed89776 100644 --- a/packages/cli/src/docs/data-attributes.md +++ b/packages/cli/src/docs/data-attributes.md @@ -11,7 +11,7 @@ Core attributes for controlling element timing and behavior. ## Media - `data-media-start="2"` — Media playback offset / trim point (seconds) -- `data-volume="0.8"` — Audio/video gain. `1` is 0 dB, `0` is silence, and values above `1` boost up to `3.98` (+12 dB) +- `data-volume="0.8"` — Audio/video gain. `1` is 0 dB, `0` is silence, and values above `1` boost up to `3.98` (+12 dB). For fades and ducking use the `data-automation` volume lane (`hyperframes-core` skill, `creator-editing-recipes.md`) - `data-has-audio="true"` — Indicates video has an audio track ## Composition diff --git a/packages/cli/src/utils/skillsManifest.ts b/packages/cli/src/utils/skillsManifest.ts index 8678e3f2f3..aaa19efceb 100644 --- a/packages/cli/src/utils/skillsManifest.ts +++ b/packages/cli/src/utils/skillsManifest.ts @@ -156,6 +156,7 @@ export const FALLBACK_CORE_SKILLS: readonly string[] = [ "hyperframes-creative", "hyperframes-keyframes", "hyperframes-registry", + "hyperframes-studio", "media-use", ]; diff --git a/packages/studio/src/components/editor/domEditing.test.ts b/packages/studio/src/components/editor/domEditing.test.ts index ca75238f1f..6cb6922e9d 100644 --- a/packages/studio/src/components/editor/domEditing.test.ts +++ b/packages/studio/src/components/editor/domEditing.test.ts @@ -1103,7 +1103,7 @@ describe("patch builders and prompt builder", () => { expect(prompt).toContain("Computed styles (browser-resolved):"); expect(prompt).toContain("Target HTML:"); expect(prompt).toContain("Guardrails:"); - expect(prompt).toContain("Do not modify other elements' data-* attributes or positioning."); + expect(prompt).toContain("Do not modify other elements' data-* attributes or positioning"); }); it("uses an absolute source path in copied agent prompts when provided", () => { diff --git a/packages/studio/src/components/editor/domEditingAgentPrompt.test.ts b/packages/studio/src/components/editor/domEditingAgentPrompt.test.ts index 235fe6c90e..5d184b0ce7 100644 --- a/packages/studio/src/components/editor/domEditingAgentPrompt.test.ts +++ b/packages/studio/src/components/editor/domEditingAgentPrompt.test.ts @@ -78,9 +78,10 @@ describe("buildElementAgentPrompt", () => { "Text: Hello world", "", "Guardrails:", - "- Make a targeted change to this element only.", - "- Preserve the rest of the composition and its timing.", - "- Do not modify other elements' data-* attributes or positioning.", + "- Make a targeted change to this element only, unless the request is a timeline edit.", + "- Preserve the rest of the composition and its timing, except what a timeline edit changes.", + "- Do not modify other elements' data-* attributes or positioning, except where the requested timeline edit requires it (split, retime, reorder, copy a group, swap media).", + "- For timeline edits (trim, split, speed, volume, copy, swap), follow the creator-editing-recipes reference of the hyperframes-core skill and use its exact attribute forms.", "- Prefer existing inline styles or existing CSS rules for this element over adding unrelated selectors.", ].join("\n"), ); @@ -160,9 +161,10 @@ describe("buildPickerAgentPrompt", () => { // Both end on the identical guardrails block, byte for byte. const guardrails = [ "Guardrails:", - "- Make a targeted change to this element only.", - "- Preserve the rest of the composition and its timing.", - "- Do not modify other elements' data-* attributes or positioning.", + "- Make a targeted change to this element only, unless the request is a timeline edit.", + "- Preserve the rest of the composition and its timing, except what a timeline edit changes.", + "- Do not modify other elements' data-* attributes or positioning, except where the requested timeline edit requires it (split, retime, reorder, copy a group, swap media).", + "- For timeline edits (trim, split, speed, volume, copy, swap), follow the creator-editing-recipes reference of the hyperframes-core skill and use its exact attribute forms.", "- Prefer existing inline styles or existing CSS rules for this element over adding unrelated selectors.", ].join("\n"); expect(domPrompt.endsWith(guardrails)).toBe(true); diff --git a/packages/studio/src/components/editor/domEditingAgentPrompt.ts b/packages/studio/src/components/editor/domEditingAgentPrompt.ts index 83a46e0c23..4640e2efe0 100644 --- a/packages/studio/src/components/editor/domEditingAgentPrompt.ts +++ b/packages/studio/src/components/editor/domEditingAgentPrompt.ts @@ -28,9 +28,10 @@ export interface AgentPromptElementInfo { const GUARDRAIL_LINES = [ "Guardrails:", - "- Make a targeted change to this element only.", - "- Preserve the rest of the composition and its timing.", - "- Do not modify other elements' data-* attributes or positioning.", + "- Make a targeted change to this element only, unless the request is a timeline edit.", + "- Preserve the rest of the composition and its timing, except what a timeline edit changes.", + "- Do not modify other elements' data-* attributes or positioning, except where the requested timeline edit requires it (split, retime, reorder, copy a group, swap media).", + "- For timeline edits (trim, split, speed, volume, copy, swap), follow the creator-editing-recipes reference of the hyperframes-core skill and use its exact attribute forms.", "- Prefer existing inline styles or existing CSS rules for this element over adding unrelated selectors.", ]; diff --git a/scripts/creator-editing-recipes.test.mjs b/scripts/creator-editing-recipes.test.mjs new file mode 100644 index 0000000000..5d00b4423c --- /dev/null +++ b/scripts/creator-editing-recipes.test.mjs @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import { lintHyperframeHtml } from "../packages/lint/src/index.ts"; + +const OWNER = "skills/hyperframes-core/references/creator-editing-recipes.md"; +const STUDIO_SKILL = "skills/hyperframes-studio/SKILL.md"; +const VOLUME_TWEEN = /\.(?:to|from|fromTo|set)\(\s*["'`]#[\w-]+["'`]\s*,\s*\{[^}]*\bvolume\s*:/; +const TWEEN_SCANNED = [ + "skills/hyperframes-core/references/variables-and-media.md", + "skills/hyperframes-core/references/data-attributes.md", + "skills/hyperframes-animation/adapters/gsap.md", + "skills/hyperframes-audio/SKILL.md", + "skills/hyperframes-audio/references/attributes.md", + "skills/music-to-video/references/montage.md", + "skills/media-use/references/operations.md", + "packages/cli/src/docs/data-attributes.md", +]; + +const read = (path) => readFile(new URL(`../${path}`, import.meta.url), "utf8"); + +const htmlBlocks = (md) => [...md.matchAll(/```html\n([\s\S]*?)```/g)].map((m) => m[1]); + +const wrap = (fragment) => + fragment.includes("
${fragment}
`; + +test("every html example in the owner doc lints with no errors and no volume double-automation", async () => { + const blocks = htmlBlocks(await read(OWNER)); + assert.ok(blocks.length >= 10, "expected the owner doc to keep its worked examples"); + for (const [i, block] of blocks.entries()) { + const { findings } = await lintHyperframeHtml(wrap(block), { filePath: "index.html" }); + const bad = findings.filter( + (f) => f.severity === "error" || f.code.startsWith("audio_volume_"), + ); + assert.deepEqual( + bad.map((f) => `${f.code}: ${f.message}`), + [], + `example ${i + 1} in ${OWNER}`, + ); + } +}); + +test("no other doc teaches a timeline tween as the way to fade volume", async () => { + for (const path of TWEEN_SCANNED) { + const lines = (await read(path)).split("\n"); + const hit = lines.findIndex((l) => VOLUME_TWEEN.test(l)); + assert.equal(hit, -1, `${path}:${hit + 1} teaches a volume tween; point at ${OWNER}`); + } +}); + +test("the Studio skill holds conventions only and points at the owner doc for edits", async () => { + const skill = await read(STUDIO_SKILL); + assert.match(skill, /creator-editing-recipes\.md/); + assert.equal(htmlBlocks(skill).length, 0, "a recipe restated in the Studio skill will drift"); +}); + +test("the Remotion translation docs do not claim volume ramps are unsupported", async () => { + for (const path of [ + "skills/remotion-to-hyperframes/references/limitations.md", + "skills/remotion-to-hyperframes/references/media.md", + ]) { + assert.doesNotMatch(await read(path), /static `data-volume` only/, path); + } +}); + +test("the Studio skill's safe boxes equal the preview's", async () => { + const src = await read("packages/studio/src/utils/previewSafeMargins.ts"); + const action = src.match(/ACTION_SAFE_PERCENT = (\d+)/)?.[1]; + const title = src.match(/TITLE_SAFE_PERCENT = (\d+)/)?.[1]; + const skill = await read(STUDIO_SKILL); + assert.match(skill, new RegExp(`Action-safe\\s*\\|\\s*${action}%`)); + assert.match(skill, new RegExp(`Title-safe\\s*\\|\\s*${title}%`)); +}); diff --git a/skills-manifest.json b/skills-manifest.json index 9cfb6eee92..05a45cd0c8 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -2,7 +2,7 @@ "source": "heygen-com/hyperframes", "skills": { "embedded-captions": { - "hash": "4023b0b051ba1e75", + "hash": "f1b298aa899d86df", "files": 142 }, "faceless-explainer": { @@ -18,11 +18,11 @@ "files": 4 }, "hyperframes": { - "hash": "d0a114a8f894758e", + "hash": "ffacb7af2a96d788", "files": 26 }, "hyperframes-animation": { - "hash": "4225f299e6ff9a0e", + "hash": "492f6f39edf2a8e5", "files": 121 }, "hyperframes-audio": { @@ -34,7 +34,7 @@ "files": 11 }, "hyperframes-core": { - "hash": "3e29cc2454953b05", + "hash": "054fe5577b064927", "files": 11 }, "hyperframes-creative": { @@ -49,8 +49,12 @@ "hash": "51e6dba95a8dfb45", "files": 12 }, + "hyperframes-studio": { + "hash": "b063eaa9eb1e4b9b", + "files": 1 + }, "media-use": { - "hash": "4531919cdbda573e", + "hash": "8f21655cd07a62ea", "files": 158 }, "motion-graphics": { @@ -58,7 +62,7 @@ "files": 23 }, "music-to-video": { - "hash": "7fad41aa135aea23", + "hash": "e69f4646ef354be9", "files": 169 }, "pr-to-video": { @@ -70,7 +74,7 @@ "files": 30 }, "remotion-to-hyperframes": { - "hash": "65222f2d894e8776", + "hash": "c8eb6f48889f5c06", "files": 77 }, "slideshow": { diff --git a/skills/embedded-captions/references/aesthetic-principles.md b/skills/embedded-captions/references/aesthetic-principles.md index 17a52307c1..fe81e7d70f 100644 --- a/skills/embedded-captions/references/aesthetic-principles.md +++ b/skills/embedded-captions/references/aesthetic-principles.md @@ -121,9 +121,8 @@ Chunk at natural pauses ≥ 250ms. A caption spanning a breath-break feels wrong ### 17. Safe zones per platform, always -- 9:16 TikTok/IG/Shorts: caption zone `y ∈ [12%, 78%]` (bottom 22% is UI) -- 16:9 broadcast: title-safe = center 80% -- TV export: 5% margin on all sides +- Use the safe boxes in `/hyperframes-studio` (Safe zones) for wide and vertical + framings; that skill owns the values. Bake into the layout solver. Never eyeball. diff --git a/skills/embedded-captions/references/rail.md b/skills/embedded-captions/references/rail.md index 580380bab0..727d19bebe 100644 --- a/skills/embedded-captions/references/rail.md +++ b/skills/embedded-captions/references/rail.md @@ -17,7 +17,7 @@ with only the climax(es) promoted to embed. Rail is not a fallback — it's the - **Lower third, horizontally centered.** Landscape (16:9): baseline ~80–120px above the bottom edge. Portrait (9:16): lower-middle, ~ 600–700px from the bottom (clear of platform UI). -- **Title-safe margins:** keep text within ~90% width / inside any letterbox-pillarbox bars +- **Title-safe margins:** keep text within the title-safe box (80% of the frame, `/hyperframes-studio` Safe zones) / inside any letterbox-pillarbox bars (see the letterbox probe). Never flush to the frame edge. - **One caption group on screen at a time.** No accumulation, no cascade — that's embed-track behaviour. - It rides above the subject; the matte does **not** occlude it (rail = in front). diff --git a/skills/hyperframes-animation/adapters/gsap.md b/skills/hyperframes-animation/adapters/gsap.md index 6638706693..53dddf92f0 100644 --- a/skills/hyperframes-animation/adapters/gsap.md +++ b/skills/hyperframes-animation/adapters/gsap.md @@ -61,7 +61,7 @@ HyperFrames is stricter than vanilla GSAP. Animate only: - **Compositor-cheap**: `opacity`, `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `rotationX`, `rotationY`, `skewX`, `skewY`, `transformOrigin` - **Visual fills**: `color`, `backgroundColor`, `borderColor`, `borderRadius` - **CSS variables**: `"--hue": 180` etc. -- **Media `volume`** (on `