Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hyperframes",
"description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.",
"version": "0.7.3",
"version": "0.7.4",
"author": {
"name": "HeyGen",
"email": "hyperframes@heygen.com",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hyperframes",
"version": "0.7.3",
"version": "0.7.4",
"description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.",
"author": {
"name": "HeyGen",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://cursor.com/schemas/cursor-plugin/plugin.json",
"name": "hyperframes",
"displayName": "HyperFrames by HeyGen",
"version": "0.7.3",
"version": "0.7.4",
"description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.",
"author": {
"name": "HeyGen",
Expand Down
54 changes: 54 additions & 0 deletions docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,60 @@ Recent HyperFrames releases, including user-facing features, fixes, and migratio

{/* New release entries are prepended by `bun run changelog:draft <version> --write`. */}

<Update
label="HyperFrames v0.7.4"
description="Released - 2026-06-24"
tags={["Release", "Producer", "Core", "Studio"]}
>
This stable release improves render reliability and skill authoring: producer duration
now falls back to sub-composition timing, empty sub-composition files no longer abort
renders, and Studio timeline styling handles missing tags safely. It also ships
music-to-video workflow updates, per-skill telemetry, and clearer CLI install/debug
guidance.

## Features

- **CLI:** Per-skill usage telemetry ([4961e4e4](https://github.com/heygen-com/hyperframes/commit/4961e4e4770a9b38eb34fef957c0ac8764614ce8))
- **Skills:** Unify bgm-to-video flows into music-to-video ([a34d3dba](https://github.com/heygen-com/hyperframes/commit/a34d3dba4d15b3d6830bc546b7b6c423a1304477))
- **Skills:** Add bgm-to-video skill ([d596379e](https://github.com/heygen-com/hyperframes/commit/d596379e52536672366e889602004a65bc8136d9))
- **Skills:** Guide install when a matched workflow isn't installed ([08328b04](https://github.com/heygen-com/hyperframes/commit/08328b04e63e16c7bea469237552c1d9587dcfec), [#1647](https://github.com/heygen-com/hyperframes/pull/1647))

## Fixes

- **Producer:** Derive duration from sub-composition timing when root has no data-duration ([45a9440c](https://github.com/heygen-com/hyperframes/commit/45a9440c61073314dd8d3ac34813d108c1e051e7), [#1680](https://github.com/heygen-com/hyperframes/pull/1680))
- **Core:** Skip empty sub-composition files instead of aborting render ([656c1200](https://github.com/heygen-com/hyperframes/commit/656c1200ee2e42fe2affa9641af8fcf0ccbb5683), [#1678](https://github.com/heygen-com/hyperframes/pull/1678))
- **Studio:** Guard against null tag in timeline track style ([d4aa5f93](https://github.com/heygen-com/hyperframes/commit/d4aa5f93e16c564d5248da99cd22101b314d3d63), [#1679](https://github.com/heygen-com/hyperframes/pull/1679))
- **CLI:** Improve whisper-cpp install guidance and add doctor check ([344618e2](https://github.com/heygen-com/hyperframes/commit/344618e22aaf27b526d16ef219dfe193ae8920fd), [#1681](https://github.com/heygen-com/hyperframes/pull/1681))
- **Engine:** Restore fast screenshot path for viewport captures ([f622e5a7](https://github.com/heygen-com/hyperframes/commit/f622e5a7ba3f840bda2703a93784ea0105d21fef), [#1670](https://github.com/heygen-com/hyperframes/pull/1670))
- **CLI:** Install skills into project dir on non-interactive init ([bf4f34b3](https://github.com/heygen-com/hyperframes/commit/bf4f34b359c252bfac25061d19bd178b28da5588), [#1671](https://github.com/heygen-com/hyperframes/pull/1671))
- **CLI:** Expose render debug mode ([7133c396](https://github.com/heygen-com/hyperframes/commit/7133c396eb29f4d28f3d25b8dc8dcf017bc70618))
- Unblock music video ci checks ([5d1bff51](https://github.com/heygen-com/hyperframes/commit/5d1bff51b5a21145b9d84c95d21447ebb90efc13))
- **Producer:** Store css-var-fonts baseline as raw binary, not LFS pointer ([119284d3](https://github.com/heygen-com/hyperframes/commit/119284d377878e38df60469a6bbeaf6d6a8ed857))
- **Producer:** Restore css-var-fonts regression baseline ([f012b8b8](https://github.com/heygen-com/hyperframes/commit/f012b8b84691c6a5989e11c0d228c5320d8d6843))
- **Lint:** Catch CSS↔GSAP transform conflicts in scoped selectors and frame sub-compositions ([9ccae863](https://github.com/heygen-com/hyperframes/commit/9ccae8637291262b8a2e1a1779e4ee7ad186f675))

## Docs & Examples

- **Skills:** Add music-source brief to music-to-video Step 0 ([7a464689](https://github.com/heygen-com/hyperframes/commit/7a4646894f0d3d5edcc185f1e1a61dcce11345ed))
- **Skills:** Register music-to-video in the hyperframes router ([44b04324](https://github.com/heygen-com/hyperframes/commit/44b04324b1576d80a2fb5a1bdf8dda0fd049a6ff))
- **Skills:** Add beat-synced montage authoring recipe ([0dcf914e](https://github.com/heygen-com/hyperframes/commit/0dcf914e14ae74ddfe6e8c6118113929bcfb894f))

## Catalog

- Refine music-to-video planning catalogs ([a23ca348](https://github.com/heygen-com/hyperframes/commit/a23ca3487f712d9df9ed72e1d6b16683dfc4bd2f))

## Internal

- **Lint:** Keep the fallow audit gate green ([4a8e2f1c](https://github.com/heygen-com/hyperframes/commit/4a8e2f1cd0a1d156ac485fd084230da8c407b152))

## Other Changes

- Merge pull request #1672 from heygen-com/fix/skill-authoring-fixes ([26810856](https://github.com/heygen-com/hyperframes/commit/2681085624056aeac851b03fd46efe512017c2a7))
- **Skills:** Apply oxfmt to music-to-video and router docs ([265b0273](https://github.com/heygen-com/hyperframes/commit/265b02738e35d21b5455a640323551f0e3cf3c6e))

[View the full commit range](https://github.com/heygen-com/hyperframes/compare/v0.7.3...v0.7.4).
</Update>

<Update
label="HyperFrames v0.7.3"
description="Released - 2026-06-23"
Expand Down
2 changes: 1 addition & 1 deletion packages/aws-lambda/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/aws-lambda",
"version": "0.7.3",
"version": "0.7.4",
"description": "AWS Lambda adapter for HyperFrames distributed rendering — handler, client-side SDK, and CDK construct.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperframes/cli",
"version": "0.7.3",
"version": "0.7.4",
"description": "HyperFrames CLI — create, preview, and render HTML video compositions",
"repository": {
"type": "git",
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ const commandLoaders = {
skills: () => import("./commands/skills.js").then((m) => m.default),
feedback: () => import("./commands/feedback.js").then((m) => m.default),
telemetry: () => import("./commands/telemetry.js").then((m) => m.default),
events: () => import("./commands/events.js").then((m) => m.default),
validate: () => import("./commands/validate.js").then((m) => m.default),
snapshot: () => import("./commands/snapshot.js").then((m) => m.default),
capture: () => import("./commands/capture.js").then((m) => m.default),
Expand Down Expand Up @@ -194,7 +195,10 @@ let _trackCommandResult:
| undefined;
let _printUpdateNotice: (() => void) | undefined;

if (!isHelp && command !== "telemetry" && command !== "unknown") {
// `events` is a telemetry-internal beacon: it self-tracks + self-flushes, so it
// skips the per-command wrapper (no duplicate cli_command, no first-run notice
// printed into a skill's captured output).
if (!isHelp && command !== "telemetry" && command !== "events" && command !== "unknown") {
import("./telemetry/index.js").then((mod) => {
_flush = mod.flush;
_flushSync = mod.flushSync;
Expand All @@ -206,7 +210,9 @@ if (!isHelp && command !== "telemetry" && command !== "unknown") {
});
}

if (!isHelp && !hasJsonFlag && command !== "upgrade") {
// `events` skips the update check too — a skill-usage beacon must not add
// network latency or trigger a background self-upgrade on the calling skill.
if (!isHelp && !hasJsonFlag && command !== "upgrade" && command !== "events") {
// Report any completed auto-install from the previous run first, before
// kicking off the next check — so the user sees "updated to vX" once and
// we don't over-print.
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ function checkEnvironment(): CheckResult {
return { ok: true, detail: parts.join(" \u00B7 ") };
}

async function checkWhisper(): Promise<CheckResult> {
const { findWhisper, getInstallInstructions } = await import("../whisper/manager.js");
const result = findWhisper();
if (result) {
return { ok: true, detail: result.executablePath };
}
return {
ok: false,
detail: "Not found (optional \u2014 needed for transcription)",
hint: getInstallInstructions(),
};
}

export interface CheckOutcome {
name: string;
ok: boolean;
Expand Down Expand Up @@ -213,6 +226,7 @@ export default defineCommand({
}

checks.push({ name: "Environment", run: checkEnvironment });
checks.push({ name: "whisper-cpp", run: checkWhisper });

const outcomes: CheckOutcome[] = [];
for (const check of checks) {
Expand Down
64 changes: 64 additions & 0 deletions packages/cli/src/commands/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { defineCommand } from "citty";
import { trackEvent, flush } from "../telemetry/client.js";

// Skill-usage telemetry endpoint. A skill reports its own invocation/outcome —
// ideally from its own bundled script, so it fires deterministically rather
// than relying on the agent to remember:
//
// npx hyperframes events --skill=product-launch-video
// npx hyperframes events --skill=product-launch-video --event=skill_completed --outcome=success
//
// Rides the SAME anonymous PostHog pipeline + consent gates as every other CLI
// event (DO_NOT_TRACK / telemetry opt-out, anonymous install UUID, IP stripped).
//
// Telemetry must NEVER break the calling skill: every arg is optional (a missing
// or malformed value is a silent no-op, not a non-zero exit), the body is
// guarded, and flush() carries its own hard timeout. This command always exits 0.

const ALLOWED_EVENTS = ["skill_invoked", "skill_completed"];
const ALLOWED_OUTCOMES = ["success", "error", "abort"];
// Skill names are lowercase slugs (e.g. "product-launch-video"). Anything that
// doesn't match is dropped, so a caller can't push high-cardinality or PII
// strings (paths, shell output, free text) into the anonymous event stream.
const SKILL_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;

export default defineCommand({
meta: {
name: "events",
description:
"Emit an anonymous skill-usage telemetry event (skills report their own invocation/outcome). Honors DO_NOT_TRACK / telemetry opt-out.",
},
args: {
skill: {
type: "string",
description: "Authoring skill slug, e.g. product-launch-video",
},
event: {
type: "string",
description: "Event name: skill_invoked | skill_completed (default: skill_invoked)",
default: "skill_invoked",
},
outcome: {
type: "string",
description: "Optional outcome for completion events: success | error | abort",
},
},
async run({ args }) {
// Best-effort: nothing here may fail the skill that called us. Missing or
// malformed input is a silent no-op rather than a non-zero exit.
try {
const skill = typeof args.skill === "string" ? args.skill.trim() : "";
if (!SKILL_SLUG.test(skill)) return; // missing / non-slug → no-op

const event = ALLOWED_EVENTS.includes(args.event) ? args.event : "skill_invoked";
const props: Record<string, string> = { authoring_skill: skill };
if (args.outcome && ALLOWED_OUTCOMES.includes(args.outcome)) {
props["outcome"] = args.outcome;
}
trackEvent(event, props);
await flush();
} catch {
// swallow — telemetry must never surface a non-zero exit to the caller
}
},
});
24 changes: 20 additions & 4 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,11 +800,27 @@ export default defineCommand({
for (const f of readdirSync(destDir).filter((f) => !f.startsWith("."))) {
console.log(` ${c.accent(f)}`);
}

if (!skipSkills) {
const { installAllSkills } = await import("./skills.js");
// --yes keeps it non-interactive. When Claude Code is driving
// (CLAUDECODE env var), target its native dir so skills land in
// .claude/skills/ instead of only .agents/skills/.
const args = process.env["CLAUDECODE"] ? ["--agent", "claude-code", "--yes"] : ["--yes"];
await installAllSkills({ cwd: destDir, extraArgs: args });
}

console.log();
console.log("Get started:");
console.log();
console.log(` ${c.accent("1.")} Install AI coding skills (one-time):`);
console.log(` ${c.accent("npx skills add heygen-com/hyperframes")}`);
if (skipSkills) {
console.log(` ${c.accent("1.")} Install AI coding skills (one-time):`);
console.log(` ${c.accent("npx skills add heygen-com/hyperframes --yes")}`);
} else {
console.log(
` ${c.accent("1.")} Restart your AI agent (new session) so it loads the skills.`,
);
}
console.log();
console.log(` ${c.accent("2.")} Open this project with your AI coding agent:`);
console.log(
Expand Down Expand Up @@ -1018,8 +1034,8 @@ export default defineCommand({
process.exit(0);
}
if (installSkills) {
const skillsCmd = await import("./skills.js").then((m) => m.default);
await runCommand(skillsCmd, { rawArgs: [] });
const { installAllSkills } = await import("./skills.js");
await installAllSkills({ cwd: destDir });
}
}

Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,21 @@ describe("renderLocal browser GPU config", () => {
expect(producerState.createdJobs[0]?.videoFrameFormat).toBe("png");
});

it("forwards debug mode to createRenderJob", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
quality: "standard",
format: "mp4",
gpu: false,
browserGpuMode: "software",
hdrMode: "auto",
quiet: true,
debug: true,
});

expect(producerState.createdJobs[0]?.debug).toBe(true);
});

it("omits variables from createRenderJob when not provided", async () => {
await renderLocal("/tmp/project", "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ export default defineCommand({
description: "Suppress verbose output",
default: false,
},
debug: {
type: "boolean",
description:
"Write full render diagnostics and keep intermediate artifacts under the producer .debug directory.",
default: false,
},
strict: {
type: "boolean",
description: "Fail render on lint errors",
Expand Down Expand Up @@ -548,6 +554,7 @@ export default defineCommand({
const browserGpuArg = args["browser-gpu"];
const browserGpuMode = resolveBrowserGpuForCli(useDocker, browserGpuArg);
const quiet = args.quiet ?? false;
const debug = args.debug ?? false;
const batchJson = args.json ?? false;
const effectiveQuiet = quiet || (batchPath != null && batchJson);
const strictAll = args["strict-all"] ?? false;
Expand Down Expand Up @@ -763,6 +770,7 @@ export default defineCommand({
pageNavigationTimeoutMs,
protocolTimeout,
playerReadyTimeout,
debug,
exitAfterComplete: false,
throwOnError: true,
skipFeedback: true,
Expand Down Expand Up @@ -815,6 +823,7 @@ export default defineCommand({
videoBitrate,
videoFrameFormat,
quiet,
debug,
variables,
entryFile,
outputResolution,
Expand All @@ -840,6 +849,7 @@ export default defineCommand({
videoFrameFormat,
quiet,
browserPath,
debug,
variables,
entryFile,
outputResolution,
Expand Down Expand Up @@ -876,6 +886,7 @@ interface RenderOptions {
videoBitrate?: string;
videoFrameFormat?: VideoFrameFormat;
quiet: boolean;
debug?: boolean;
browserPath?: string;
variables?: Record<string, unknown>;
entryFile?: string;
Expand Down Expand Up @@ -1124,6 +1135,7 @@ async function renderDocker(
entryFile: options.entryFile,
outputResolution: options.outputResolution,
pageSideCompositing: options.pageSideCompositing,
debug: options.debug,
pageNavigationTimeoutMs: options.pageNavigationTimeoutMs,
},
});
Expand Down Expand Up @@ -1206,7 +1218,7 @@ export async function renderLocal(

const startTime = Date.now();
const logger = createRenderTelemetryLogger(
producer.createConsoleLogger?.("info") ?? createNoopProducerLogger(),
producer.createConsoleLogger?.(options.debug ? "debug" : "info") ?? createNoopProducerLogger(),
);

const job = producer.createRenderJob({
Expand All @@ -1233,6 +1245,7 @@ export async function renderLocal(
variables: options.variables,
entryFile: options.entryFile,
outputResolution: options.outputResolution,
debug: options.debug,
});

const onProgress = options.quiet
Expand Down
Loading