2e9fa45580
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>
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { computeEditRange, frameRegion, isHandleFrame } from "./handles";
|
|
|
|
describe("computeEditRange", () => {
|
|
it("is null when no handles are marked", () => {
|
|
expect(computeEditRange(null, 100)).toBeNull();
|
|
expect(computeEditRange({}, 100)).toBeNull();
|
|
expect(computeEditRange({ handleHead: 0, handleTail: 0 }, 100)).toBeNull();
|
|
});
|
|
|
|
it("is null until the media length is known", () => {
|
|
expect(computeEditRange({ handleHead: 8, handleTail: 8 }, 0)).toBeNull();
|
|
});
|
|
|
|
it("marks 8+8 on media with no slate", () => {
|
|
expect(computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 100 }, 100)).toEqual({
|
|
pictureStart: 0,
|
|
cutIn: 8,
|
|
tailStart: 92,
|
|
pictureEnd: 100,
|
|
});
|
|
});
|
|
|
|
it("infers a 1-frame head slate from the media being one frame longer than the EXRs", () => {
|
|
// Pipeline preview: 100 EXR frames + 1 slate frame = 101 in the MP4
|
|
expect(computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 100 }, 101)).toEqual({
|
|
pictureStart: 1,
|
|
cutIn: 9,
|
|
tailStart: 93,
|
|
pictureEnd: 101,
|
|
});
|
|
});
|
|
|
|
it("uses a recorded picture start over inference", () => {
|
|
const r = computeEditRange(
|
|
{ handleHead: 8, handleTail: 8, frameCount: 100, pictureStartFrame: 2 },
|
|
102
|
|
);
|
|
expect(r).toEqual({ pictureStart: 2, cutIn: 10, tailStart: 94, pictureEnd: 102 });
|
|
});
|
|
|
|
it("anchors the tail on the picture when the container rounds a frame short", () => {
|
|
// Recorded 1-frame slate, 100 EXRs, but the MP4 duration floors to 100
|
|
const r = computeEditRange(
|
|
{ handleHead: 8, handleTail: 8, frameCount: 100, pictureStartFrame: 1 },
|
|
100
|
|
);
|
|
expect(r).toEqual({ pictureStart: 1, cutIn: 9, tailStart: 92, pictureEnd: 100 });
|
|
});
|
|
|
|
it("doesn't mistake a large unexplained length difference for a slate", () => {
|
|
const r = computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 50 }, 100);
|
|
expect(r?.pictureStart).toBe(0);
|
|
});
|
|
|
|
it("supports unequal and one-sided handles", () => {
|
|
expect(computeEditRange({ handleHead: 12, handleTail: 0, frameCount: 100 }, 100)).toEqual({
|
|
pictureStart: 0,
|
|
cutIn: 12,
|
|
tailStart: 100,
|
|
pictureEnd: 100,
|
|
});
|
|
});
|
|
|
|
it("never lets the handles overlap on a very short shot", () => {
|
|
const r = computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 10 }, 10)!;
|
|
expect(r.cutIn).toBe(8);
|
|
expect(r.tailStart).toBe(8);
|
|
});
|
|
});
|
|
|
|
describe("frameRegion", () => {
|
|
const range = computeEditRange({ handleHead: 8, handleTail: 8, frameCount: 100 }, 101);
|
|
|
|
it("classifies slate, handles and edit", () => {
|
|
expect(frameRegion(range, 0)).toBe("slate");
|
|
expect(frameRegion(range, 1)).toBe("head");
|
|
expect(frameRegion(range, 8)).toBe("head");
|
|
expect(frameRegion(range, 9)).toBe("edit");
|
|
expect(frameRegion(range, 92)).toBe("edit");
|
|
expect(frameRegion(range, 93)).toBe("tail");
|
|
expect(frameRegion(range, 100)).toBe("tail");
|
|
});
|
|
|
|
it("treats everything as edit when handles are unknown", () => {
|
|
expect(frameRegion(null, 0)).toBe("edit");
|
|
expect(isHandleFrame(null, 0)).toBe(false);
|
|
});
|
|
|
|
it("does not count the slate as a handle", () => {
|
|
expect(isHandleFrame(range, 0)).toBe(false);
|
|
expect(isHandleFrame(range, 1)).toBe(true);
|
|
});
|
|
});
|