Skip to content

feat(studio): speed presets, voice, look, crop presets and freeze frame in the clip menu (3/8) - #4815

Merged
vanceingalls merged 10 commits into
mainfrom
aov/03-clip-tools
Oct 2, 2026
Merged

vanceingalls merged 10 commits into
mainfrom
aov/03-clip-tools

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Clip-menu tools for picture and speed: gentle speed-ramp presets, Voice and Look submenus, Crop with aspect presets on the canvas, clip badges, and Freeze frame.

Changes

  • core: ramp-in, ramp-out and slow-mo-middle speed presets (speedRamp).
  • studio: Voice ▸ and Look ▸ single-choice submenus write data-fx-chain / data-color-grading, and None clears them. Crop opens a canvas preset bar (Free, 16:9, 9:16, 1:1, 4:5, Reset, Done) that commits a centred inset() clip-path. It is built on upstream's stylesheet crop lift (fix(studio): resizing a cropped element keeps the same part of it in view #4743). Clip badges read the live preview node.
  • studio-server: freeze-frame file mutation plus a /freeze-frame route. It writes through @hyperframes/core/atomic-file.
  • studio: Freeze frame in the clip menu (useFreezeFrame).

Testing

Static gates on the layer tip: bun install, bun run build, tsc --noEmit for every touched package (0 errors), oxlint and oxfmt --check on files changed vs main (clean), gen:skills-manifest --check (in sync), scripts/comment-ratchet.mjs (ok).

  • core 3789, studio 6255, studio-server 958. All pass.

Notes

  • Known limits: freeze doesn't retime GSAP or ramps on the shifted clips, and undo leaves the freeze PNG on disk.
  • Part of the audio-on-video stack. See the bottom PR for the overview.

🤖 Generated with Claude Code

Before

before

After

after

Clip menu: Freeze frame, Voice and Look added (menu runs past the bottom of the 900px viewport).

@vanceingalls
vanceingalls added this pull request to stack #4822 October 1, 2026 01:24
Comment thread packages/studio-server/src/helpers/freezeFrame.ts Fixed

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At 720871da192bce80bc119e2109165f28079bc460 against #4814’s exact base, the freeze-frame route passes an HTML-controlled clip ID through to an unconstrained ffmpeg output path; an ID with ../ segments can escape the project and write outside its asset directory. The inline comment includes the source→path chain and a reproducing path.join example. I did not establish exploitability of the separate CodeQL stored-XSS alert at freezeFrame.ts:131, but that red check also needs disposition. The upstream #4814 has its own changes-requested review, so this child is not independently merge-ready. No local Studio UI or media run, and no merge/release action. — Jerrai

if (!mediaPath) return { error: `forbidden media path: ${source.src}`, status: 403 };
const freezeDir = join(projectDir, "assets", "freeze");
const imagePath = join(freezeDir, `${source.id}-${Math.round(playhead * 1000)}.png`);
mkdirWithinProject(projectDir, freezeDir);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

source.id comes straight from the project HTML's <video id> (readFreezeSource) and is interpolated into this ffmpeg -y output path. Unlike the input media path, imagePath is never pinned within projectDir; Node path.join('/project/assets/freeze', '../../../../outside/frame-2500.png') resolves to /outside/frame-2500.png. If the destination directory exists and the server can write it, a crafted clip ID writes/overwrites a PNG outside the project sandbox. Sanitize the ID as a filename component, then validate/pin the resolved output path before extract(), and test a traversal ID through the route. mkdirWithinProject only validates freezeDir, not this output.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Edit accuracy: 951 passing here, 951 on the base branch

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (2)

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-reviewed #4815 at exact a6c12ffb6101bfbdf6e3198ca37d824b566cdff4. The previous traversal write is repaired: the HTML-controlled clip ID is reduced to one filename component, the output is pinned within assets/freeze, and the route checks its directory before extraction. CodeQL and Comments checks now pass. A new filename-collision defect in that repair remains (inline): two valid clip IDs or shared 80-character prefixes at the same playhead can generate the same output; ffmpeg -y overwrites the earlier still. Please make output identity collision-resistant and test preservation across two freezes. The studio/player capture check is also red for missing Before/After evidence; no browser or real ffmpeg run was performed in this review. The upstream stack remains separately gated. No merge/release action. — Jerrai

}

export function freezeStillFileName(clipId: string, playhead: number): string {
const stem = clipId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80) || "clip";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The sanitization closes the ../ path escape, but it is not injective: a.b and a_b both become a_b, and at the same 2.5s playhead both produce a_b-2500.png. Two overlapping videos on different tracks can each be frozen there; the second extraction runs ffmpeg with -y on the first still's path, silently replacing its pixels while the first still keeps referencing that URL. The 80-character truncation also collides distinct IDs sharing a prefix. Please assign a collision-resistant, immutable still name per extraction (including concurrent/repeated requests), and test two valid colliding IDs frozen at one time with distinct output paths and intact first pixels.

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At exact c16614b676aa74fdb0f83ff2c4ccb99f3439efd3, my prior freeze-still filename collision hold is repaired: the output name includes a hash of the original clip ID and a random token, the route pins the path under assets/freeze, returns 409 for an existing filename, and FFmpeg uses -n rather than overwrite. Focused route/helper tests and current-head hosted checks ran green, including file-size/Comments; no real FFmpeg concurrency or live UI run was performed. I have not approved because the Studio and player captures check still ran and failed for missing Before/After evidence, with no explicit waiver. The old CHANGES_REQUESTED review remains GitHub's gate. Please provide captures or a per-PR waiver. This is not merge/release clearance. — Jerrai

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed #4815 at bf04ab97 against stacked parent #4814 cd13d54b. The freeze-frame path still uses a collision-resistant still name and non-overwriting output. Relative to the earlier reviewed tip, only two PR-scope blobs differ: ClipContextMenu’s main color token and TimelineClip’s main fade/waveform integration; no conflict loss found in those comparisons. Required Studio/player captures remain FAILED for missing Before/After evidence, so no approval; edit-accuracy and Comments checks pass. No local FFmpeg, Studio or render test. — Jerrai

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-reviewed the final #4814-based layer. The freeze output uses a bounded filename component, original-ID hash, fresh token and non-overwriting FFmpeg output; earlier traversal and collision holds were repaired in reviewed successors. The 31 scoped files survive the main rebase, and the sole concurrent App change composes with main's pending-edit tracking without dropping this layer's patch. The Studio/player capture check passes. This is code approval, not merge/release clearance. — Jerrai

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exact-head rebase audit: the old and new parent→head patches are identical across all 31 layer paths, and none of those parent blobs changed in the rebase. I am holding a fresh code approval because this head's Studio: timeline viewport gate is red: the explicitly disabled arm measured interaction p95 82.0 and 81.8 ms on two attempts (75 ms ceiling), while #4814's corresponding arm passed at 49.5 ms. The layer changes TimelineClip and related per-clip UI, so this is possible code-quality evidence, but a precise cause has not been established; the 1000-row fixture does not prove playback-observer overhead. Please investigate or provide a controlled rerun/ablation establishing whether this is in the diff. Capture succeeded; the remaining CI/merge gate is separate. — Jerrai

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The freeze-path traversal/collision repairs remain in the reviewed head. I withheld this head on the disabled-virtualization viewport gate after two 82.0/81.8 ms readings exceeded 75 ms. The same head then passed at 49.1 ms (5/5), and the paired hosted rerun passed at 57.9 ms (5/5) beside #4814 at 51.5 ms (5/5), both with 1,000 mounted clip roots. This supports an approval on the present code and gate, not a claim that the first failures were proved to be runner noise or that ClipBadges has zero cost. Please keep an eye on the disabled-arm p95 after landing. No merge/release authorization is implied. — Jerrai

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The complete 31-file layer patch is unchanged from the previously reviewed and approved head; merged main touches none of those paths. The earlier freeze-frame safety objections are repaired in this source: the still name includes a token/hash and the output is pinned inside the project. The prior paired hosted viewport measurements remain the evidence for this layer, not proof that the first failed runs were noise. Current CI is still running. Approval is code-only, not a merge or release authorization. — Jerrai

Base automatically changed from aov/02-audible-video-audio-engine to main October 2, 2026 02:43
…sets

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vanceingalls and others added 9 commits October 2, 2026 02:43
Voice and Look are single-choice submenus writing data-fx-chain and
data-color-grading; None clears. Crop opens the canvas crop preset bar.
ClipBadges reads the live preview node and follows attribute edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Free, 16:9, 9:16, 1:1, 4:5, Reset and Done float above the clip opened from
the clip menu. Presets commit a centred inset clip-path; Reset removes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extracts the frame under the playhead with ffmpeg (media time honours
data-media-start, playback rate and a rate lane) into assets/freeze/, then in
one write splits the video, inserts a 2 s <img> still with the video's box
styles, shifts the rest of the track and splits a data-link partner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Freeze frame on a video under the playhead posts the studio-server
freeze-frame mutation and records its before/after as one undo step.
onFreezeFrame is threaded through the timeline edit callbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clip id from the project HTML went straight into ffmpeg's -y output
path, so an id with ../ segments wrote outside the project. The id is now
reduced to one safe filename component, the output is pinned inside
assets/freeze before extraction, and the still's src is built from that
name rather than from a filesystem path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two clip ids that sanitise alike (a.b / a_b), ids sharing an 80-character
prefix, or a second freeze of one clip at the same playhead all produced the
same still path, and ffmpeg's -y replaced the first still's pixels. The name
now carries a hash of the raw id and a per-extraction token, the route
refuses a name that already exists, and ffmpeg runs with -n so it can never
overwrite. The freeze media time reads data-playback-start before
data-media-start, as playback does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 2, 2026, 2:52 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-approved this rebased head. I compared the complete 31-path parent-to-head layer with the previously approved layer: same paths and patch hunks, including the immutable freeze-frame filename/path safeguards and tests. The prior paired hosted viewport evidence still informs the code review, but the new-head CI is independently running; this approval is not a merge or release decision. — Jerrai

@vanceingalls
vanceingalls added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 000ccd4 Oct 2, 2026
85 checks passed
@vanceingalls
vanceingalls deleted the aov/03-clip-tools branch October 2, 2026 03:22

This branch was successfully deployed

1 active deployment
staging - docs — 9c6f0a3e Deployed Oct 2, 2026 by mintlify[bot]
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.

3 participants