diff --git a/.claude/settings.json b/.claude/settings.json index 095491496..e9ba04395 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -8,7 +8,7 @@ "type": "command", "timeout": 180, "statusMessage": "Running build + lint + typecheck before commit…", - "command": "node -e \"\nconst chunks = [];\nprocess.stdin.on('data', d => chunks.push(d));\nprocess.stdin.on('end', () => {\n const input = JSON.parse(Buffer.concat(chunks).toString());\n const cmd = input.tool_input?.command || '';\n if (!/git\\\\s+commit\\\\b/.test(cmd)) process.exit(0);\n const { execSync } = require('child_process');\n const cwd = process.env.PWD || process.cwd();\n const steps = [\n ['pnpm build', 'Build'],\n ['pnpm run -w lint', 'Lint'],\n ['bun run --filter \\'*\\' typecheck 2>&1 | grep -v \\'vitest\\\\|test\\\\.ts\\' || true', 'Typecheck'],\n ];\n const failures = [];\n for (const [script, label] of steps) {\n try { execSync(script, { cwd, stdio: 'pipe' }); }\n catch (e) {\n failures.push(label + ':\\\\n' + (e.stdout?.toString() || e.message).slice(0, 400));\n }\n }\n if (failures.length > 0) {\n process.stdout.write(JSON.stringify({\n continue: false,\n stopReason: '\\u274c Pre-commit checks failed:\\\\n\\\\n' + failures.join('\\\\n\\\\n') + '\\\\n\\\\nFix the issues above before committing.',\n }));\n }\n});\"" + "command": "node -e \"\nconst chunks = [];\nprocess.stdin.on('data', d => chunks.push(d));\nprocess.stdin.on('end', () => {\n const input = JSON.parse(Buffer.concat(chunks).toString());\n const cmd = input.tool_input?.command || '';\n if (!/git\\\\s+commit\\\\b/.test(cmd)) process.exit(0);\n const { execSync } = require('child_process');\n const cwd = process.env.PWD || process.cwd();\n const steps = [\n ['bun run build', 'Build'],\n ['bun run lint', 'Lint'],\n ['bun run --filter \\'*\\' typecheck 2>&1 | grep -v \\'vitest\\\\|test\\\\.ts\\' || true', 'Typecheck'],\n ];\n const failures = [];\n for (const [script, label] of steps) {\n try { execSync(script, { cwd, stdio: 'pipe' }); }\n catch (e) {\n failures.push(label + ':\\\\n' + (e.stdout?.toString() || e.message).slice(0, 400));\n }\n }\n if (failures.length > 0) {\n process.stdout.write(JSON.stringify({\n continue: false,\n stopReason: '\\u274c Pre-commit checks failed:\\\\n\\\\n' + failures.join('\\\\n\\\\n') + '\\\\n\\\\nFix the issues above before committing.',\n }));\n }\n});\"" } ] } diff --git a/.claude/worktrees/agent-a692178b b/.claude/worktrees/agent-a692178b new file mode 160000 index 000000000..1e4c101fb --- /dev/null +++ b/.claude/worktrees/agent-a692178b @@ -0,0 +1 @@ +Subproject commit 1e4c101fb4412e554d740d4c662ef4779a63eba6 diff --git a/CLAUDE.md b/CLAUDE.md index 837fae642..f5f607efe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,11 +39,13 @@ The skills encode HyperFrames-specific patterns (e.g., required `class="clip"` o ### Installing skills ```bash -npx hyperframes skills # install all to Claude, Gemini, Codex -npx hyperframes skills --claude # Claude Code only -npx skills add greensock/gsap-skills # alternative: via skills CLI +npx skills add heygen-com/hyperframes # HyperFrames skills +npx skills add greensock/gsap-skills # GSAP skills ``` +Uses [vercel-labs/skills](https://github.com/vercel-labs/skills). Installs to Claude Code, Gemini CLI, and Codex CLI by default. Pass `-a ` for other targets. + + ## Project Overview Open-source video rendering framework: write HTML, render video. @@ -93,13 +95,13 @@ npx hyperframes transcribe openai-response.json Default is `small.en`. Upgrade for better accuracy: -| Model | Size | Use case | -| ----------- | ------ | -------------------------------- | -| `tiny` | 75 MB | Quick testing | -| `base` | 142 MB | Short clips, clear audio | -| `small` | 466 MB | **Default** — most content | -| `medium` | 1.5 GB | Important content, noisy audio | -| `large-v3` | 3.1 GB | Production quality | +| Model | Size | Use case | +| ---------- | ------ | ------------------------------ | +| `tiny` | 75 MB | Quick testing | +| `base` | 142 MB | Short clips, clear audio | +| `small` | 466 MB | **Default** — most content | +| `medium` | 1.5 GB | Important content, noisy audio | +| `large-v3` | 3.1 GB | Production quality | **Only use `.en` suffix when you know the audio is English.** `.en` models translate non-English audio into English instead of transcribing it. diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index a94d7b771..11a9b761b 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -32,7 +32,6 @@ const subCommands = { compositions: () => import("./commands/compositions.js").then((m) => m.default), benchmark: () => import("./commands/benchmark.js").then((m) => m.default), browser: () => import("./commands/browser.js").then((m) => m.default), - skills: () => import("./commands/install-skills.js").then((m) => m.default), transcribe: () => import("./commands/transcribe.js").then((m) => m.default), docs: () => import("./commands/docs.js").then((m) => m.default), doctor: () => import("./commands/doctor.js").then((m) => m.default), diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index aed2cd4b6..40d2056e0 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -23,70 +23,6 @@ import { fetchRemoteTemplate } from "../templates/remote.js"; import { trackInitTemplate } from "../telemetry/events.js"; import { hasFFmpeg } from "../whisper/manager.js"; -// --------------------------------------------------------------------------- -// Install skills silently after scaffolding -// --------------------------------------------------------------------------- - -async function installSkills(interactive: boolean): Promise { - try { - const { installAllSkills, TARGETS } = await import("./install-skills.js"); - - let selectedTargets: string[] | undefined; - - if (interactive) { - const choices = await clack.multiselect({ - message: "Install skills for:", - options: TARGETS.map((t) => ({ - value: t.flag, - label: t.name, - hint: t.dir, - })), - initialValues: TARGETS.filter((t) => t.defaultEnabled).map((t) => t.flag), - required: false, - }); - - if (clack.isCancel(choices)) { - return; - } - - selectedTargets = choices as string[]; - if (selectedTargets.length === 0) { - clack.log.info(c.dim("Skipping skills installation")); - return; - } - } - - const spin = interactive ? clack.spinner() : null; - spin?.start("Installing AI coding skills..."); - - const result = await installAllSkills(selectedTargets, { - onProgress: (msg) => spin?.message(msg), - }); - if (result.count > 0) { - const msg = `${result.count} skills installed (${result.targets.join(", ")})`; - if (spin) { - spin.stop(c.success(msg)); - } else { - console.log(c.success(msg)); - } - if (result.skipped.length > 0) { - const skipMsg = `Skipped: ${result.skipped.join(", ")} (repo not accessible)`; - if (interactive) { - clack.log.warn(c.dim(skipMsg)); - } else { - console.log(c.dim(` ${skipMsg}`)); - } - } - } else { - spin?.stop(c.dim("No skills installed")); - } - } catch { - if (interactive) { - clack.log.warn(c.dim("Skills install skipped (no git or network)")); - } - } -} - interface VideoMeta { durationSeconds: number; width: number; @@ -218,12 +154,6 @@ function getSharedTemplateDir(): string { return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]); } -function getBundledSkillsDir(): string { - // In dev: cli/src/commands/ → repo root skills/ - // In built: cli/dist/ → cli/dist/skills/ - return resolveAssetDir(["..", "..", "..", "..", "skills"], ["skills"]); -} - function patchVideoSrc( dir: string, videoFilename: string | undefined, @@ -405,20 +335,6 @@ async function scaffoldProject( } } } - - // Copy project-level skills (.claude/skills/) for immediate availability - const skillsSrcDir = getBundledSkillsDir(); - if (existsSync(skillsSrcDir)) { - const projectSkills = ["hyperframes-compose", "hyperframes-captions", "hyperframes-cli"]; - for (const skill of projectSkills) { - const src = join(skillsSrcDir, skill); - if (existsSync(src)) { - const dest = resolve(destDir, ".claude", "skills", skill); - mkdirSync(dest, { recursive: true }); - cpSync(src, dest, { recursive: true }); - } - } - } } // --------------------------------------------------------------------------- @@ -447,10 +363,6 @@ export default defineCommand({ description: "Path to an audio file (MP3, WAV, M4A)", alias: "a", }, - "skip-skills": { - type: "boolean", - description: "Skip AI coding skills installation", - }, "skip-transcribe": { type: "boolean", description: "Skip whisper transcription", @@ -474,7 +386,6 @@ export default defineCommand({ const templateFlag = args.template; const videoFlag = args.video; const audioFlag = args.audio; - const skipSkills = args["skip-skills"] === true; const skipTranscribe = args["skip-transcribe"] === true; const nonInteractive = args["non-interactive"] === true; const modelFlag = args.model; @@ -578,11 +489,6 @@ export default defineCommand({ await patchTranscript(destDir, transcriptFile); } - // Skills - if (!skipSkills) { - await installSkills(false); - } - console.log(c.success(`Created ${c.accent(name + "/")}`)); for (const f of readdirSync(destDir).filter((f) => !f.startsWith("."))) { console.log(` ${c.accent(f)}`); @@ -590,18 +496,18 @@ export default defineCommand({ console.log(); console.log("Get started:"); console.log(); - console.log(` ${c.accent("1.")} Open this project with your AI coding agent:`); + console.log(` ${c.accent("1.")} Install AI coding skills (one-time):`); + console.log(` ${c.accent("npx skills add heygen-com/hyperframes")}`); + console.log(); + console.log(` ${c.accent("2.")} Open this project with your AI coding agent:`); console.log( ` ${c.accent(`cd ${name}`)} then start ${c.accent("Claude Code")}, ${c.accent("Cursor")}, or your preferred agent`, ); - console.log( - ` ${c.dim("AI skills are installed — your agent knows how to create and edit compositions.")}`, - ); console.log(); - console.log(` ${c.accent("2.")} Preview in the browser:`); + console.log(` ${c.accent("3.")} Preview in the browser:`); console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npx hyperframes preview")}`); console.log(); - console.log(` ${c.accent("3.")} Render to MP4 when ready:`); + console.log(` ${c.accent("4.")} Render to MP4 when ready:`); console.log(` ${c.accent(`cd ${name}`)} && ${c.accent("npx hyperframes render")}`); console.log(); console.log(` ${c.dim("Full docs: hyperframes.heygen.com")}`); @@ -774,17 +680,12 @@ export default defineCommand({ await patchTranscript(destDir, transcriptFile); } - // 5. Install AI coding skills - if (!skipSkills) { - await installSkills(true); - } - const files = readdirSync(destDir); clack.note(files.map((f) => c.accent(f)).join("\n"), c.success(`Created ${name}/`)); clack.log.message( - `${c.dim("Tip:")} Open this project with ${c.accent("Claude Code")}, ${c.accent("Cursor")}, or your preferred AI agent.\n` + - `${c.dim(" AI skills are installed — your agent knows how to create and edit compositions.")}`, + `${c.dim("Tip:")} Install AI coding skills: ${c.accent("npx skills add heygen-com/hyperframes")}\n` + + `${c.dim(" Then open this project with")} ${c.accent("Claude Code")}${c.dim(",")} ${c.accent("Cursor")}${c.dim(", or your preferred agent.")}`, ); // Auto-launch studio preview diff --git a/packages/cli/src/commands/install-skills.ts b/packages/cli/src/commands/install-skills.ts deleted file mode 100644 index 377664435..000000000 --- a/packages/cli/src/commands/install-skills.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { defineCommand } from "citty"; -import { existsSync, mkdirSync, readdirSync, rmSync, cpSync } from "node:fs"; -import { join, dirname } from "node:path"; -import { homedir } from "node:os"; -import { execFileSync, execFile } from "node:child_process"; -import * as clack from "@clack/prompts"; -import { c } from "../ui/colors.js"; - -function execFileAsync( - cmd: string, - args: string[], - options: { stdio?: "ignore"; timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv }, -): Promise { - return new Promise((resolve, reject) => { - execFile(cmd, args, options, (error) => { - if (error) reject(error); - else resolve(); - }); - }); -} - -// --------------------------------------------------------------------------- -// Target CLI tools — each has a global skills directory -// --------------------------------------------------------------------------- - -interface Target { - name: string; - flag: string; - /** Agent name for `npx skills add -a ` */ - skillsAgent: string; - dir: string; - defaultEnabled: boolean; -} - -const TARGETS: Target[] = [ - { - name: "Claude Code", - flag: "claude", - skillsAgent: "claude-code", - dir: join(homedir(), ".claude", "skills"), - defaultEnabled: true, - }, - { - name: "Gemini CLI", - flag: "gemini", - skillsAgent: "gemini-cli", - dir: join(homedir(), ".gemini", "skills"), - defaultEnabled: true, - }, - { - name: "Codex CLI", - flag: "codex", - skillsAgent: "codex", - dir: join(homedir(), ".codex", "skills"), - defaultEnabled: true, - }, - { - name: "Cursor", - flag: "cursor", - skillsAgent: "cursor", - get dir() { - return join(process.cwd(), ".cursor", "skills"); - }, - defaultEnabled: false, - }, -]; - -// --------------------------------------------------------------------------- -// Skill sources — GitHub repos containing skill directories -// --------------------------------------------------------------------------- - -interface SkillSource { - name: string; - /** GitHub shorthand (owner/repo) or full URL */ - repo: string; - /** For fallback: subdirectory within the repo that contains skill folders */ - skillsPath: string; - /** For fallback: local cache directory */ - cache: string; -} - -const SOURCES: SkillSource[] = [ - { - name: "HyperFrames", - repo: "heygen-com/hyperframes", - skillsPath: "skills", - cache: join(homedir(), ".cache", "hyperframes", "hyperframes-skills"), - }, - { - name: "GSAP", - repo: "greensock/gsap-skills", - skillsPath: "skills", - cache: join(homedir(), ".cache", "hyperframes", "gsap-skills"), - }, -]; - -// --------------------------------------------------------------------------- -// npx skills add — primary install method -// --------------------------------------------------------------------------- - -function hasNpx(): boolean { - try { - execFileSync("npx", ["--version"], { stdio: "ignore", timeout: 5000 }); - return true; - } catch { - return false; - } -} - -async function runSkillsAdd(repo: string, agents: string[], global: boolean): Promise { - const args = ["skills", "add", repo, "-y"]; - if (global) args.push("-g"); - for (const agent of agents) { - args.push("-a", agent); - } - await execFileAsync("npx", args, { - stdio: "ignore", - timeout: 120_000, - }); -} - -// --------------------------------------------------------------------------- -// Fallback — git clone + copy (used when npx skills add is unavailable) -// --------------------------------------------------------------------------- - -function hasGit(): boolean { - try { - execFileSync("git", ["--version"], { stdio: "ignore", timeout: 5000 }); - return true; - } catch { - return false; - } -} - -const GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0" }; - -async function gitClone(repo: string, dest: string): Promise { - await execFileAsync("git", ["clone", "--depth", "1", repo, dest], { - stdio: "ignore", - timeout: 60_000, - env: GIT_ENV, - }); -} - -async function fetchRepo(source: SkillSource): Promise { - const gitUrl = `https://github.com/${source.repo}.git`; - if (existsSync(source.cache)) { - try { - await execFileAsync("git", ["pull", "--ff-only"], { - cwd: source.cache, - stdio: "ignore", - timeout: 30_000, - env: GIT_ENV, - }); - } catch { - const skillsDir = join(source.cache, source.skillsPath); - if (existsSync(skillsDir)) return skillsDir; - rmSync(source.cache, { recursive: true, force: true }); - await gitClone(gitUrl, source.cache); - } - } else { - mkdirSync(dirname(source.cache), { recursive: true }); - await gitClone(gitUrl, source.cache); - } - const skillsDir = join(source.cache, source.skillsPath); - return existsSync(skillsDir) ? skillsDir : undefined; -} - -interface InstalledSkill { - name: string; - source: string; -} - -function installSkillsFromDir( - sourceDir: string, - targetDir: string, - sourceName: string, -): InstalledSkill[] { - const installed: InstalledSkill[] = []; - if (!existsSync(sourceDir)) return installed; - - const entries = readdirSync(sourceDir, { withFileTypes: true }); - for (const entry of entries) { - if (!entry.isDirectory()) continue; - const skillFile = join(sourceDir, entry.name, "SKILL.md"); - if (!existsSync(skillFile)) continue; - - const destDir = join(targetDir, entry.name); - if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true }); - mkdirSync(destDir, { recursive: true }); - cpSync(join(sourceDir, entry.name), destDir, { recursive: true }); - installed.push({ name: entry.name, source: sourceName }); - } - return installed; -} - -async function fallbackInstall(targets: Target[]): Promise<{ - count: number; - installed: InstalledSkill[]; - skipped: string[]; -}> { - const skipped: string[] = []; - const fetched: { source: SkillSource; skillsDir: string }[] = []; - - for (const source of SOURCES) { - try { - const skillsDir = await fetchRepo(source); - if (skillsDir) { - fetched.push({ source, skillsDir }); - } else { - skipped.push(source.name); - } - } catch { - skipped.push(source.name); - } - } - - // Install to first target and collect results, then copy to remaining targets - const [first, ...rest] = targets; - const allInstalled: InstalledSkill[] = []; - if (first) { - mkdirSync(first.dir, { recursive: true }); - for (const { skillsDir, source } of fetched) { - allInstalled.push(...installSkillsFromDir(skillsDir, first.dir, source.name)); - } - } - for (const target of rest) { - mkdirSync(target.dir, { recursive: true }); - for (const { skillsDir, source } of fetched) { - installSkillsFromDir(skillsDir, target.dir, source.name); - } - } - - return { count: allInstalled.length, installed: allInstalled, skipped }; -} - -// --------------------------------------------------------------------------- -// Programmatic API — used by init command -// --------------------------------------------------------------------------- - -export { TARGETS }; - -export async function installAllSkills( - targetNames?: string[], - options?: { onProgress?: (message: string) => void }, -): Promise<{ count: number; targets: string[]; skipped: string[] }> { - const targets = targetNames - ? TARGETS.filter((t) => targetNames.includes(t.flag)) - : TARGETS.filter((t) => t.defaultEnabled); - const agents = targets.map((t) => t.skillsAgent); - const progress = options?.onProgress; - - // Try npx skills add first - if (hasNpx()) { - const skipped: string[] = []; - let count = 0; - for (const source of SOURCES) { - try { - progress?.(`Installing ${source.name} skills...`); - await runSkillsAdd(source.repo, agents, true); - count += 1; - } catch { - skipped.push(source.name); - } - } - if (count > 0) { - return { count, targets: targets.map((t) => t.name), skipped }; - } - // npx skills add failed for all sources — try fallback - } - - // Fallback: git clone + copy - if (!hasGit()) { - return { count: 0, targets: [], skipped: SOURCES.map((s) => s.name) }; - } - progress?.("Cloning skill repositories..."); - const result = await fallbackInstall(targets); - return { count: result.count, targets: targets.map((t) => t.name), skipped: result.skipped }; -} - -// --------------------------------------------------------------------------- -// Command -// --------------------------------------------------------------------------- - -function resolveTargets(args: Record): Target[] { - const hasAnyFlag = TARGETS.some((t) => args[t.flag] === true); - if (hasAnyFlag) { - return TARGETS.filter((t) => args[t.flag] === true); - } - return TARGETS.filter((t) => t.defaultEnabled); -} - -async function runInstall({ args }: { args: Record }): Promise { - clack.intro(c.bold("hyperframes skills")); - - const targets = resolveTargets(args); - const agents = targets.map((t) => t.skillsAgent); - - // Try npx skills add - if (hasNpx()) { - const installed: string[] = []; - const skippedSources: string[] = []; - - for (const source of SOURCES) { - const spinner = clack.spinner(); - spinner.start(`Installing ${source.name} skills...`); - try { - await runSkillsAdd(source.repo, agents, true); - installed.push(source.name); - spinner.stop(c.success(`${source.name} skills installed`)); - } catch { - skippedSources.push(source.name); - spinner.stop(c.dim(`${source.name} skills skipped (unavailable)`)); - } - } - - console.log(); - console.log(` ${c.dim("Targets:")} ${targets.map((t) => t.name).join(", ")}`); - if (skippedSources.length > 0) { - console.log(` ${c.dim("Skipped:")} ${skippedSources.join(", ")}`); - } - console.log(); - - if (installed.length > 0) { - clack.outro(c.success(`${installed.join(" + ")} skills installed.`)); - return; - } - - clack.log.warn("npx skills add failed — trying fallback..."); - } - - // Fallback: git clone + copy - if (!hasGit()) { - clack.log.error(c.error("Neither npx nor git available. Install Node.js or git and retry.")); - clack.outro(c.warn("No skills installed.")); - return; - } - - clack.log.info(c.dim("Using git fallback...")); - - const result = await fallbackInstall(targets); - - console.log(); - for (const source of SOURCES) { - const names = result.installed.filter((s) => s.source === source.name).map((s) => s.name); - if (names.length > 0) { - const label = `${source.name}:`.padEnd(14); - console.log(` ${c.dim(label)} ${names.map((s) => c.accent(s)).join(", ")}`); - } - } - console.log(` ${c.dim("Targets:")} ${targets.map((t) => t.name).join(", ")}`); - if (result.skipped.length > 0) { - console.log(` ${c.dim("Skipped:")} ${result.skipped.join(", ")}`); - } - console.log(); - - if (result.count > 0) { - clack.outro(c.success(`${result.count} skills ready.`)); - } else { - clack.outro(c.warn("No skills installed.")); - } -} - -export default defineCommand({ - meta: { - name: "skills", - description: "Install HyperFrames and GSAP skills for AI coding tools", - }, - args: { - claude: { type: "boolean", description: "Install to Claude Code (~/.claude/skills/)" }, - gemini: { type: "boolean", description: "Install to Gemini CLI (~/.gemini/skills/)" }, - codex: { type: "boolean", description: "Install to Codex CLI (~/.codex/skills/)" }, - cursor: { - type: "boolean", - description: "Install to Cursor (.cursor/skills/ in current project)", - }, - "human-friendly": { type: "boolean", description: "Enable interactive terminal UI" }, - }, - run: runInstall, -}); diff --git a/skills/audio-reactive/SKILL.md b/skills/audio-reactive/SKILL.md index 26d36c00f..c01517b9e 100644 --- a/skills/audio-reactive/SKILL.md +++ b/skills/audio-reactive/SKILL.md @@ -1,7 +1,7 @@ --- name: audio-reactive -description: Drive any visual element in a HyperFrames composition from audio data — captions, backgrounds, shapes, overlays, anything GSAP can animate. Use when a composition should respond to music, voice, or sound. -trigger: Use when a composition involves music, beat-synced animation, audio visualization, or any visual element that should react to sound. +description: Audio-reactive animation — drive visuals from music, voice, or sound in HyperFrames. Maps frequency bands and amplitude to any GSAP-animatable property. +trigger: Use when a composition involves music, beat-synced animation, audio visualization, or any visual reacting to sound. --- # Audio-Reactive Animation diff --git a/skills/gsap-effects/SKILL.md b/skills/gsap-effects/SKILL.md index 5369015c0..17b112eae 100644 --- a/skills/gsap-effects/SKILL.md +++ b/skills/gsap-effects/SKILL.md @@ -1,6 +1,6 @@ --- name: gsap-effects -description: Ready-made animation effects for HyperFrames compositions. Use when adding typewriter text, text reveals, character-by-character animation, audio visualizations, spectrum bars, waveform displays, or any reactive audio-driven animation to a composition. Also use when audio has been analyzed or transcribed in the current session and music is detected — the audio visualizer can enhance the composition with reactive visuals. Reference files contain patterns and data contracts. +description: Typewriter text, audio visualizer, and drop-in animation effects for HyperFrames compositions. Use for character-by-character reveals, spectrum bars, waveforms, or audio-reactive visuals. --- # GSAP Effects diff --git a/skills/hyperframes-captions/SKILL.md b/skills/hyperframes-captions/SKILL.md index ecad8cd43..dc8292feb 100644 --- a/skills/hyperframes-captions/SKILL.md +++ b/skills/hyperframes-captions/SKILL.md @@ -1,7 +1,7 @@ --- name: hyperframes-captions -description: Build tone-adaptive captions from whisper transcripts. Detects script energy (hype, corporate, tutorial, storytelling, social) and applies matching typography, color, and animation. Supports per-word styling for brand names, ALL CAPS, numbers, and CTAs. Use when adding captions, subtitles, or lyrics to a HyperFrames composition. Lyric videos ARE captions — any text synced to audio uses this skill. -trigger: Use this skill whenever a task involves syncing text to audio timing. This includes captions, subtitles, lyrics, karaoke, transcription overlays, and any word-level or phrase-level text timed to speech or music. +description: Captions, subtitles, lyrics, and karaoke synced to audio in HyperFrames. Tone-adaptive — detects script energy and applies matching typography, color, and animation with per-word styling. +trigger: Syncing text to audio timing — captions, subtitles, lyrics, karaoke, transcription overlays, word-level or phrase-level text timed to speech or music. --- # Captions @@ -11,6 +11,7 @@ trigger: Use this skill whenever a task involves syncing text to audio timing. T **Never use `.en` models unless the user explicitly states the audio is English.** `.en` models (small.en, medium.en) TRANSLATE non-English audio into English instead of transcribing it. This silently destroys the original language. When transcribing: + 1. If the user says the language → use `--model small --language ` (no `.en` suffix) 2. If the user says it's English → use `--model small.en` 3. If the language is unknown → use `--model small` (no `.en`, no `--language`) — whisper auto-detects @@ -19,6 +20,7 @@ When transcribing: --- + Analyze the spoken content to determine caption style. If the user specifies a style, use that. Otherwise, detect tone from the transcript. ## Transcript Source @@ -38,7 +40,7 @@ This is the only format the captions composition consumes. Use it directly: const words = JSON.parse(transcriptJson); // [{ text, start, end }] ``` -For transcription commands, whisper model selection, external APIs (OpenAI, Groq), and supported input formats, see [transcript-guide.md](./transcript-guide.md). **After every transcription, read the transcript and run the quality check** — bad transcripts (music tokens, garbled words) must be retried with a larger model before proceeding. +For transcription commands, whisper model selection, external APIs (OpenAI, Groq), and supported input formats, see [transcript-guide.md](./transcript-guide.md). ## Style Detection (Default — When No Style Is Specified) @@ -99,42 +101,13 @@ For each detected word, specify: ## Script-to-Style Mapping -Read the transcript. Detect the energy. The tone determines everything — typography, color, animation techniques. Use the table below to select your full animation stack. - -| Detected energy | Font mood | Color | Entrance | Highlight | Exit | -| ------------------------------------ | ------------------------------ | --------------------------- | ---------------------------- | ------------------------ | ------------------- | -| High (hype, launch, music, anthem) | Heavy condensed, 800-900 | Bright accent on dark | Slam heroes + elastic others | Karaoke with accent glow | Scatter or drop | -| Medium-high (social, casual, upbeat) | Rounded sans, 700-800 | Playful, colored pills | Elastic springs + staggered | Karaoke with color pop | Scatter or collapse | -| Medium (corporate, pitch, explainer) | Clean sans, 600-700 | White on dark, muted accent | Clip-path reveal | Karaoke (subtle) | Fade + slide | -| Medium-low (tutorial, educational) | Mono or clean sans, 500-600 | High contrast, minimal | Staggered entrance | Karaoke (minimal scale) | Fade | -| Low (storytelling, cinematic, brand) | Serif or elegant sans, 400-500 | Warm muted tones | 3D rotation | Karaoke (warm tones) | Collapse | - -**How to detect energy from the transcript:** - -- High energy: short sentences, exclamations, repetition ("up, up, up"), emotional vocabulary ("dream", "shine", "believe", "fire"), song lyrics, fast delivery (many words per second) -- Medium energy: declarative statements, product descriptions, mixed sentence length, moderate pacing -- Low energy: long flowing sentences, reflective/introspective language, slow pacing (few words per second), narrative arcs - -When in doubt, **bias toward higher energy**. Boring captions are worse than slightly over-animated ones. - -## Animation Design (Mandatory) - -Before writing any animation code, read [dynamic-techniques.md](./dynamic-techniques.md) for the implementation patterns referenced in the table above. - -**Minimum requirements — every caption composition must have:** - -- At least **2 distinct highlight techniques** — cycle them across groups (e.g., odd groups get elastic pop, even groups get clip-path wipe) -- At least **1 kinetic exit** (scatter, collapse, or drop) — fade-out alone is not acceptable for medium energy or above -- **Karaoke highlight** on every composition — all words visible but muted, each lights up when spoken. This is the baseline, not optional. -- **Emphasis words get special treatment** — words flagged by per-word styling (emotional keywords, ALL CAPS, brand names) must use a different animation than surrounding words (slam, scale-pop with overshoot, or 3D flip) - -**Technique cycling:** never use the same entrance on more than 3 consecutive groups. Rotate techniques using the group index to create variety. Higher energy content should cycle through more techniques. - -**Energy scaling:** the detected energy level controls animation intensity: - -- High: large overshoot (back.out(2.5)), fast timing (0.1-0.2s), 3+ techniques per composition, scatter/drop exits -- Medium: moderate motion (back.out(1.4)), standard timing (0.2-0.4s), 2 techniques, clip-path + fade exits -- Low: gentle reveals (power2.out), slow timing (0.4-0.6s), 1-2 techniques, collapse/fade exits +| Script tone | Font mood | Animation | Color | Size | +| -------------------- | ------------------------------------- | --------------------------------------- | -------------------------------------------- | -------------------- | +| Hype/launch | Heavy condensed, 800-900 weight | Scale-pop, back.out(1.7), fast 0.1-0.2s | Bright accent on dark (cyan, yellow, lime) | Large 72-96px | +| Corporate/pitch | Clean sans-serif, 600-700 weight | Fade + slide-up, power3.out, 0.3s | White/neutral on dark, single muted accent | Medium 56-72px | +| Tutorial/educational | Mono or clean sans, 500-600 weight | Typewriter or gentle fade, 0.4-0.5s | High contrast, minimal color | Medium 48-64px | +| Storytelling/brand | Serif or elegant sans, 400-500 weight | Slow fade, power2.out, 0.5-0.6s | Warm muted tones, low opacity (0.85-0.9) | Smaller 44-56px | +| Social/casual | Rounded sans, 700-800 weight | Bounce, elastic.out, word-by-word | Playful colors, colored backgrounds on pills | Medium-large 56-80px | ## Word Grouping by Tone @@ -222,14 +195,11 @@ tl.seek(0); Place this **before** `window.__timelines[id] = tl` so it runs at composition init. -## Studio Caption Editor Compatibility +## References -The HyperFrames Studio can edit captions in real time, but only if the composition follows these rules: +For dynamic animation techniques (karaoke, clip-path reveals, slam words, scatter exits, elastic entrances, 3D rotation, audio-reactive captions, pretext-based positioning and grouping), see [dynamic-techniques.md](./dynamic-techniques.md). -- **Inline the transcript as `var TRANSCRIPT = [...]`** — the studio's parser extracts the transcript by matching this variable name in the composition source. Using `fetch()` to load transcript data at runtime will NOT be detected. -- **Use JSON-quoted property keys** — write `{ "text": "hello", "start": 0, "end": 1 }` not `{ text: "hello", start: 0, end: 1 }`. The parser's fallback normalization for unquoted keys breaks on apostrophes in words like `didn't`. -- **Use `.caption-group` and `.caption-word` CSS classes** — the studio detects caption elements by these class names. -- **Audio data can be inline or fetched** — only the transcript must be inline. Audio data loaded via `fetch("audio-data.json")` or embedded as `var AUDIO = {...}` both work. +For transcription commands, whisper models, external APIs, and troubleshooting, see [transcript-guide.md](./transcript-guide.md). ## Constraints @@ -237,6 +207,4 @@ The HyperFrames Studio can edit captions in real time, but only if the compositi - **Sync to transcript timestamps.** Words appear when spoken. - **One group visible at a time.** No overlapping caption groups. - **Every caption group must have a hard `tl.set` kill at `group.end`.** Exit animations alone are not sufficient. -- **Never `overflow: hidden` on caption containers or groups.** Glow, shadow, and scale effects paint outside the box — clipping them creates hard visual cutoffs. Always use `overflow: visible`. -- **Music requires audio-reactive captions.** If the source audio is music (any genre, any energy level), extract audio data with `extract-audio-data.py` and use it to modulate group entrance intensity (scale, glow) in the group loop. No special wiring needed — see [dynamic-techniques.md](./dynamic-techniques.md). This is not optional. - **Check project root** for font files before defaulting to Google Fonts. diff --git a/skills/hyperframes-cli/SKILL.md b/skills/hyperframes-cli/SKILL.md index 34613fdc7..d68ed903c 100644 --- a/skills/hyperframes-cli/SKILL.md +++ b/skills/hyperframes-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: hyperframes-cli -description: Use when the user mentions "hyperframes", wants to preview a composition in the studio, render to MP4/WebM, scaffold a new video project, lint or validate a composition, or troubleshoot rendering. Also use after finishing a composition with compose-video — lint and preview are the natural next steps. +description: Preview, render, lint, validate, scaffold, or troubleshoot HyperFrames compositions. Also use after finishing a composition — lint and preview are the natural next steps. --- # HyperFrames CLI diff --git a/skills/hyperframes-compose/SKILL.md b/skills/hyperframes-compose/SKILL.md index ac2a3263a..372eb9a49 100644 --- a/skills/hyperframes-compose/SKILL.md +++ b/skills/hyperframes-compose/SKILL.md @@ -1,6 +1,6 @@ --- name: hyperframes-compose -description: Create HyperFrames HTML video compositions. Use when asked to create a video, build an animation, make a composition, add a title card, or generate any HTML-based video content for HyperFrames. +description: Create video compositions, animations, title cards, or overlays in HyperFrames HTML. Use when asked to build any HTML-based video content. --- # Compose Video