Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
58ef24c
chore: release v0.9.44 (#7708)
miguel-heygen Nov 14, 2026
2f0ade1
fix(core): at full quality a 4K clip plays a copy at the size the pre…
miguel-heygen Nov 14, 2026
fe8d930
chore: release v0.9.45 (#7729)
miguel-heygen Nov 14, 2026
ff9c3b7
fix(engine): an unrecognised capture failure is unknown, not the auth…
miguel-heygen Nov 14, 2026
b5e9bc6
fix(studio-server): an edit to a read-only project file is refused an…
miguel-heygen Nov 14, 2026
4313241
chore: release v0.9.46 (#7732)
miguel-heygen Nov 14, 2026
c31e7dd
fix(cli): check times a line held across a split clip as one hold (#7…
miguel-heygen Nov 14, 2026
32f562b
feat(core): a folder with index.html is one a step can work on, apart…
miguel-heygen Nov 14, 2026
35d1451
fix(cli): check no longer reports GPU stalls from its own pixel reads…
miguel-heygen Nov 14, 2026
e21e64f
fix(registry): caption-highlight clears a short word's highlight when…
miguel-heygen Nov 14, 2026
88beb28
fix(cli): an unknown flag lists the command's real flags (#7740)
miguel-heygen Nov 14, 2026
d26ec25
fix(cli): check still reports a film's other GL driver notes (#7745)
miguel-heygen Nov 14, 2026
b723874
fix(studio): HEVC with long-term reference pictures keeps its pixel a…
miguel-heygen Nov 14, 2026
b746a0e
fix: check, render and downloads work on a drive without hard links (…
miguel-heygen Nov 14, 2026
cb52dcf
fix(cli): an unknown flag's error leaves out hidden rename shims (#7746)
miguel-heygen Nov 14, 2026
a92498b
fix(studio-server): save an edit Windows briefly holds without freezi…
miguel-heygen Nov 14, 2026
a3fa250
fix(producer): a clip whose file name has a space or accent is mixed …
miguel-heygen Nov 14, 2026
691bb0c
fix(core): a clip the preview lets go of stays unloaded in Firefox
robinebers Nov 14, 2026
7e59d73
fix(player): a film cut from one long recording plays through in Chro…
robinebers Nov 14, 2026
87b8d69
chore(player): raise the packed size budget for the frame name
robinebers Nov 14, 2026
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 capture for HyperFrames.",
"version": "0.9.43",
"version": "0.9.46",
"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,7 +1,7 @@
{
"name": "hyperframes",
"description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
"version": "0.9.43",
"version": "0.9.46",
"author": {
"name": "HeyGen",
"email": "hyperframes@heygen.com",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "hyperframes",
"displayName": "HyperFrames by HeyGen",
"description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.",
"version": "0.9.43",
"version": "0.9.46",
"author": {
"name": "HeyGen",
"email": "hyperframes@heygen.com"
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 59 additions & 0 deletions docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,65 @@ 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.9.46"
description="Released - 2026-11-14"
tags={["Release", "Studio Server", "Engine"]}
>
Editing a project file that is read-only is now refused, and the file stays as it was. A capture failure the engine does not recognise is now reported as unknown, not blamed on the project.

## Fixes

- **Studio Server:** An edit to a read-only project file is refused and leaves it unchanged ([b5e9bc689](https://github.com/heygen-com/hyperframes/commit/b5e9bc6893a7a5b4ab306151c362f46b1b24e4e1), [#7731](https://github.com/heygen-com/hyperframes/pull/7731)).
- **Engine:** An unrecognised capture failure is unknown, not the author's fault ([ff9c3b761](https://github.com/heygen-com/hyperframes/commit/ff9c3b7619c6c46c3145b287914cd09b377174f9), [#7730](https://github.com/heygen-com/hyperframes/pull/7730)).

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

<Update
label="HyperFrames v0.9.45"
description="Released - 2026-11-14"
tags={["Release", "Core"]}
>
A preview of 4K takes uses far less memory at full quality. Each 4K clip now plays a copy at the size the preview shows it, so long edits of 4K footage no longer run out of memory. Exports still use the original files.

## Fixes

- **Core:** At full quality a 4K clip plays a copy at the size the preview shows it ([2f0ade181](https://github.com/heygen-com/hyperframes/commit/2f0ade181585323ebb2b53a2e15d68098655c90e), [#7723](https://github.com/heygen-com/hyperframes/pull/7723)).

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

<Update
label="HyperFrames v0.9.44"
description="Released - 2026-11-14"
tags={["Release", "Studio", "CLI", "Core"]}
>
Check now warns when text leaves the safe areas of a 9:16 social video. Previews free a clip's decoder once the playhead leaves it, and a cancelled audio fade no longer crashes the page. Set HYPERFRAMES_OWNER to tell which run started a leftover browser.

## Features

- **CLI:** Name a caller's owner id on the browsers and warm check it starts ([798c241e4d0cf](https://github.com/heygen-com/hyperframes/commit/798c241e4d0cf18fc328d90c41823a766a9d8d8e), [#7687](https://github.com/heygen-com/hyperframes/pull/7687)).
- **CLI:** Check warns when text leaves the 9:16 social safe areas ([45882027bd284](https://github.com/heygen-com/hyperframes/commit/45882027bd284a8fbd96ffe854184104304c224f), [#7680](https://github.com/heygen-com/hyperframes/pull/7680)).

## Fixes

- **Studio:** Edit clips added to a project outside Studio ([cb072eb047efa](https://github.com/heygen-com/hyperframes/commit/cb072eb047efafe9749ae81613ccbb8036be3e21), [#7686](https://github.com/heygen-com/hyperframes/pull/7686)).
- **Core:** Stop a cancelled audio envelope from crashing the page ([49623307ce86c](https://github.com/heygen-com/hyperframes/commit/49623307ce86c030f8016c3e21b6aa1120806d5c), [#7682](https://github.com/heygen-com/hyperframes/pull/7682)).
- **Core:** Free a preview clip's decoder when the playhead leaves it ([f7b49f12078e1](https://github.com/heygen-com/hyperframes/commit/f7b49f12078e14cdc401808edd4f217df8d24ce4), [#7679](https://github.com/heygen-com/hyperframes/pull/7679)).

## Performance

- **Core:** A reloaded preview video resyncs only itself ([92816ed56bde0](https://github.com/heygen-com/hyperframes/commit/92816ed56bde07affff0f6e0a5bce3dd96a96808), [#7684](https://github.com/heygen-com/hyperframes/pull/7684)).

## Catalog

- **CLI:** Report an invalid catalog --type as a usage error ([affa03b60fb08](https://github.com/heygen-com/hyperframes/commit/affa03b60fb082c93b1efa9341ac85a106dda61e), [#7678](https://github.com/heygen-com/hyperframes/pull/7678)).
- Publish the hosted HyperFrames MCP to the official MCP registry ([0f6ed7d57bc9c](https://github.com/heygen-com/hyperframes/commit/0f6ed7d57bc9c6d449ce89cbfc3a7948a7427157), [#7676](https://github.com/heygen-com/hyperframes/pull/7676)).

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

<Update
label="HyperFrames v0.9.43"
description="Released - 2026-11-13"
Expand Down
8 changes: 8 additions & 0 deletions docs/contributing/testing-local-changes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ npx ./hyperframes-<version>.tgz --help

Run the archive against an isolated project as well as the command you changed.

A `hyperframes preview` started through `npx` keeps its server running when the
`npx` process is stopped. Stop the server by the process listening on its port,
or the next run talks to the old one:

```bash
kill "$(lsof -ti tcp:<port> -sTCP:LISTEN)"
```

## What to verify

Test the user-visible outcome, not only the exit code:
Expand Down
4 changes: 2 additions & 2 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ machine-readable `code` and, when there is a next step, a `hint`:
"ok": false,
"error": {
"code": "unknown_flag",
"message": "Unknown flag: --out",
"hint": "Did you mean --output? Run 'hyperframes render --help' to see its flags."
"message": "Unknown flag: --search",
"hint": "Did you mean --query? Its flags: --type, --tag, --json, --human-friendly, --query, --yes, --on-device. Run 'hyperframes catalog --help' for what each does."
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "hyperframes",
"version": "0.9.43",
"version": "0.9.46",
"description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames."
}
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.9.43",
"version": "0.9.46",
"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.9.43",
"version": "0.9.46",
"description": "Make videos with code and AI agents: write HTML, render MP4. Open source.",
"keywords": [
"ai-agents",
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/src/registry/captionLastGroup.chromium.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,42 @@ describe.runIf(executablePath)("caption-highlight last group in Chromium", () =>
expect(await lastGroupVisibleAt(LAST_WORD_END_S - 0.05)).toBe(true);
});
});

describe.runIf(executablePath)("caption-highlight short word in Chromium", () => {
let browser: Browser;

beforeAll(async () => {
browser = await puppeteer.launch({ executablePath, args: ["--no-sandbox"] });
});

afterAll(async () => {
await browser?.close();
});

function componentWithShortWord(): string {
const demoWord = '{ text: "great", start: 0.3, end: 0.55 }';
if (!componentHtml.includes(demoWord)) throw new Error("the demo's second word changed");
return componentHtml
.replace(demoWord, '{ text: "great", start: 0.3, end: 0.34 }')
.replace(/<script src="https:[^"]*gsap[^"]*"><\/script>/, `<script>${gsapScript}</script>`);
}

// fails without the fix: a frame inside the 0.01 s tail relit the pill until the phrase ended.
it("clears a 0.04 s word's highlight once the word is over", async () => {
const page = await browser.newPage();
try {
await page.setContent(componentWithShortWord());
const opacity = await page.evaluate(() => {
const timeline = (
window as unknown as { __timelines: Record<string, { seek(t: number): void }> }
).__timelines["caption-highlight"];
for (const time of [0, 0.3, 0.333, 0.367, 0.4, 0.433, 0.445, 0.467, 0.5])
timeline!.seek(time);
return Number(getComputedStyle(document.getElementById("hl-bg-1")!).opacity);
});
expect(opacity).toBe(0);
} finally {
await page.close();
}
});
});
37 changes: 37 additions & 0 deletions packages/cli/src/utils/checkBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,43 @@ it("elevates and deduplicates WebGPU validation warnings while preserving ordina
);
});

// fails without the narrowing: every GL driver Performance note was hidden, not only the ReadPixels stall.
it("hides only the ReadPixels stall check's own reads cause, not the film's other GL driver notes", async () => {
vi.spyOn(Date, "now").mockReturnValue(100);
mountCanvasFixture();
const page = fakePage();
const stall = fakeConsoleMessage(
"warn",
"[.WebGL-0x2c84001d4800]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels",
);
const ordinaryWarning = fakeConsoleMessage("warn", "Optional caption font was not loaded");
const otherDriverNote = fakeConsoleMessage(
"warn",
"[.WebGL-0x2c84001d4800]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): Program undefined behavior warning",
);
page.on = vi.fn(
(event: string, handler: (message: ReturnType<typeof fakeConsoleMessage>) => void) => {
if (event === "console") {
handler(stall);
handler(otherDriverNote);
handler(ordinaryWarning);
}
},
);
installSessionMock(page);

const result = await runBrowserCheck(
PROJECT,
{ ...DEFAULT_CHECK_OPTIONS, samples: 1, contrast: false },
{ kind: "none" },
runAuditGrid,
);

expect(
result.runtimeFindings.filter(({ code }) => code === "console_warning").map((f) => f.message),
).toEqual([otherDriverNote.text(), ordinaryWarning.text()]);
});

describe("collectSeekClock", () => {
afterEach(() => {
Reflect.deleteProperty(window, "__timelines");
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/utils/checkBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,9 @@ function runtimeInfoFindingCode(text: string): string | null {
: "media_proxy_fallback";
}

const READ_PIXELS_STALL =
/^\[\.WebGL-[^\]]*\]GL Driver Message \([^)]*\): GPU stall due to ReadPixels/;

function wireRuntimeListeners(page: Page, drafts: RuntimeDraft[], clock: RuntimeClock): void {
followSampleTimeMarkers(page, clock.at);
page.on("console", (message) => {
Expand All @@ -885,6 +888,7 @@ function wireRuntimeListeners(page: Page, drafts: RuntimeDraft[], clock: Runtime
line: location.lineNumber,
});
} else if (type === "warn") {
if (READ_PIXELS_STALL.test(text)) return;
const location = message.location();
const webGpuFailure = isWebGpuRuntimeFailure(text);
pushRuntimeDraft(drafts, {
Expand Down
23 changes: 22 additions & 1 deletion packages/cli/src/utils/craftAudit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ interface Shown {
block?: number;
/** The block's rendered text; defaults to `text`. */
blockText?: string;
/** Top edge in px; defaults to a row per key. */
top?: number;
}

function part(shown: Shown): CraftTextPart {
Expand All @@ -40,7 +42,7 @@ function part(shown: Shown): CraftTextPart {
blockText: shown.blockText ?? shown.text,
opacity: 1,
fontPx,
rect: { left: 100, top: 100 + shown.key * 10, width: 800, height: fontPx * 1.2 },
rect: { left: 100, top: shown.top ?? 100 + shown.key * 10, width: 800, height: fontPx * 1.2 },
captions: shown.captions ?? false,
};
}
Expand Down Expand Up @@ -218,6 +220,25 @@ describe("line_unread", () => {
expect(await detectLineUnread(film(12, shown), 12, CANVAS, probeFor(shown))).toEqual([]);
});

// fails without the fix: each instance of a split clip was timed alone, so its 0.5 s first half warned.
it("times a line held across a split clip's two instances as one hold", async () => {
const line = "Your reminder card holds here";
const shown: Shown[] = [
{ key: 1, text: line, from: 3, to: 3.5, top: 800 },
{ key: 2, text: line, from: 3.5, to: 8, top: 800 },
];
expect(await detectLineUnread(film(8, shown), 8, CANVAS, probeFor(shown))).toEqual([]);
});

it("still times a different line that takes over the same place on its own", async () => {
const shown: Shown[] = [
{ key: 1, text: "Every team gets one shared calendar now", from: 3, to: 3.5, top: 800 },
{ key: 2, text: "Start your free trial today", from: 3.5, to: 8, top: 800 },
];
const findings = await detectLineUnread(film(8, shown), 8, CANVAS, probeFor(shown));
expect(findings.map((finding) => finding.selector)).toEqual(["#t1"]);
});

it("skips text too small to be a reading line", async () => {
const shown: Shown[] = [
{ key: 1, text: "Legal fine print that flashes by", from: 1, to: 1.3, fontPx: 18 },
Expand Down
Loading
Loading