Conversation
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds selective screen-region masking to timelapse editing, persisting normalized mask regions and applying FFmpeg pixelation during compilation.
Changes:
- Adds shared mask models, validation, persistence, and migration.
- Adds React/desktop editor masking UI, timeline tracks, snapping, and API forwarding.
- Adds worker-side mask conversion, FFmpeg processing, and tests.
File summaries
| File | Description |
|---|---|
| packages/worker/test/maskVideo.test.ts | Updated as part of this pull request. |
| packages/worker/src/segments.ts | Updated as part of this pull request. |
| packages/worker/src/schema.ts | Updated as part of this pull request. |
| packages/worker/src/compile.ts | Updated as part of this pull request. |
| packages/shared/src/types.ts | Updated as part of this pull request. |
| packages/shared/src/masks.ts | Updated as part of this pull request. |
| packages/shared/src/masks.test.ts | Updated as part of this pull request. |
| packages/shared/src/index.ts | Updated as part of this pull request. |
| packages/shared/src/cuts.ts | Updated as part of this pull request. |
| packages/server/test/edits.integration.test.ts | Updated as part of this pull request. |
| packages/server/src/routes/sessions.ts | Updated as part of this pull request. |
| packages/server/src/lib/timeouts.ts | Updated as part of this pull request. |
| packages/server/src/db/schema.ts | Updated as part of this pull request. |
| packages/server/drizzle/meta/0032_snapshot.json | Updated as part of this pull request. |
| packages/server/drizzle/meta/_journal.json | Updated as part of this pull request. |
| packages/server/drizzle/0032_session_masks.sql | Updated as part of this pull request. |
| clients/react/tsup.config.ts | Updated as part of this pull request. |
| clients/react/src/index.ts | Updated as part of this pull request. |
| clients/react/src/hooks/webCodecsClipEngine.ts | Updated as part of this pull request. |
| clients/react/src/hooks/editorMath.ts | Updated as part of this pull request. |
| clients/react/src/hooks/editorMath.test.ts | Updated as part of this pull request. |
| clients/react/src/components/ResultView.tsx | Updated as part of this pull request. |
| clients/react/src/components/ResultView.test.tsx | Updated as part of this pull request. |
| clients/react/src/components/editorStyles.ts | Updated as part of this pull request. |
| clients/react/src/api/client.ts | Updated as part of this pull request. |
| clients/react/src/api/client.test.ts | Updated as part of this pull request. |
| clients/playground/src/App.tsx | Updated as part of this pull request. |
| clients/desktop/vitest.config.ts | Updated as part of this pull request. |
| clients/desktop/src/testSetup.ts | Updated as part of this pull request. |
| clients/desktop/src/components/EditorWindow.tsx | Updated as part of this pull request. |
| clients/desktop/src/components/EditorWindow.test.ts | Updated as part of this pull request. |
| clients/desktop/src/api/tauriClient.ts | Updated as part of this pull request. |
| clients/desktop/src-tauri/src/lib.rs | Updated as part of this pull request. |
| clients/desktop/src-tauri/crates/lookout-core/src/api.rs | Updated as part of this pull request. |
Review details
Suppressed comments (4)
clients/desktop/src/components/EditorWindow.tsx:339
- The close handler can run while
TimelapseEditoris still loading. In that windowmasksRef.currentis still its empty initial value, so this always sendsmasks: []through the desktop client and clears any masks already saved for the session when the user closes immediately. Gate publishing on editor readiness or initialize the ref from the loaded session before allowing close.
await client.setCuts(cutsRef.current, masksRef.current);
packages/shared/src/masks.ts:99
- The normalized ID is retained for every non-empty
id, but IDs are used as identity keys throughout the editor (React keys, selection, and the track-assignment map). A request containing two masks with the same ID is accepted and, after reload, those masks cannot be edited independently and one assignment overwrites the other. Reject duplicate IDs or generate a unique replacement during normalization.
id: typeof id === "string" && id.trim().length > 0 ? id : `mask-${i}-${startMs}`,
packages/worker/test/maskVideo.test.ts:122
- The pure timestamp-mapping tests in this file are nested under the FFmpeg-gated suite, so a machine without FFmpeg silently skips them even though they only exercise
maskToVideoMaskand do not invoke FFmpeg. Split those mapping tests into an ungateddescribeso out-of-range and wall-clock conversion behavior remains covered when only unit tests can run.
packages/worker/test/maskVideo.test.ts:140 - Each loop starts a fresh
testsrcand writes only its first frame, so everyunit_i.jpghas the same pixels despite the comment. The assertions therefore cannot detect a mask applied to the wrong unit or active outside[startSec, endSec); an implementation that masks every frame could pass. Generate distinct per-unit frames and assert pre/post-interval frames remain unmodified.
- Files reviewed: 34/35 changed files
- Comments generated: 6
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return call<SetCutsResponse>("api_session_set_cuts", { | ||
| ...(await session()), | ||
| cuts, | ||
| masks: masks ?? [], |
| throw new Error("Invalid upload URL: must be HTTPS or a relative path."); | ||
| } | ||
| let isAllowed = false; | ||
| if (uploadUrl.startsWith("/") && !uploadUrl.startsWith("//")) { |
| return masks.map((m) => { | ||
| let startUnit = 0; | ||
| let endUnit = units.length; |
| typeof startSec === "number" && | ||
| Number.isFinite(startSec) && | ||
| typeof endSec === "number" && | ||
| Number.isFinite(endSec) && | ||
| endSec > startSec; |
| const videoMasks = masksToVideoMasks(masks, videoUnits); | ||
| const hasEffectiveMasks = videoMasks.length > 0; | ||
| const needsEdit = hasEffectiveCuts || hasEffectiveMasks; | ||
| const hasEdits = hasEffectiveCuts || (masks && masks.length > 0); |
| `[${currentIn}]split=2[m_base_${i}][m_crop_${i}]`, | ||
| `[m_crop_${i}]crop=${W}:${H}:${X}:${Y},scale=${downW}:${downH}:flags=neighbor,scale=${W}:${H}:flags=neighbor[m_pix_${i}]`, | ||
| `[m_base_${i}][m_pix_${i}]overlay=${X}:${Y}:enable='gte(t\\,${m.startSec - 0.005})*lte(t\\,${m.endSec + 0.005})'[${outTag}]`, |
|
hey, this is a cool idea and from the video it looks like it was implemented very well. that said, this PR was not created in relation to a ready-to-build issue, and is therefore on the backlog for review for now |
Addresses hackclub/lapse#215.
Currently when recording timelapses, if sensitive info (API keys,
.envfiles, private messages) appears on screen, the only option is to cut out that entire minute of footage. This PR adds selective masking directly to the editor so users can redact specific screen regions while preserving the rest of their recorded footage.Summary
Adds an in-editor masking tool to redact sensitive on-screen areas, persists mask regions to the session, and applies a pixelation filter during video compilation.
Screen.Recording.2026-09-16.at.12.20.14.PM.mov
What's Added
@lookout/react): Click-and-drag bounding box overlay on top of the video player to draw, reposition, and resize mask regions with 8-point handles.@lookout/worker): Compiles masked videos through an ffmpeg filtergraph that burns pixelated boxes over the target coordinates and timestamps. Sessions without masks still take the instant lossless stream-copy path.@lookout/server): Adds amaskscolumn to thesessionstable (migration0032_session_masks.sql), returning saved masks inGET /unitsand validating them onPUT /cuts.@lookout/shared): Centralizes theMaskRegionschema with bounds checking to ensure coordinates stay normalized(0..1)across the frontend, server, and worker.Verification
tsc --noEmit).