feat(review): flag handles in the review player (metadata only)
Deploy / deploy (push) Successful in 3m31s

Renders run 8 frames past the cut at each end. Directors were flagging
glitches in unfixed handles as shot problems, so the player now marks
them — without trimming or burning anything into the EXRs/MOV/MP4.

- Version.handleHead/handleTail/pictureStartFrame, Export.handleHead/
  handleTail; migration backfills pipeline exports and their preview
  versions from the manifest's handleFrames (else 8)
- AE panel: "Includes handles [8] frames each end" -> manifest.review;
  older manifests fall back to source.handleFrames
- finalize copies handles onto the Version; worker 1.6.4 reports the
  head slate length (pictureStartSeconds), else the player infers it
  as MP4 frames - EXR frameCount
- Player: hatched HANDLE / SLATE regions and cut marks on the timeline,
  "HEAD/TAIL HANDLE · NOT IN EDIT" overlay on handle frames, HANDLE badge
  on notes, "Edit range only" toggle (default on in the client portal)
- Staff Handles editor on the review page for manual uploads/corrections

Also fixes two player bugs found while testing: timeToFrame floored
float error (every seek landed a frame early), and J/reverse never ran
(the keyboard effect's cleanup stopped it on the next render).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-09-22 15:07:29 +02:00
parent 2817c7e815
commit 2e9fa45580
27 changed files with 938 additions and 23 deletions
+12
View File
@@ -1,6 +1,7 @@
import { create } from "zustand";
import type { AnnotationTool } from "@/types";
import { ASPECT_OFF, type BlankingMode } from "@/lib/aspect-overlays";
import type { EditRange } from "@/lib/handles";
interface ReviewPlayerStore {
// Playback
@@ -28,6 +29,11 @@ interface ReviewPlayerStore {
fps: number;
totalFrames: number;
// Handles — where the cut sits in the media (null: no handles marked), and
// whether playback keeps to it. Viewing aids only; the media is untouched.
editRange: EditRange | null;
editRangeOnly: boolean;
// Actions
setPlaying: (playing: boolean) => void;
setCurrentFrame: (frame: number) => void;
@@ -46,6 +52,8 @@ interface ReviewPlayerStore {
setBlankingMode: (mode: BlankingMode) => void;
setFps: (fps: number) => void;
setTotalFrames: (frames: number) => void;
setEditRange: (range: EditRange | null) => void;
setEditRangeOnly: (on: boolean) => void;
reset: () => void;
}
@@ -67,6 +75,8 @@ const initialState = {
blankingMode: "solid" as BlankingMode,
fps: 24,
totalFrames: 0,
editRange: null as EditRange | null,
editRangeOnly: false,
};
export const useReviewStore = create<ReviewPlayerStore>((set) => ({
@@ -88,5 +98,7 @@ export const useReviewStore = create<ReviewPlayerStore>((set) => ({
setBlankingMode: (blankingMode) => set({ blankingMode }),
setFps: (fps) => set({ fps }),
setTotalFrames: (totalFrames) => set({ totalFrames }),
setEditRange: (editRange) => set({ editRange }),
setEditRangeOnly: (editRangeOnly) => set({ editRangeOnly }),
reset: () => set(initialState),
}));