Use a Take frame as a Keyframe (#60) #115

Merged
crowmaki merged 2 commits from issue-60-take-frame-keyframe into main 2026-10-09 07:57:37 +00:00
Owner

Closes #60. Part of #24 (User Stories 91 and 92).

What changes

A filmmaker can continue across a cut by turning a Take's first or last frame into a Keyframe of any Shot.

  • Dailies. The Take details panel has a new row: Use as Keyframe: First frame… / Last frame…. Each opens Use first / last frame as Keyframe for…. This dialog:

    • shows the frame as it will be taken
    • lets you pick the target Shot (any Shot in the Project, labelled with its Scene) and the anchor (Start, End or A time)
    • warns when the frame will replace a Keyframe already at that anchor

    The defaults follow the cut. A last frame goes to the start of the next Shot, and a first frame goes to the end of the previous Shot. At either end of the Project the default is the Take's own Shot.

  • Core. keyframe_from_take(rendition, frame, shot, anchor) reads the frame from the Rendition's video in the media folder with the ffmpeg sidecar:

    • first frame: the existing poster extraction
    • last frame: a new Ffmpeg::last_frame, which decodes the final second backwards

    The frame keeps its pixels and goes in as one undoable edit ("Use take frame as keyframe"), replacing the Keyframe at that anchor like import does. It refuses a Rendition that is not rendered or whose files are purged, an unknown Shot, and a time that is not inside the Shot.

  • Provenance. A new Keyframe origin, take_frame, records the source Take, the Rendition, which frame (first or last) and the frame time in ms. The last frame's time is (frames − 1) / 24 s of the Take.

  • frame_from link. When a Take that sends such a Keyframe is queued, it gets take_link (take, source_take, 'frame_from') in the same transaction. "Rebuild with current inputs" writes the link again from the new inputs. TakeDto.frameFromTakeIds exposes it.

  • Board. The Keyframe strip labels a take-frame Keyframe with its source, for example 1A-2 last. Hovering shows "Last frame of 1A-2 (5.833 s)".

  • Schema. V31 rebuilds keyframe to allow origin take_frame and adds source_take_id, source_rendition_id, source_frame and source_frame_ms. A CHECK makes all four required for a take frame and absent otherwise.

Decision to confirm

take_link goes from one Take to another, and a Keyframe is not a Take. So the frame_from link is written when a Take that uses the frame Keyframe is queued, not when the Keyframe is made. The Keyframe holds its own provenance from the moment it is made. Queuing is append-only, so the link stays out of the undoable Keyframe edit.

Screenshots

Take details with the new Use as Keyframe row.

Details

Use last frame as Keyframe for… on 1A-2. The defaults are the next Shot (1B) and its start.

Dialog

Opened again after use, the dialog warns that it would replace 1B's start Keyframe. The status bar shows the undo entry.

Replaces

1B's Board. The start Keyframe is the last frame of 1A-2, and its badge names the source.

Board

The clips come from a stand-in ComfyUI that serves ffmpeg test clips.

Tests

  • crates/studio-core/tests/take_frame_keyframes.rs (new, against a fake ComfyUI and a brightening test clip):
    • the last frame lands with its source Take, name, Rendition, frame and time, and its pixels are the clip's bright last frame
    • the first frame is at 0 ms, its pixels are dark, and it can go to the Take's own Shot
    • taking a frame replaces the Keyframe at the anchor, and undo restores the old one and then nothing
    • a Take that sends the Keyframe is linked frame_from to the source Take, and rebuilding after the Keyframe is removed drops the link
    • refusals: an unrendered or unknown Rendition, a time outside the Shot, an unknown Shot
  • crates/studio-core/tests/migrations.rs: Keyframes from before V31 survive the rebuild (anchors, prompts, hidden rows), the new CHECK holds, and the partial anchor index is back
  • src/lib/dailies/frameKeyframe.test.ts: the Shot choices, the default target and anchor, and the labels

cargo test --workspace --all-features, bun test src and bun run check all pass, except ffmpeg::tests::a_proxy_*. Those two tests fail on this machine before and after this change, because the local ffmpeg has no libopenh264.

Closes #60. Part of #24 (User Stories 91 and 92). ## What changes A filmmaker can continue across a cut by turning a Take's first or last frame into a Keyframe of any Shot. - **Dailies.** The Take details panel has a new row: **Use as Keyframe: First frame… / Last frame…**. Each opens **Use first / last frame as Keyframe for…**. This dialog: - shows the frame as it will be taken - lets you pick the target **Shot** (any Shot in the Project, labelled with its Scene) and the anchor (**Start**, **End** or **A time**) - warns when the frame will replace a Keyframe already at that anchor The defaults follow the cut. A last frame goes to the start of the next Shot, and a first frame goes to the end of the previous Shot. At either end of the Project the default is the Take's own Shot. - **Core.** `keyframe_from_take(rendition, frame, shot, anchor)` reads the frame from the Rendition's video in the media folder with the ffmpeg sidecar: - first frame: the existing poster extraction - last frame: a new `Ffmpeg::last_frame`, which decodes the final second backwards The frame keeps its pixels and goes in as one undoable edit ("Use take frame as keyframe"), replacing the Keyframe at that anchor like import does. It refuses a Rendition that is not rendered or whose files are purged, an unknown Shot, and a time that is not inside the Shot. - **Provenance.** A new Keyframe origin, `take_frame`, records the source Take, the Rendition, which frame (first or last) and the frame time in ms. The last frame's time is `(frames − 1) / 24 s` of the Take. - **`frame_from` link.** When a Take that sends such a Keyframe is queued, it gets `take_link (take, source_take, 'frame_from')` in the same transaction. "Rebuild with current inputs" writes the link again from the new inputs. `TakeDto.frameFromTakeIds` exposes it. - **Board.** The Keyframe strip labels a take-frame Keyframe with its source, for example `1A-2 last`. Hovering shows "Last frame of 1A-2 (5.833 s)". - **Schema.** V31 rebuilds `keyframe` to allow origin `take_frame` and adds `source_take_id`, `source_rendition_id`, `source_frame` and `source_frame_ms`. A CHECK makes all four required for a take frame and absent otherwise. ### Decision to confirm `take_link` goes from one Take to another, and a Keyframe is not a Take. So the `frame_from` link is written when a Take that **uses** the frame Keyframe is queued, not when the Keyframe is made. The Keyframe holds its own provenance from the moment it is made. Queuing is append-only, so the link stays out of the undoable Keyframe edit. ## Screenshots Take details with the new **Use as Keyframe** row. ![Details](https://git.crowmaki.me/attachments/700ce141-a98a-4805-878d-5b01b4fac0b5) **Use last frame as Keyframe for…** on 1A-2. The defaults are the next Shot (1B) and its start. ![Dialog](https://git.crowmaki.me/attachments/97cb9390-b580-49c1-beba-2abb60cb48f3) Opened again after use, the dialog warns that it would replace 1B's start Keyframe. The status bar shows the undo entry. ![Replaces](https://git.crowmaki.me/attachments/80725653-6b68-41ab-ac23-cd44eb681d01) 1B's Board. The start Keyframe is the last frame of 1A-2, and its badge names the source. ![Board](https://git.crowmaki.me/attachments/e1dc4f21-1e98-401a-a14a-8b3ffca49345) The clips come from a stand-in ComfyUI that serves ffmpeg test clips. ## Tests - `crates/studio-core/tests/take_frame_keyframes.rs` (new, against a fake ComfyUI and a brightening test clip): - the last frame lands with its source Take, name, Rendition, frame and time, and its pixels are the clip's bright last frame - the first frame is at 0 ms, its pixels are dark, and it can go to the Take's own Shot - taking a frame replaces the Keyframe at the anchor, and undo restores the old one and then nothing - a Take that sends the Keyframe is linked `frame_from` to the source Take, and rebuilding after the Keyframe is removed drops the link - refusals: an unrendered or unknown Rendition, a time outside the Shot, an unknown Shot - `crates/studio-core/tests/migrations.rs`: Keyframes from before V31 survive the rebuild (anchors, prompts, hidden rows), the new CHECK holds, and the partial anchor index is back - `src/lib/dailies/frameKeyframe.test.ts`: the Shot choices, the default target and anchor, and the labels `cargo test --workspace --all-features`, `bun test src` and `bun run check` all pass, except `ffmpeg::tests::a_proxy_*`. Those two tests fail on this machine before and after this change, because the local ffmpeg has no `libopenh264`.
"Use first / last frame as Keyframe for..." in Dailies puts the first or
last frame of a Take's playing Rendition at an anchor of any Shot. The
Keyframe (origin take_frame) records its source Take, Rendition, frame and
frame time. A Take that sends it is linked frame_from to the source Take.
crowmaki deleted branch issue-60-take-frame-keyframe 2026-10-09 07:57:37 +00:00
Sign in to join this conversation.
No description provided.