Skip to content

feat(studio): support GIF export with loop count - #5593

Merged
miguel-heygen merged 3 commits into
heygen-com:mainfrom
SophiaHuangCc:feat/5552-studio-gif-render
Oct 26, 2026
Merged

miguel-heygen merged 3 commits into
heygen-com:mainfrom
SophiaHuangCc:feat/5552-studio-gif-render

Conversation

@SophiaHuangCc

@SophiaHuangCc SophiaHuangCc commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

Add GIF export to Studio, including configurable loop counts, image previews, downloads, deletion, and cancellation cleanup.

Why

GIF rendering already works through the CLI, but Studio only offers MP4, WebM, and MOV. This lets Studio users export GIFs for documentation, READMEs, and chat without switching to the terminal.

Related work

Fixes #5552.
No overlapping PRs were found during the initial issue review.

How

Added GIF to Studio’s format picker and render API.
Added optional gifLoop validation: integers from 0 to 65535, with 0 meaning infinite looping.
Forwarded GIF settings through both server adapters to the existing producer.
Updated render history, MIME types, file serving, deletion, and metadata paths to recognize .gif.
Used image previews for completed GIFs.
Ensured cancellation removes both the GIF output and its metadata sidecar, including cancellation racing with completion.
Extracted render UI components and shared test helpers to meet repository size, complexity, and duplication checks.

Test plan

How was this tested?

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

Before

Built from upstream main a11280eef: Studio offers MP4, MOV, and WebM, with no GIF export option.

Before: main-branch format picker

After

Built from PR commit 1d74ae52b: GIF selected with loop count 3, a 30 fps limit, and the completed GIF in the render list.

After: completed Studio GIF export

After: 60 fps disabled for GIF

Actual GIF exported from Studio

Actual downloaded Studio GIF

Chrome automation clicked Export in Studio and downloaded this file. FFprobe reports 640 x 360, 3 seconds, 90 frames; the Netscape extension records loop count 3. All 90 decoded frames differ, the image preview loads, downloaded bytes match the saved file, and the metadata sidecar is separate. Machine-readable evidence.

Rebased onto current main with both upstream motion-blur tests and GIF tests retained. Added a regression test confirming renders/*.gif is excluded from project history by current main, and documented Studio GIF export. Local verification: workspace build/typechecks, lint/format and all pre-commit hooks; 1,509 server tests, 53 Studio render tests, and 63 CLI tests passed. Mintlify validation and broken-link checks passed. This is local verification; GitHub CI still needs to complete.

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a clean, well-scoped change. It widens every spot the issue lists, and the tests cover the route, the list/serve/delete path, and cancel cleanup, including the race where cancel lands after completion. One behavior gap needs fixing, and the branch needs a rebase.

Should fix: GIF fps is not capped in Studio

The CLI caps GIF output at 30 fps (packages/cli/src/commands/render/plan.ts:281-285) and records that it did (gifFpsCapped). The producer does not cap it. Studio still offers 60 fps for GIF (RenderQueue.tsx FPS_OPTIONS), and neither routes/render.ts nor the adapters clamp it. So a Studio GIF at 60 fps is encoded with ~1.67 cs frame delays. Browsers stretch delays below 2 cs to about 10 cs, so the GIF plays far too slowly, including in Studio's own <img> preview. That also misses the issue's "same frame count as the CLI" check. The format tooltip already promises "up to 30 fps" (renderSettings.ts FORMAT_INFO.gif), but nothing enforces it.

Suggested fix: clamp fps to 30 for format === "gif" on the server, in routes/render.ts next to the fps parse, so API callers get the cap too. In the UI, hide or disable the 60 fps option when GIF is selected. A route test that sends { format: "gif", fps: 60 } and expects fps 30/1 in startRender would lock it in. Optionally, pass a gifFpsCapped flag through to Studio render telemetry, as the CLI does. This PR removes the comment in studioRenderTelemetry.ts that explained why that field was absent.

Rebase needed

The PR currently conflicts with main. The only conflict is packages/cli/src/server/studioServer.test.ts: your two GIF tests and a CPU-sampler test added on main were appended at the same spot. Keep both sides. studioRenderTelemetry.ts and studioServer.ts merge cleanly.

Checked and fine

  • gifLoop validation (helpers/encoderOptions.ts) rejects non-integers and values outside 0 to 65535 with a 400, and ignores the field for non-GIF formats. The tests cover both cases.
  • Both adapters forward gifLoop only when set, and their extension regexes now include .gif for the sidecar and cancel cleanup. A completed GIF writes out.meta.json and doesn't clobber the output.
  • GPU encoding stays MP4-only (startConfiguredRender forces gpu: false for GIF, and the route already gates on format === "mp4").
  • I verified the PR's claim that the list, serve, and delete paths recognize .gif. They go through RENDER_MIME/RENDER_EXTENSIONS, and the job-id strip regex was widened.

What I ran: read the full changed files, checked git merge-tree against current main, and compared against the CLI's GIF path. I did not run the tests locally. CI hasn't run on this fork PR yet.

— Rames

@SophiaHuangCc
SophiaHuangCc force-pushed the feat/5552-studio-gif-render branch from 90d0ad7 to ad9ce86 Compare October 19, 2026 03:10
@SophiaHuangCc

Copy link
Copy Markdown
Contributor Author

Thanks, this is a clean, well-scoped change. It widens every spot the issue lists, and the tests cover the route, the list/serve/delete path, and cancel cleanup, including the race where cancel lands after completion. One behavior gap needs fixing, and the branch needs a rebase.

Should fix: GIF fps is not capped in Studio

The CLI caps GIF output at 30 fps (packages/cli/src/commands/render/plan.ts:281-285) and records that it did (gifFpsCapped). The producer does not cap it. Studio still offers 60 fps for GIF (RenderQueue.tsx FPS_OPTIONS), and neither routes/render.ts nor the adapters clamp it. So a Studio GIF at 60 fps is encoded with ~1.67 cs frame delays. Browsers stretch delays below 2 cs to about 10 cs, so the GIF plays far too slowly, including in Studio's own <img> preview. That also misses the issue's "same frame count as the CLI" check. The format tooltip already promises "up to 30 fps" (renderSettings.ts FORMAT_INFO.gif), but nothing enforces it.

Suggested fix: clamp fps to 30 for format === "gif" on the server, in routes/render.ts next to the fps parse, so API callers get the cap too. In the UI, hide or disable the 60 fps option when GIF is selected. A route test that sends { format: "gif", fps: 60 } and expects fps 30/1 in startRender would lock it in. Optionally, pass a gifFpsCapped flag through to Studio render telemetry, as the CLI does. This PR removes the comment in studioRenderTelemetry.ts that explained why that field was absent.

Rebase needed

The PR currently conflicts with main. The only conflict is packages/cli/src/server/studioServer.test.ts: your two GIF tests and a CPU-sampler test added on main were appended at the same spot. Keep both sides. studioRenderTelemetry.ts and studioServer.ts merge cleanly.

Checked and fine

  • gifLoop validation (helpers/encoderOptions.ts) rejects non-integers and values outside 0 to 65535 with a 400, and ignores the field for non-GIF formats. The tests cover both cases.
  • Both adapters forward gifLoop only when set, and their extension regexes now include .gif for the sidecar and cancel cleanup. A completed GIF writes out.meta.json and doesn't clobber the output.
  • GPU encoding stays MP4-only (startConfiguredRender forces gpu: false for GIF, and the route already gates on format === "mp4").
  • I verified the PR's claim that the list, serve, and delete paths recognize .gif. They go through RENDER_MIME/RENDER_EXTENSIONS, and the job-id strip regex was widened.

What I ran: read the full changed files, checked git merge-tree against current main, and compared against the CLI's GIF path. I did not run the tests locally. CI hasn't run on this fork PR yet.

— Rames

Thanks for the review! Addressed the GIF frame-rate issue in ad9ce86: the server now caps GIF requests at 30 fps, and Studio disables the 60 fps option, including handling previously saved settings. Added regression tests and verified Studio/CLI output parity for a 60 fps request.
I also rebased onto current main, preserving both the GIF tests and the upstream CPU-sampler test. The build, relevant tests, and pre-commit checks pass locally. Could you take another look when you have time? Thank you!

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at ad9ce86. The frame-rate fix is what was asked for. The route clamps GIF requests above 30 fps to 30/1 right after the fps parse, so API callers get the cap too, and the table test covers 60, 59.94 and just-over-30 being clamped, 29.97/30/24 passing through, and the other formats untouched. In the UI, 60 fps is disabled for GIF and a saved 60 resolves to 30. I also checked that every render path on the merge result (route MIME and extension lists, the job-id strip, both adapters' sidecar and cancel cleanup) now handles .gif.

Before this can merge:

  1. A small rebase. main added motion blur tests to packages/studio/vite.adapter.render.test.ts at the same spot as yours, so that file conflicts. Keep both sides. The source files merge cleanly.
  2. Before/After captures in the description. The required "Studio and player captures" check reads the PR body, and since this changes Studio .tsx files it needs ## Before and ## After sections, each with an image or video (for example the format picker with GIF and the loop count field, and a finished GIF in the render list). Without them that check fails once CI runs. CI hasn't run on this fork yet.

One smaller thing, from reading the code rather than running it: with the CLI's Studio server, renders are written to <project>/renders, and project history walks that folder. History hashes video and audio in place but copies other files under 64 MiB into its own store (packages/studio-server/src/history/uncopied.ts). .mp4/.webm/.mov are in that media list and .gif isn't, so each GIF export would also be copied into the project's history. It might be worth leaving render outputs out of history, or treating them as uncopied. It's fine as a follow-up if the maintainers prefer.

Verdict: COMMENT
Reasoning: The requested fps cap is correctly done and tested, but the branch needs a one-file rebase and the description needs Before/After captures for a required check; CI has not run on this head.

@jrusso1020

Copy link
Copy Markdown
Collaborator

We want GIF export. Please rebase on main and add Before/After captures, then we'll review again.

@SophiaHuangCc
SophiaHuangCc force-pushed the feat/5552-studio-gif-render branch from ad9ce86 to 1d74ae5 Compare October 26, 2026 07:05
@mintlify

mintlify Bot commented Oct 26, 2026

Copy link
Copy Markdown

Preview approval required

This pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit 1d74ae5.

Approve preview

@SophiaHuangCc

Copy link
Copy Markdown
Contributor Author

Thanks for the re-review! Rebased onto current main, keeping both the motion-blur tests and the GIF tests. The required Before and After captures are now in the PR description.

I also ran the built app in Chrome, selected GIF with loop count 3, clicked Export, and downloaded this actual output:

GIF exported from Studio

Verified: 640 x 360, 3 seconds, 90 frames at 30 fps, loop count 3; all 90 frames contain distinct motion, the preview loads, and downloaded bytes match the saved render. Before/After captures and evidence.

For the history concern, current main already excludes the renders/ directory. I added a regression test proving a GIF export is left out of history without changing undo behavior for GIF source assets. I also added the Studio GIF workflow to the rendering guide. Local checks pass: 1,509 server tests, 53 Studio tests, 63 CLI tests, build/typechecks, lint/format, commit hooks, and docs validation/link checks.

Latest commit: 1d74ae5. Ready for another review when convenient; please approve any waiting fork workflows so CI can run. Thank you!

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI has now finished at this head. The Studio and player captures check passes on the run that carries the edited description (an older red result from before the edit is still listed beside it), and the other checks are green, including the ones that were cancelled earlier.

Everything else is as in my last review: the rebase is clean, the Before/After captures are in the description, GIF renders stay out of project history, and the new tests fail when the code is broken. Whether GIF export is something to add is for the maintainers to decide.

Verdict: APPROVE
Reasoning: All earlier points are fixed and the tests catch regressions; checks are green at this head apart from one superseded captures run.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 26, 2026
Merged via the queue into heygen-com:main with commit 2d695a8 Oct 26, 2026
117 of 164 checks passed
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Oct 26, 2026
Merge 8 upstream commits (85dcc19..7d6c02f: heygen-com#6418, heygen-com#5273, heygen-com#6245, heygen-com#5593,
heygen-com#5447, heygen-com#6415, heygen-com#6373, heygen-com#6375). Keep the fork's Windows-only ci.yml. For
cancellableProcess.ts take upstream's heygen-com#5273, which supersedes the fork's own
744a620 port (same StringDecoder design, but the output limit counts raw
bytes). Record the batch in docs/DECISIONS.md, mark heygen-com#6373/heygen-com#6375/heygen-com#6415/heygen-com#6418
as merged, and advance the commit watermark to 7d6c02f.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio render route: support GIF output (with loop count)

3 participants