From 67d8639861fbbd2c2b094ac2e96709afc410c021 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:22:49 +0200 Subject: [PATCH] fix(pipeline): trim picture-lock handles at the footage comp, not the template's SHOT layer Manipulating startTime/outPoint on previewComp's SHOT layer touched a comp duplicated from the artist-authored template, whose slate/shot timing relationship the build script doesn't own -- and it dropped the slate frame in production. The trim now happens one level down, on the private single-layer footage precomp this script fully controls; previewComp's own sizing logic is untouched from before this feature existed, since it already generalizes correctly to any shotComp duration. Also threads the picture-lock skip reason through to the export's timeline, so a shot missing sequence timecodes or a matching picture lock file is visible without digging into worker-machine logs. Co-Authored-By: Claude Sonnet 5 --- RenderWorker/VFXReviewWorker/JobExecutor.cs | 1 + RenderWorker/scripts/vfxr_build_preview.jsx | 77 ++++++++++++--------- lib/render-pipeline/preview.ts | 9 ++- 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/RenderWorker/VFXReviewWorker/JobExecutor.cs b/RenderWorker/VFXReviewWorker/JobExecutor.cs index 42f3c7e..01f270f 100644 --- a/RenderWorker/VFXReviewWorker/JobExecutor.cs +++ b/RenderWorker/VFXReviewWorker/JobExecutor.cs @@ -461,6 +461,7 @@ public sealed class JobExecutor ["thumbnailKey"] = thumbKey, ["piclockRefKey"] = piclockRefKey, }, + ["piclockSkippedReason"] = result.Build?.PiclockSkippedReason, ["renderStats"] = new Dictionary { ["previewSeconds"] = (int)result.Seconds, diff --git a/RenderWorker/scripts/vfxr_build_preview.jsx b/RenderWorker/scripts/vfxr_build_preview.jsx index f21109c..a6a32ff 100644 --- a/RenderWorker/scripts/vfxr_build_preview.jsx +++ b/RenderWorker/scripts/vfxr_build_preview.jsx @@ -484,22 +484,56 @@ result.height = height; result.frameCount = Math.round(duration * fps); + // Trim decision (§9, picture lock): the render's frame range is + // editorial ± a fixed symmetric handle count by studio convention (the + // same constant pullPictureLock hardcodes), so no picture-lock file is + // needed for this — only ctx.handleFrames. Applied at the footage/shot + // comp level (below) rather than by juggling layer timing inside + // previewComp — previewComp is duplicated from an artist-authored + // template whose slate/shot timing relationship this script does not + // own, so it is left completely untouched, exactly as it always + // rendered correctly before this feature existed. + var handleSeconds = (ctx.handleFrames || 0) / fps; + var contentDuration = duration; + var headSkipSeconds = 0; + var trimmedToPictureLock = false; + if (ctx.noPictureLock) { + result.piclockSkippedReason = "No Picture Lock was set for this export"; + } else if (handleSeconds <= 0) { + result.piclockSkippedReason = "No handle frame count configured"; + } else if (2 * handleSeconds >= duration) { + warn("Picture lock: handle count (" + ctx.handleFrames + "fr) covers the whole render (" + + result.frameCount + "fr) — trim skipped"); + result.piclockSkippedReason = "handle count exceeds the render duration"; + } else { + headSkipSeconds = handleSeconds; + contentDuration = duration - (2 * handleSeconds); + trimmedToPictureLock = true; + } + var contentStartFrame = (ctx.frameStart !== null && ctx.frameStart !== undefined) + ? ctx.frameStart + Math.round(headSkipSeconds * fps) + : null; + // 3. Footage precomp (mirrors buildShotFromCode) var precompFolder = findFolder("_PRECOMPS"); var footageComp = app.project.items.addComp( - ctx.shotCode + "_FOOTAGE", width, height, 1, duration, fps); + ctx.shotCode + "_FOOTAGE", width, height, 1, contentDuration, fps); if (precompFolder) { footageComp.parentFolder = precompFolder; } - if (ctx.frameStart !== null && ctx.frameStart !== undefined) { - setCompStartFrame(footageComp, ctx.frameStart); + if (contentStartFrame !== null) { + setCompStartFrame(footageComp, contentStartFrame); } - addOCIOToLayer(footageComp.layers.add(footage)); + var footageLayer = footageComp.layers.add(footage); + if (headSkipSeconds > 0) { + footageLayer.startTime -= headSkipSeconds; + } + addOCIOToLayer(footageLayer); // 4. Shot comp: footage + show LUT + overlay - var shotComp = app.project.items.addComp(ctx.shotCode, width, height, 1, duration, fps); - if (ctx.frameStart !== null && ctx.frameStart !== undefined) { - setCompStartFrame(shotComp, ctx.frameStart); + var shotComp = app.project.items.addComp(ctx.shotCode, width, height, 1, contentDuration, fps); + if (contentStartFrame !== null) { + setCompStartFrame(shotComp, contentStartFrame); } shotComp.layers.add(footageComp); @@ -573,31 +607,12 @@ warn("NETFLIX_SLATE layer not found in template — slate fields not filled"); } - // Trim the client-facing comp back to the picture-lock edit (§9, - // picture lock): the render's frame range is editorial ± a fixed - // symmetric handle count by studio convention (the same constant - // pullPictureLock hardcodes), so no picture-lock file is needed for - // this — only ctx.handleFrames. Both movOutput and mp4Output queue - // against this same previewComp, so trimming it once fixes both. + // previewComp sizes itself to whatever shotComp.duration actually is — + // unchanged from before this feature existed. shotComp is already the + // trimmed (or full, if skipped above) duration, so this needs no + // special-casing here. var shotLayer = previewComp.layer("SHOT"); - var handleSeconds = (ctx.handleFrames || 0) / fps; - var visibleDuration = shotComp.duration; - var trimmedToPictureLock = false; - if (ctx.noPictureLock) { - result.piclockSkippedReason = "No Picture Lock was set for this export"; - } else if (handleSeconds <= 0) { - result.piclockSkippedReason = "No handle frame count configured"; - } else if (2 * handleSeconds >= shotComp.duration) { - warn("Picture lock: handle count (" + ctx.handleFrames + "fr) covers the whole render (" + - result.frameCount + "fr) — trim skipped"); - result.piclockSkippedReason = "handle count exceeds the render duration"; - } else { - shotLayer.startTime -= handleSeconds; - visibleDuration = shotComp.duration - (2 * handleSeconds); - trimmedToPictureLock = true; - } - - var targetOutPoint = shotLayer.inPoint + visibleDuration; + var targetOutPoint = shotLayer.inPoint + shotComp.duration; if (targetOutPoint > previewComp.duration) { previewComp.duration = targetOutPoint; } diff --git a/lib/render-pipeline/preview.ts b/lib/render-pipeline/preview.ts index 6c83457..422b92f 100644 --- a/lib/render-pipeline/preview.ts +++ b/lib/render-pipeline/preview.ts @@ -303,6 +303,8 @@ export interface FinalizeInput { }; renderStats?: { renderSeconds?: number; previewSeconds?: number }; media?: { width?: number; height?: number; frameCount?: number; fps?: number; fileName?: string }; + /** Why the picture-lock trim/reference was skipped, when it was — surfaced on the export's timeline. */ + piclockSkippedReason?: string | null; } /** @@ -471,7 +473,12 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi ? " — review version media replaced" : ` — review version created (${inheritedApproval ?? "PENDING_REVIEW"})` : " — no preview media reported") + - (taskRenamed ? ` — task renamed to ${taskTitle}` : ""), + (taskRenamed ? ` — task renamed to ${taskTitle}` : "") + + (artifacts.piclockRefKey + ? " — picture-lock reference built" + : body.piclockSkippedReason + ? ` — picture lock skipped (${body.piclockSkippedReason})` + : ""), }, { ...(versionId ? { versionId } : {}),