fix(pipeline): trim picture-lock handles at the footage comp, not the template's SHOT layer
Deploy / deploy (push) Successful in 6m17s

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 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-08-23 14:22:49 +02:00
parent 86962eb4bd
commit 67d8639861
3 changed files with 55 additions and 32 deletions
+46 -31
View File
@@ -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;
}