feat(studio): speed presets, voice, look, crop presets and freeze frame in the clip menu (3/8) - #4815
Conversation
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
720871d to
a6c12ff
Compare
Edit accuracy: 951 passing here, 951 on the base branchThe gate passes. Quarantined, measured but not gated (2)
|
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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.
e4931f9 to
a6c12ff
Compare
e4931f9 to
c16614b
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
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
c16614b to
bf04ab9
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
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
bf04ab9 to
c9e62b8
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
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
c9e62b8 to
bff5d19
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
bff5d19 to
a88f4fe
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
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
…sets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
a88f4fe to
9c6f0a3
Compare
jerrai-bot-heygen
left a comment
There was a problem hiding this comment.
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
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
speedRamp).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 centredinset()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./freeze-frameroute. It writes through@hyperframes/core/atomic-file.useFreezeFrame).Testing
Static gates on the layer tip:
bun install,bun run build,tsc --noEmitfor every touched package (0 errors),oxlintandoxfmt --checkon files changed vs main (clean),gen:skills-manifest --check(in sync),scripts/comment-ratchet.mjs(ok).Notes
🤖 Generated with Claude Code
Before
After
Clip menu: Freeze frame, Voice and Look added (menu runs past the bottom of the 900px viewport).