From a18dfc62036ddbea59ffb764ebaa0e427556655a Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Sun, 23 Aug 2026 21:44:45 +0200 Subject: [PATCH] revert(pipeline): back out picture-lock trimming from the preview build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production fallout after enabling it: the trimmed EXR footage started after the head handle as intended, but the overlay's burn-in timecode (hardcoded to start at frame 1001) did not shift with it, so the delivered burn-in no longer matched the footage. The picture-lock reference render also never worked correctly in practice. Reverts the AE panel checkbox, the preview-build script's trim/reference logic, and the server/worker plumbing around it back to their pre-feature state (82dfaee) — untrimmed previews, no picture-lock reference, overlay timecode correct again. The noPictureLock/piclockRefKey schema columns and their migration are deliberately left in place: they're already applied in production, and dropping an applied migration's file risks the next `prisma migrate deploy`. They sit unused and harmless until this is revisited. Co-Authored-By: Claude Sonnet 5 --- RenderWorker/VFXReviewWorker/Dtos.cs | 17 -- RenderWorker/VFXReviewWorker/JobExecutor.cs | 15 -- RenderWorker/VFXReviewWorker/PreviewStage.cs | 35 +--- RenderWorker/scripts/vfxr_build_preview.jsx | 193 +----------------- VFXReviewConnector.jsx | 10 - .../exports/[exportId]/ExportDetailClient.tsx | 13 +- app/api/ext/exports/route.ts | 4 +- .../jobs/[jobId]/artifact-presign/route.ts | 1 - lib/render-pipeline/config.ts | 11 +- lib/render-pipeline/exports.ts | 9 +- lib/render-pipeline/preview.ts | 45 +--- lib/render-pipeline/project-templates.ts | 9 +- 12 files changed, 21 insertions(+), 341 deletions(-) diff --git a/RenderWorker/VFXReviewWorker/Dtos.cs b/RenderWorker/VFXReviewWorker/Dtos.cs index 108a18d..8143cc7 100644 --- a/RenderWorker/VFXReviewWorker/Dtos.cs +++ b/RenderWorker/VFXReviewWorker/Dtos.cs @@ -157,19 +157,6 @@ public sealed class PreviewManifest public string? SlateSubmissionProp { get; set; } public SlateInfo Slate { get; set; } = new(); - /// Skip picture-lock trimming and the reference render entirely. - public bool NoPictureLock { get; set; } - /// "{footageRoot}/{showId}/_PICLOCKS" — empty when not configured. - public string? PiclockDir { get; set; } - /// e.g. "UNG_106" — the picture-lock file glob prefix. - public string? PiclockGlobPrefix { get; set; } - public string? SeqTimecodeStart { get; set; } - public string? SeqTimecodeEnd { get; set; } - /// Studio-standard head/tail handle count, applied symmetrically. - public int HandleFrames { get; set; } - /// Full-duration (handles included), ungraded picture-lock reference render. - public string? PiclockRefOutput { get; set; } - public int TotalFrames => FrameEnd - FrameStart + 1; } @@ -199,10 +186,6 @@ public sealed class PreviewBuildResult public int? FrameCount { get; set; } public int? Width { get; set; } public int? Height { get; set; } - /// Whether the picture-lock reference comp was found, positioned and queued. - public bool PiclockRefBuilt { get; set; } - /// Why the picture-lock reference/trim was skipped, when it was (non-fatal). - public string? PiclockSkippedReason { get; set; } } /// The Render Manifest (§6.2) — the fields the render and validation stages need; the rest is preserved raw. diff --git a/RenderWorker/VFXReviewWorker/JobExecutor.cs b/RenderWorker/VFXReviewWorker/JobExecutor.cs index 01f270f..b5a8d67 100644 --- a/RenderWorker/VFXReviewWorker/JobExecutor.cs +++ b/RenderWorker/VFXReviewWorker/JobExecutor.cs @@ -438,19 +438,6 @@ public sealed class JobExecutor job.Id, machineId, "thumbnail", result.ThumbnailPath, "image/jpeg", shutdown); } - // Diagnostic sidecar, not a deliverable — an upload failure here must - // never hold back the mov/mp4/thumbnail that already succeeded. - string? piclockRefKey = null; - if (result.PiclockRefPath is not null) - { - piclockRefKey = await _api.UploadArtifactAsync( - job.Id, machineId, "piclockRef", result.PiclockRefPath, "video/mp4", shutdown); - if (piclockRefKey is null) - { - _log.LogWarning("Picture-lock reference upload failed for job {JobId} (non-fatal)", job.Id); - } - } - _spooler.Enqueue("finalize", job.Id, new Dictionary { ["machineId"] = machineId, @@ -459,9 +446,7 @@ public sealed class JobExecutor ["deliveryMovPath"] = manifest.MovOutput, ["previewMovKey"] = previewKey, ["thumbnailKey"] = thumbKey, - ["piclockRefKey"] = piclockRefKey, }, - ["piclockSkippedReason"] = result.Build?.PiclockSkippedReason, ["renderStats"] = new Dictionary { ["previewSeconds"] = (int)result.Seconds, diff --git a/RenderWorker/VFXReviewWorker/PreviewStage.cs b/RenderWorker/VFXReviewWorker/PreviewStage.cs index 169a221..9eec8f3 100644 --- a/RenderWorker/VFXReviewWorker/PreviewStage.cs +++ b/RenderWorker/VFXReviewWorker/PreviewStage.cs @@ -14,8 +14,7 @@ public sealed record PreviewStageResult( string? ThumbnailPath, string? TempAep, PreviewBuildResult? Build, - double Seconds, - string? PiclockRefPath = null); + double Seconds); /// /// Stage 3 — preview generation (RenderPipeline2 §9), the second half of a @@ -60,8 +59,6 @@ public sealed class PreviewStage var movLocal = mapper.Map(manifest.MovOutput); var mp4Local = mapper.Map(manifest.Mp4Output); - var piclockDirLocal = string.IsNullOrWhiteSpace(manifest.PiclockDir) ? null : mapper.Map(manifest.PiclockDir); - var piclockRefLocal = string.IsNullOrWhiteSpace(manifest.PiclockRefOutput) ? null : mapper.Map(manifest.PiclockRefOutput); // Everything the script needs, in machine-local paths. var context = new @@ -86,13 +83,6 @@ public sealed class PreviewStage slateScopeProp = manifest.SlateScopeProp, slateSubmissionProp = manifest.SlateSubmissionProp, slate = manifest.Slate, - noPictureLock = manifest.NoPictureLock, - piclockDir = piclockDirLocal, - piclockGlobPrefix = manifest.PiclockGlobPrefix, - seqTimecodeStart = manifest.SeqTimecodeStart, - seqTimecodeEnd = manifest.SeqTimecodeEnd, - handleFrames = manifest.HandleFrames, - piclockRefOutput = piclockRefLocal, }; await File.WriteAllTextAsync(contextPath, JsonSerializer.Serialize(context, WorkerOptions.JsonOpts), shutdown); @@ -191,29 +181,12 @@ public sealed class PreviewStage var thumbnail = await TryMakeThumbnailAsync(job.Id, mp4Local, shutdown); - // Non-fatal by design: the picture-lock reference is a diagnostic aid, - // not part of the deliverable, so a missing file here must never hold - // back the mov/mp4 that already rendered successfully. - string? piclockRefResult = null; - if (build.PiclockRefBuilt) - { - if (!string.IsNullOrWhiteSpace(piclockRefLocal) && File.Exists(piclockRefLocal)) - { - piclockRefResult = piclockRefLocal; - } - else - { - _log.LogWarning("Picture-lock reference was queued but is missing after render: {Path}", piclockRefLocal); - } - } - sw.Stop(); - _log.LogInformation("Preview complete for job {JobId} in {Sec:F0}s (mov + mp4{Thumb}{Piclock})", - job.Id, sw.Elapsed.TotalSeconds, thumbnail is null ? "" : " + thumbnail", - piclockRefResult is null ? "" : " + piclock ref"); + _log.LogInformation("Preview complete for job {JobId} in {Sec:F0}s (mov + mp4{Thumb})", + job.Id, sw.Elapsed.TotalSeconds, thumbnail is null ? "" : " + thumbnail"); return new PreviewStageResult(true, true, null, movLocal, mp4Local, thumbnail, - build.TempAep, build, sw.Elapsed.TotalSeconds, piclockRefResult); + build.TempAep, build, sw.Elapsed.TotalSeconds); } private async Task RunAfterFxAsync( diff --git a/RenderWorker/scripts/vfxr_build_preview.jsx b/RenderWorker/scripts/vfxr_build_preview.jsx index a6a32ff..81c8b13 100644 --- a/RenderWorker/scripts/vfxr_build_preview.jsx +++ b/RenderWorker/scripts/vfxr_build_preview.jsx @@ -30,9 +30,7 @@ queued: [], frameCount: null, width: null, - height: null, - piclockRefBuilt: false, - piclockSkippedReason: null + height: null }; var resultPath = null; @@ -158,67 +156,6 @@ return null; } - // Mirrors the panel's parseTimecodeToSeconds exactly. - function parseTimecodeToSeconds(tc, fps) { - var parts = String(tc).split(/[:;]/); - if (parts.length < 4) { - return 0; - } - return parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + - parseInt(parts[2], 10) + parseInt(parts[3], 10) / (fps || 24); - } - - // Locates the picture-lock reference footage the same way the panel's - // pullPictureLock does: the _PICLOCKS project folder first, then a disk - // glob under ctx.piclockDir. Returns the FootageItem, or null (with a - // warn()) if it cannot be found — never fatal to the preview build. - function findPiclockFootage(ctx) { - var piclocksProjectFolder = findFolder("_PICLOCKS"); - var i; - var item; - var piclocksDir; - var files; - var importOptions; - - if (piclocksProjectFolder) { - for (i = 1; i <= app.project.numItems; i += 1) { - item = app.project.item(i); - if (item instanceof FootageItem && - item.parentFolder === piclocksProjectFolder && - item.name.indexOf(ctx.piclockGlobPrefix + "_") === 0) { - return item; - } - } - } - - if (!ctx.piclockDir) { - warn("Picture lock: no footage root configured for this project — skipping reference render"); - return null; - } - piclocksDir = new Folder(ctx.piclockDir); - if (!piclocksDir.exists) { - warn("Picture lock: folder not found: " + piclocksDir.fsName); - return null; - } - files = piclocksDir.getFiles(ctx.piclockGlobPrefix + "_*"); - if (!files || files.length < 1) { - warn("Picture lock: no file matching " + ctx.piclockGlobPrefix + "_* found in " + piclocksDir.fsName); - return null; - } - for (i = 0; i < files.length; i += 1) { - if (files[i] instanceof File) { - try { - importOptions = new ImportOptions(files[i]); - return app.project.importFile(importOptions); - } catch (importError) { - warn("Picture lock: import failed: " + importError.message); - return null; - } - } - } - return null; - } - // Mirrors the panel's addOCIOToLayer: effect added but disabled, so the // artist/farm chain matches exactly. function addOCIOToLayer(layer) { @@ -484,56 +421,22 @@ 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, contentDuration, fps); + ctx.shotCode + "_FOOTAGE", width, height, 1, duration, fps); if (precompFolder) { footageComp.parentFolder = precompFolder; } - if (contentStartFrame !== null) { - setCompStartFrame(footageComp, contentStartFrame); + if (ctx.frameStart !== null && ctx.frameStart !== undefined) { + setCompStartFrame(footageComp, ctx.frameStart); } - var footageLayer = footageComp.layers.add(footage); - if (headSkipSeconds > 0) { - footageLayer.startTime -= headSkipSeconds; - } - addOCIOToLayer(footageLayer); + addOCIOToLayer(footageComp.layers.add(footage)); // 4. Shot comp: footage + show LUT + overlay - var shotComp = app.project.items.addComp(ctx.shotCode, width, height, 1, contentDuration, fps); - if (contentStartFrame !== null) { - setCompStartFrame(shotComp, contentStartFrame); + var shotComp = app.project.items.addComp(ctx.shotCode, width, height, 1, duration, fps); + if (ctx.frameStart !== null && ctx.frameStart !== undefined) { + setCompStartFrame(shotComp, ctx.frameStart); } shotComp.layers.add(footageComp); @@ -607,10 +510,6 @@ warn("NETFLIX_SLATE layer not found in template — slate fields not filled"); } - // 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 targetOutPoint = shotLayer.inPoint + shotComp.duration; if (targetOutPoint > previewComp.duration) { @@ -620,85 +519,13 @@ previewComp.duration = targetOutPoint; result.previewComp = previewComp.name; - result.trimmedToPictureLock = trimmedToPictureLock; + // 6. Queue both outputs against the same preview comp. aerender will + // render the whole queue in one launch. app.project.renderQueue.showWindow(false); while (app.project.renderQueue.numItems > 0) { app.project.renderQueue.item(1).remove(); } - - // 5b. Picture-lock reference comp: the raw plate only — no OCIO, no - // show LUT, no overlay — spanning the FULL padded duration (handles - // included), so a supervisor can check back against it if a delivered - // edit is ever suspected to be off. Independent of the trim above: - // this one needs the actual picture-lock file and sequence timecodes, - // and is skipped gracefully (never fatal) if either is missing. - if (ctx.noPictureLock) { - if (!result.piclockSkippedReason) { - result.piclockSkippedReason = "No Picture Lock was set for this export"; - } - } else if (!ctx.seqTimecodeStart || !ctx.seqTimecodeEnd) { - warn("Picture lock: no sequence timecodes for this shot — reference render skipped"); - result.piclockSkippedReason = "no sequence timecodes for this shot"; - } else { - // A diagnostic aid, not a deliverable: any failure building or - // queuing it must warn and move on, never take down the mov/mp4 - // that already succeeded (queueOutput() throws on template errors). - try { - var piclockFootage = findPiclockFootage(ctx); - if (piclockFootage) { - var piclockStartTC = 0; - try { - if (piclockFootage.mainSource && typeof piclockFootage.mainSource.startTimecode === "number") { - piclockStartTC = piclockFootage.mainSource.startTimecode; - } - } catch (tcReadError) { - } - - var tcIn = parseTimecodeToSeconds(ctx.seqTimecodeStart, fps); - var tcOut = parseTimecodeToSeconds(ctx.seqTimecodeEnd, fps); - var refIn = tcIn - piclockStartTC - handleSeconds; - var refOut = tcOut - piclockStartTC + handleSeconds; - if (refIn < 0) { - refIn = 0; - } - if (refOut > piclockFootage.duration) { - refOut = piclockFootage.duration; - } - - var piclockRefComp = app.project.items.addComp( - ctx.shotCode + "_PICLOCK_REF", width, height, 1, refOut - refIn, fps); - if (previewsFolder) { - piclockRefComp.parentFolder = previewsFolder; - } - var piclockLayer = piclockRefComp.layers.add(piclockFootage); - var piclockScale = Math.max( - piclockRefComp.width / piclockFootage.width, - piclockRefComp.height / piclockFootage.height - ) * 100; - piclockLayer.property("Scale").setValue([piclockScale, piclockScale]); - piclockLayer.startTime = -refIn; - piclockLayer.inPoint = 0; - piclockLayer.outPoint = refOut - refIn; - - if (queueOutput(piclockRefComp, ctx.mp4Template, ctx.piclockRefOutput, "piclockRef")) { - result.piclockRefBuilt = true; - } else { - result.piclockSkippedReason = "no mp4 template or output path configured"; - } - } else if (!result.piclockSkippedReason) { - result.piclockSkippedReason = "picture-lock file not found"; - } - } catch (piclockError) { - warn("Picture lock: reference render skipped — " + - (piclockError && piclockError.toString ? piclockError.toString() : String(piclockError))); - result.piclockSkippedReason = "error building the reference render"; - result.piclockRefBuilt = false; - } - } - - // 6. Queue both outputs against the same preview comp. aerender will - // render the whole queue in one launch. queueOutput(previewComp, ctx.movTemplate, ctx.movOutput, "mov"); queueOutput(previewComp, ctx.mp4Template, ctx.mp4Output, "mp4"); diff --git a/VFXReviewConnector.jsx b/VFXReviewConnector.jsx index f2c856f..8dea799 100644 --- a/VFXReviewConnector.jsx +++ b/VFXReviewConnector.jsx @@ -3962,9 +3962,6 @@ var PANEL_VERSION = "2.5.0"; if (uiState.skipPreviewCheckbox && uiState.skipPreviewCheckbox.value) { body.skipPreview = true; } - if (uiState.noPictureLockCheckbox && uiState.noPictureLockCheckbox.value) { - body.noPictureLock = true; - } if (CONFIG.artistEmail) { body.submittedByEmail = CONFIG.artistEmail; @@ -4534,13 +4531,6 @@ var PANEL_VERSION = "2.5.0"; "For deliverables framed differently from the review — unscaled or\n" + "un-punched-in EXRs — where a preview rebuilt from these frames\n" + "would be the wrong picture."; - uiState.noPictureLockCheckbox = pipelineRow1.add("checkbox", undefined, "No Picture Lock"); - uiState.noPictureLockCheckbox.helpTip = - "Skip picture-lock trimming and the picture-lock reference render\n" + - "for this export.\n\n" + - "Check if picture lock is missing or wrong for this shot — the\n" + - "preview build will fall back to the full rendered range instead\n" + - "of trimming against bad data."; var slateNoteGroup = pipelinePanel.add("group"); slateNoteGroup.orientation = "row"; diff --git a/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx b/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx index a42a363..ba54937 100644 --- a/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx +++ b/app/(dashboard)/pipeline/exports/[exportId]/ExportDetailClient.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import Link from "next/link"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, RotateCcw, XCircle, ShieldAlert, Clapperboard } from "lucide-react"; +import { ArrowLeft, RotateCcw, XCircle, ShieldAlert } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; @@ -36,9 +36,6 @@ interface ExportDetail { vfxScope: string | null; submissionNote: string | null; submittedByName: string | null; - noPictureLock: boolean; - /** Full-duration, ungraded picture-lock reference MP4 — a diagnostic sidecar, not a Version. */ - piclockRefKey: string | null; shot: { id: string; shotCode: string; @@ -180,7 +177,6 @@ export function ExportDetailClient({ exportId }: { exportId: string }) { ["Colorspace", e.colorspace ?? "—"], ["EXR files", e.exrFileCount != null ? String(e.exrFileCount) : "—"], ["Submitted by", e.submittedByName ?? "—"], - ["Picture lock", e.noPictureLock ? "Skipped (No Picture Lock)" : "Trimmed to picture lock"], ]; return ( @@ -207,13 +203,6 @@ export function ExportDetailClient({ exportId }: { exportId: string }) {
- {e.piclockRefKey && ( - - - - )} {FAILED_STATUSES.includes(e.status) && (