Skip to content

Add selective content masking to the timelapse editor (Updated with fixes) - #198

Open
httperry wants to merge 10 commits into
hackclub:mainfrom
httperry:feature/mask-tool-editor
Open

httperry wants to merge 10 commits into
hackclub:mainfrom
httperry:feature/mask-tool-editor

Conversation

@httperry

Copy link
Copy Markdown

Addresses hackclub/lapse#215.

Currently when recording timelapses, if sensitive info (API keys, .env files, 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

  • Interactive canvas overlay (@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.
  • Dedicated Mask tab: Keeps cut editing and mask editing separate so controls and timeline interactions don't conflict with each other.
  • Multi-track timeline lanes: Uses interval scheduling to automatically stack concurrent masks into separate lanes (supporting up to 3 parallel tracks), preventing overlapping masks from colliding.
  • Scene-aware handle snapping: Snaps mask start and end handles to shot transitions using a fast client-side frame difference check on a downscaled 48×48 canvas, eliminating extra server roundtrips.
  • Timeline minimap & zoom overview: Adds a translucent overview bar underneath the timeline with smooth scrubbing and dynamic duration formatting.
  • Video pixelation in compilation (@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.
  • Session mask persistence (@lookout/server): Adds a masks column to the sessions table (migration 0032_session_masks.sql), returning saved masks in GET /units and validating them on PUT /cuts.
  • Shared data model & bounds validation (@lookout/shared): Centralizes the MaskRegion schema with bounds checking to ensure coordinates stay normalized (0..1) across the frontend, server, and worker.

Verification

  • Monorepo builds clean (tsc --noEmit).
  • All test suites pass:
    • 162 React tests (canvas interactions, track allocation, snapping math)
    • 32 Worker tests (pixelation filter execution, cutVideo bit-exactness, segment stitching)
    • 30 Shared tests (coordinate bounds checking and interval intersection math)
  • Tested end-to-end locally: drawn masks save to Postgres, reload cleanly on refresh, and the worker compiles an MP4 with pixelated boxes in the specified coordinates.

Copilot AI lite review requested due to automatic review settings September 16, 2026 16:27
@httperry

Copy link
Copy Markdown
Author

Copilot AI 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.

🟡 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 TimelapseEditor is still loading. In that window masksRef.current is still its empty initial value, so this always sends masks: [] 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 maskToVideoMask and do not invoke FFmpeg. Split those mapping tests into an ungated describe so 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 testsrc and writes only its first frame, so every unit_i.jpg has 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("//")) {
Comment on lines +295 to +297
return masks.map((m) => {
let startUnit = 0;
let endUnit = units.length;
Comment on lines +92 to +96
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);
Comment on lines +675 to +677
`[${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}]`,
@Lamparter

Copy link
Copy Markdown
Member

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

This branch has not been deployed

No deployments
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