Skip to content

Commit 0fffa66

Browse files
miga-heygenclaude
andcommitted
Merge main into feat/studio-webmcp-frame
Resolve conflicts from squash-merged #3515 (selection tools). Branch retains frame + inspect tools from the stack. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 7e03e0b + 0e558d5 commit 0fffa66

61 files changed

Lines changed: 1324 additions & 179 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "hyperframes",
33
"description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
4-
"version": "0.8.16",
4+
"version": "0.8.20",
55
"author": {
66
"name": "HeyGen",
77
"email": "hyperframes@heygen.com",

.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "hyperframes",
33
"description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
4-
"version": "0.8.16",
4+
"version": "0.8.20",
55
"author": {
66
"name": "HeyGen",
77
"email": "hyperframes@heygen.com",

.cursor-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "hyperframes",
44
"displayName": "HyperFrames by HeyGen",
55
"description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
6-
"version": "0.8.16",
6+
"version": "0.8.20",
77
"author": {
88
"name": "HeyGen",
99
"email": "hyperframes@heygen.com"

docs/changelog.mdx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,77 @@ Recent HyperFrames releases, including user-facing features, fixes, and migratio
88

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

11+
<Update
12+
label="HyperFrames v0.8.20"
13+
description="Released - 2026-08-30"
14+
tags={["Release", "Studio"]}
15+
>
16+
Studio now separates blocked direct edits from real save failures and keeps the file and HTTP details needed to diagnose them.
17+
Repeated failures are grouped, and write tracking now works in more secure browser contexts.
18+
19+
## Fixes
20+
21+
- **Studio:** Correct save failure telemetry ([28be8dddfa](https://github.com/heygen-com/hyperframes/commit/28be8dddfacefe29b5e25ae2d36fd76ffe4c4852), [#3499](https://github.com/heygen-com/hyperframes/pull/3499)).
22+
23+
[View the full commit range](https://github.com/heygen-com/hyperframes/compare/v0.8.19...v0.8.20).
24+
</Update>
25+
26+
<Update
27+
label="HyperFrames v0.8.19"
28+
description="Released - 2026-08-29"
29+
tags={["Release", "Studio"]}
30+
>
31+
Export in Studio now renders the composition you have selected, not the project's root file. Multi-part projects can finally be exported without dropping to the CLI.
32+
33+
## Fixes
34+
35+
- **Studio:** Export the composition the user has selected ([b71f45981](https://github.com/heygen-com/hyperframes/commit/b71f45981c376d3b2b4ae8e07a0837f63a091a8c), [#3550](https://github.com/heygen-com/hyperframes/pull/3550)).
36+
37+
[View the full commit range](https://github.com/heygen-com/hyperframes/compare/v0.8.18...v0.8.19).
38+
</Update>
39+
40+
<Update
41+
label="HyperFrames v0.8.18"
42+
description="Released - 2026-08-29"
43+
tags={["Release", "Studio", "Core", "CLI"]}
44+
>
45+
Studio now switches the canvas and timeline for compositions stored anywhere in a project. Snapshots now preserve playback rate, nested-media timing, static compositions, and source files.
46+
47+
## Fixes
48+
49+
- **Studio:** Switch the canvas and timeline for compositions stored outside `compositions/` ([adf9b0cce](https://github.com/heygen-com/hyperframes/commit/adf9b0cceeb561fd5424e8cad73b781f5383a9ef)).
50+
- **Core:** Keep long nested videos visible when their local start precedes the host window ([3dc185623](https://github.com/heygen-com/hyperframes/commit/3dc18562323c9dd7dfa1f9416d90708677f3652b), [#3535](https://github.com/heygen-com/hyperframes/pull/3535)).
51+
- **CLI:** Capture the correct source frame for trimmed or accelerated videos ([d99eeef0b](https://github.com/heygen-com/hyperframes/commit/d99eeef0b27460683831664b4ac68f5cf4f700cd), [#3536](https://github.com/heygen-com/hyperframes/pull/3536)).
52+
- **CLI:** Reject keyframe-shot outputs that would overwrite the active composition source ([e4dabf830](https://github.com/heygen-com/hyperframes/commit/e4dabf830c6cf377734de043d5f5345bcefe7b02), [#3534](https://github.com/heygen-com/hyperframes/pull/3534)).
53+
- **CLI:** Allow intentional `data-no-timeline` compositions without false frozen-sweep errors ([b28747df0](https://github.com/heygen-com/hyperframes/commit/b28747df0f2db9c5e7148e188a6ce6b99bcdda85), [#3533](https://github.com/heygen-com/hyperframes/pull/3533)).
54+
55+
[View the full commit range](https://github.com/heygen-com/hyperframes/compare/v0.8.17...v0.8.18).
56+
</Update>
57+
58+
<Update
59+
label="HyperFrames v0.8.17"
60+
description="Released - 2026-08-28"
61+
tags={["Release", "Studio", "Producer", "Engine"]}
62+
>
63+
Studio now exposes live editing state to agentic browsers, including browsers without native WebMCP. Renders now fail instead of silently succeeding when sub-compositions, media extraction, or artifact validation break, while nested runtime videos keep distinct identities.
64+
65+
## Features
66+
67+
- **Studio:** Fall back to a WebMCP polyfill where the browser has none ([097d901d7](https://github.com/heygen-com/hyperframes/commit/097d901d703be01781a47f2a8aa2dc65e0a3e19e), [#3514](https://github.com/heygen-com/hyperframes/pull/3514)).
68+
- **Studio:** Expose Studio's live state to an agentic browser (WebMCP) ([94da403d6](https://github.com/heygen-com/hyperframes/commit/94da403d6d9e3bdf23665794572a90918832036d), [#3511](https://github.com/heygen-com/hyperframes/pull/3511)).
69+
70+
## Fixes
71+
72+
- **Producer:** Enforce video extraction failures by default (#3372) ([4d87f8bba](https://github.com/heygen-com/hyperframes/commit/4d87f8bbae3d4cda71f75fa33e8d48ce38a3afc7), [#3526](https://github.com/heygen-com/hyperframes/pull/3526)).
73+
- **Core,producer:** Stamp render ids on empty-src media and pair the snapshot by them ([9eb84c91b](https://github.com/heygen-com/hyperframes/commit/9eb84c91b6a785028d6bc4c122b633bda0741c7f), [#3513](https://github.com/heygen-com/hyperframes/pull/3513)).
74+
- **Engine:** Fail render on sub-composition script failures (#3352) ([e69be30e9](https://github.com/heygen-com/hyperframes/commit/e69be30e985a32a70d04de607763135884246193), [#3528](https://github.com/heygen-com/hyperframes/pull/3528)).
75+
- **Producer:** Assert render artifact duration and frame count before commit ([05275c1e8](https://github.com/heygen-com/hyperframes/commit/05275c1e8ce3bee6d331cca9ff9c8eec02c34977), [#3506](https://github.com/heygen-com/hyperframes/pull/3506)).
76+
- **Core:** Read CSS animation opacity through color grading's own hide (#3329) ([e5c7dc75e](https://github.com/heygen-com/hyperframes/commit/e5c7dc75e8ae7146d81d07e1f0e0731d4ad9eb6c), [#3507](https://github.com/heygen-com/hyperframes/pull/3507)).
77+
- **Studio:** Let a failed DOM edit report that it failed ([21bcd5745](https://github.com/heygen-com/hyperframes/commit/21bcd5745cc0d8bb4c51363ef4183b343cbbd169), [#3510](https://github.com/heygen-com/hyperframes/pull/3510)).
78+
79+
[View the full commit range](https://github.com/heygen-com/hyperframes/compare/v0.8.16...v0.8.17).
80+
</Update>
81+
1182
<Update
1283
label="HyperFrames v0.8.16"
1384
description="Released - 2026-08-27"

packages/aws-lambda/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hyperframes/aws-lambda",
3-
"version": "0.8.16",
3+
"version": "0.8.20",
44
"description": "AWS Lambda adapter for HyperFrames distributed rendering — handler, client-side SDK, and CDK construct.",
55
"repository": {
66
"type": "git",

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hyperframes/cli",
3-
"version": "0.8.16",
3+
"version": "0.8.20",
44
"description": "HyperFrames CLI — create, preview, and render HTML video compositions",
55
"license": "Apache-2.0",
66
"repository": {

packages/cli/src/commands/check.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ function fakeDriver(overrides: Partial<CheckAuditDriver> = {}): CheckAuditDriver
145145
return {
146146
initialize: vi.fn(async (_contrast: boolean) => undefined),
147147
getDuration: vi.fn(async () => 9),
148+
hasNoTimelineDeclaration: vi.fn(async () => false),
148149
getTransitionBoundaries: vi.fn(async () => []),
149150
getCanvas: vi.fn(async () => ({ width: 1920, height: 1080 })),
150151
findAmbiguousSelectors: vi.fn(async (_selectors: string[]) => []),
@@ -1222,6 +1223,18 @@ describe("check pipeline", () => {
12221223
).toBe(true);
12231224
});
12241225

1226+
it("does not flag intentional static content declared with data-no-timeline", async () => {
1227+
const driver = fakeDriver({
1228+
getDuration: vi.fn(async () => 6),
1229+
hasNoTimelineDeclaration: vi.fn(async () => true),
1230+
collectLayoutGeometry: vi.fn(async () => "frozen"),
1231+
});
1232+
1233+
const { report } = await runScenario(driver);
1234+
1235+
expect(report.layout.findings.some((finding) => finding.code === "sweep_static")).toBe(false);
1236+
});
1237+
12251238
it("does not flag a 1.5s static title card — too short for the guard to apply", async () => {
12261239
const driver = fakeDriver({
12271240
getDuration: vi.fn(async () => 1.5),

packages/cli/src/commands/keyframes.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
1+
import { existsSync, linkSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import { beforeAll, describe, expect, it } from "vitest";
@@ -28,6 +28,29 @@ describe("keyframes direct composition scope", () => {
2828
});
2929

3030
describe("keyframes shot output", () => {
31+
it("rejects an output path that would overwrite the composition source", () => {
32+
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-source-"));
33+
const sourcePath = join(projectDir, "index.html");
34+
writeFileSync(sourcePath, wrap(""));
35+
36+
expect(() => ensureShotOutputDir(sourcePath, sourcePath)).toThrow(
37+
/must not overwrite the composition source/,
38+
);
39+
expect(readFileSync(sourcePath, "utf8")).toBe(wrap(""));
40+
});
41+
42+
it("rejects an existing output alias that refers to the composition source", () => {
43+
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-alias-"));
44+
const sourcePath = join(projectDir, "index.html");
45+
const aliasPath = join(projectDir, "shot.png");
46+
writeFileSync(sourcePath, wrap(""));
47+
linkSync(sourcePath, aliasPath);
48+
49+
expect(() => ensureShotOutputDir(aliasPath, sourcePath)).toThrow(
50+
/must not overwrite the composition source/,
51+
);
52+
});
53+
3154
it("creates a missing parent directory before writing --shot", () => {
3255
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-dir-"));
3356
const outputDir = join(projectDir, "nested", "proofs");

packages/cli/src/commands/motionShot.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
// exactly what it's editing. All geometry + SVG live in ./motionShotLayout.ts
1111
// (pure, tested); this file only drives the browser and SAMPLES.
1212

13-
import { mkdirSync, writeFileSync } from "node:fs";
14-
import { dirname } from "node:path";
13+
import { mkdirSync, statSync, writeFileSync } from "node:fs";
14+
import { dirname, resolve } from "node:path";
1515
import { resolveDiagnosticNavigationTimeoutMs } from "../utils/renderArgs.js";
1616
import { resolveCompositionViewportFromHtml } from "../utils/compositionViewport.js";
1717
import {
@@ -33,7 +33,25 @@ export interface ShotRequest {
3333
selector: string;
3434
}
3535

36-
export function ensureShotOutputDir(outPath: string): void {
36+
function pathsReferToSameFile(firstPath: string, secondPath: string): boolean {
37+
const first = resolve(firstPath);
38+
const second = resolve(secondPath);
39+
if (first === second) return true;
40+
try {
41+
const firstStat = statSync(first);
42+
const secondStat = statSync(second);
43+
return firstStat.dev === secondStat.dev && firstStat.ino === secondStat.ino;
44+
} catch {
45+
return false;
46+
}
47+
}
48+
49+
export function ensureShotOutputDir(outPath: string, sourcePath?: string): void {
50+
if (sourcePath && pathsReferToSameFile(outPath, sourcePath)) {
51+
throw new Error(
52+
`--shot output must not overwrite the composition source: ${sourcePath}. Choose a separate .png path.`,
53+
);
54+
}
3755
mkdirSync(dirname(outPath), { recursive: true });
3856
}
3957

@@ -644,7 +662,7 @@ export async function captureMotionPathShot(
644662
outPath: string,
645663
opts: ShotOptions = {},
646664
): Promise<string> {
647-
ensureShotOutputDir(outPath);
665+
ensureShotOutputDir(outPath, resolve(projectDir, opts.entryFile ?? "index.html"));
648666
let requests = requestsIn;
649667
const samples = Math.max(1, Math.min(60, opts.samples ?? 9));
650668
const layout = opts.layout ?? "path";

packages/cli/src/commands/snapshot.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import snapshotCommand, {
2929
requireSnapshotFfmpeg,
3030
resolveSnapshotVideoClipStart,
3131
resolveSnapshotVideoFrameTime,
32+
resolveSnapshotVideoPlaybackRate,
3233
tailFrameTime,
3334
} from "./snapshot.js";
3435

@@ -262,6 +263,12 @@ describe("resolveSnapshotVideoClipStart", () => {
262263
});
263264
});
264265

266+
describe("resolveSnapshotVideoPlaybackRate", () => {
267+
it("prefers the authored data-playback-rate over the browser default", () => {
268+
expect(resolveSnapshotVideoPlaybackRate({ authoredRate: "1.8", defaultRate: 1 })).toBe(1.8);
269+
});
270+
});
271+
265272
describe("computeSnapshotTimes (FINDING [7]: tail is always captured)", () => {
266273
it("default frames: last point is the readable tail, never exact duration", () => {
267274
const { times, appendedTail } = computeSnapshotTimes(8, { frames: 5 });

0 commit comments

Comments
 (0)