revert(pipeline): back out picture-lock trimming from the preview build
Deploy / deploy (push) Successful in 6m19s
Deploy / deploy (push) Successful in 6m19s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -157,19 +157,6 @@ public sealed class PreviewManifest
|
||||
public string? SlateSubmissionProp { get; set; }
|
||||
public SlateInfo Slate { get; set; } = new();
|
||||
|
||||
/// <summary>Skip picture-lock trimming and the reference render entirely.</summary>
|
||||
public bool NoPictureLock { get; set; }
|
||||
/// <summary>"{footageRoot}/{showId}/_PICLOCKS" — empty when not configured.</summary>
|
||||
public string? PiclockDir { get; set; }
|
||||
/// <summary>e.g. "UNG_106" — the picture-lock file glob prefix.</summary>
|
||||
public string? PiclockGlobPrefix { get; set; }
|
||||
public string? SeqTimecodeStart { get; set; }
|
||||
public string? SeqTimecodeEnd { get; set; }
|
||||
/// <summary>Studio-standard head/tail handle count, applied symmetrically.</summary>
|
||||
public int HandleFrames { get; set; }
|
||||
/// <summary>Full-duration (handles included), ungraded picture-lock reference render.</summary>
|
||||
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; }
|
||||
/// <summary>Whether the picture-lock reference comp was found, positioned and queued.</summary>
|
||||
public bool PiclockRefBuilt { get; set; }
|
||||
/// <summary>Why the picture-lock reference/trim was skipped, when it was (non-fatal).</summary>
|
||||
public string? PiclockSkippedReason { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>The Render Manifest (§6.2) — the fields the render and validation stages need; the rest is preserved raw.</summary>
|
||||
|
||||
@@ -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<string, object?>
|
||||
{
|
||||
["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<string, object?>
|
||||
{
|
||||
["previewSeconds"] = (int)result.Seconds,
|
||||
|
||||
@@ -14,8 +14,7 @@ public sealed record PreviewStageResult(
|
||||
string? ThumbnailPath,
|
||||
string? TempAep,
|
||||
PreviewBuildResult? Build,
|
||||
double Seconds,
|
||||
string? PiclockRefPath = null);
|
||||
double Seconds);
|
||||
|
||||
/// <summary>
|
||||
/// 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<int> RunAfterFxAsync(
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{e.piclockRefKey && (
|
||||
<a href={`/api/files/${e.piclockRefKey}`} target="_blank" rel="noreferrer">
|
||||
<Button variant="outline" size="sm">
|
||||
<Clapperboard className="h-4 w-4 mr-2 text-zinc-400" /> Picture Lock Reference
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
{FAILED_STATUSES.includes(e.status) && (
|
||||
<Button variant="outline" size="sm" onClick={() => action("retry")}>
|
||||
<RotateCcw className="h-4 w-4 mr-2 text-amber-400" /> Retry
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createExport } from "@/lib/render-pipeline/exports";
|
||||
|
||||
// ── POST /api/ext/exports (E1 — Queue Export) ────────────────────────────────
|
||||
//
|
||||
// Body: { manifest: RenderManifest, submittedByEmail?, priority?, force?, skipPreview?, noPictureLock? }
|
||||
// Body: { manifest: RenderManifest, submittedByEmail?, priority?, force?, skipPreview? }
|
||||
// The server decides the new version number, updates Shot.shotVersion/exrOutput,
|
||||
// supersedes older non-terminal exports, and creates Export(QUEUED) + RenderJob.
|
||||
|
||||
@@ -21,7 +21,6 @@ export async function POST(req: NextRequest) {
|
||||
vfxScope?: string | null;
|
||||
submissionNote?: string | null;
|
||||
skipPreview?: boolean;
|
||||
noPictureLock?: boolean;
|
||||
};
|
||||
try {
|
||||
body = await req.json();
|
||||
@@ -41,7 +40,6 @@ export async function POST(req: NextRequest) {
|
||||
vfxScope: body.vfxScope,
|
||||
submissionNote: body.submissionNote,
|
||||
skipPreview: body.skipPreview === true,
|
||||
noPictureLock: body.noPictureLock === true,
|
||||
});
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
} catch (err) {
|
||||
|
||||
@@ -16,7 +16,6 @@ const KIND_FOLDERS: Record<string, string> = {
|
||||
thumbnail: "image",
|
||||
metadata: "renders",
|
||||
log: "renders",
|
||||
piclockRef: "videos",
|
||||
};
|
||||
|
||||
export async function POST(
|
||||
|
||||
@@ -26,11 +26,7 @@ export const PIPELINE_CONFIG_DEFAULTS = {
|
||||
"validation.sampleEvery": 25,
|
||||
/** Per-check severity overrides, e.g. {"exr_type":"warn"} (§17.3). */
|
||||
"validation.overrides": "{}",
|
||||
/**
|
||||
* Frames of head/tail handle the studio pads every render with. Used for the
|
||||
* API timecode fallback (§8.1a) and, symmetrically, to trim the preview
|
||||
* build's client-facing comp back to the picture-lock edit (§9).
|
||||
*/
|
||||
/** Frames of head handle the plates carry; only used for the API timecode fallback (§8.1a). */
|
||||
"validation.handleFrames": 8,
|
||||
/**
|
||||
* Studio-wide delivery defaults (§11.2). A project's own `deliveryConfig`
|
||||
@@ -47,11 +43,6 @@ export const PIPELINE_CONFIG_DEFAULTS = {
|
||||
"preview.lutComp": "_SHOW LUT",
|
||||
"preview.movTemplate": "4444 Tri",
|
||||
"preview.mp4Template": "REVIEW_PREVIEW",
|
||||
// Bare footage root the preview build reads picture-lock plates from, at
|
||||
// `{footageRoot}/{showId}/_PICLOCKS` — same convention as the panel's own
|
||||
// "Footage Root" setting (VFXReviewConnector.md). Per-project override via
|
||||
// pipelineConfig.footageRoot for a show on different storage.
|
||||
"preview.footageRoot": "V:/_FOOTAGE",
|
||||
// Essential Property names on the NETFLIX_SLATE layer for the per-submission
|
||||
// fields. If a name is wrong the build logs the template's actual property
|
||||
// names as a warning, so it is a config fix rather than a code change.
|
||||
|
||||
@@ -70,11 +70,6 @@ export interface CreateExportInput {
|
||||
* frames would produce the wrong MOV.
|
||||
*/
|
||||
skipPreview?: boolean;
|
||||
/**
|
||||
* Skip picture-lock trimming and the picture-lock reference render for this
|
||||
* export — set when picture lock is missing or wrong for the shot.
|
||||
*/
|
||||
noPictureLock?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +205,6 @@ export async function createExport(input: CreateExportInput) {
|
||||
vfxScope,
|
||||
submissionNote,
|
||||
skipPreview: input.skipPreview === true,
|
||||
noPictureLock: input.noPictureLock === true,
|
||||
submittedById: submittedBy?.id ?? null,
|
||||
submittedByName: submittedBy?.name ?? input.submittedByEmail ?? null,
|
||||
},
|
||||
@@ -253,8 +247,7 @@ export async function createExport(input: CreateExportInput) {
|
||||
actorId: submittedBy?.id ?? null,
|
||||
note:
|
||||
`Queue Export ${versionString}${input.force ? " (force)" : ""}` +
|
||||
`${input.skipPreview ? " (skip preview)" : ""}` +
|
||||
`${input.noPictureLock ? " (no picture lock)" : ""} — ` +
|
||||
`${input.skipPreview ? " (skip preview)" : ""} — ` +
|
||||
`${provenance.ok ? "" : "WARNING: "}${provenance.note}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,18 +32,6 @@ export interface PreviewManifest {
|
||||
mp4Output: string;
|
||||
slateScopeProp: string;
|
||||
slateSubmissionProp: string;
|
||||
/** Skip picture-lock trimming and the reference render entirely (§9, picture lock). */
|
||||
noPictureLock: boolean;
|
||||
/** `{footageRoot}/{showId}/_PICLOCKS`, mirroring the panel's getShowFootageRoot(). */
|
||||
piclockDir: string;
|
||||
/** e.g. "UNG_106" — mirrors the panel's getEpisodePrefix(); the picture-lock glob prefix. */
|
||||
piclockGlobPrefix: string;
|
||||
seqTimecodeStart: string | null;
|
||||
seqTimecodeEnd: string | null;
|
||||
/** Studio-standard head/tail handle count, applied symmetrically. */
|
||||
handleFrames: number;
|
||||
/** Full-duration (handles included), ungraded picture-lock reference render. */
|
||||
piclockRefOutput: string;
|
||||
slate: {
|
||||
versionName: string;
|
||||
date: string;
|
||||
@@ -70,7 +58,6 @@ type ExportForPreview = {
|
||||
fps: number;
|
||||
vfxScope: string | null;
|
||||
submissionNote: string | null;
|
||||
noPictureLock: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,27 +78,18 @@ export async function buildPreviewManifest(exportRow: ExportForPreview): Promise
|
||||
description: true,
|
||||
notes: true,
|
||||
slateDescription: true,
|
||||
seqTimecodeStart: true,
|
||||
seqTimecodeEnd: true,
|
||||
project: { select: { name: true, code: true, showId: true, pipelineConfig: true } },
|
||||
},
|
||||
});
|
||||
if (!shot) throw new PipelineError(404, "Shot not found for export");
|
||||
|
||||
const [cfg, handleFrames] = await Promise.all([getPreviewConfig(), getConfigNumber("validation.handleFrames")]);
|
||||
const cfg = await getPreviewConfig();
|
||||
// Per-production templates: each show has its own slate/overlay AEP, and
|
||||
// above all its own show LUT. Falling back to another show's would render a
|
||||
// preview that is quietly the wrong colour.
|
||||
const templates = await resolveProjectTemplates(shot.project);
|
||||
const base = `${shot.shotCode}_cmp_TT_${exportRow.versionString}`;
|
||||
|
||||
// Same convention as the panel's getShowFootageRoot()/getEpisodePrefix(): the
|
||||
// picture-lock plate lives at {footageRoot}/{showId}/_PICLOCKS, glob-matched
|
||||
// by "{showId}[_{episode}]_*".
|
||||
const showId = shot.project.showId?.trim() || shot.project.code;
|
||||
const piclockDir = templates.footageRoot ? `${templates.footageRoot}/${showId}/_PICLOCKS` : "";
|
||||
const piclockGlobPrefix = shot.episode?.trim() ? `${showId}_${shot.episode.trim()}` : showId;
|
||||
|
||||
return {
|
||||
stage: "PREVIEW",
|
||||
exportId: exportRow.id,
|
||||
@@ -132,13 +110,6 @@ export async function buildPreviewManifest(exportRow: ExportForPreview): Promise
|
||||
mp4Output: `${exportRow.outputDir}/${base}.mp4`,
|
||||
slateScopeProp: cfg.slateScopeProp,
|
||||
slateSubmissionProp: cfg.slateSubmissionProp,
|
||||
noPictureLock: exportRow.noPictureLock,
|
||||
piclockDir,
|
||||
piclockGlobPrefix,
|
||||
seqTimecodeStart: shot.seqTimecodeStart,
|
||||
seqTimecodeEnd: shot.seqTimecodeEnd,
|
||||
handleFrames,
|
||||
piclockRefOutput: `${exportRow.outputDir}/${base}_PICLOCK_REF.mp4`,
|
||||
slate: {
|
||||
versionName: base,
|
||||
date: slateDate(),
|
||||
@@ -187,7 +158,6 @@ export async function inheritPreviewMedia(
|
||||
deliveryMovPath: true,
|
||||
previewMovKey: true,
|
||||
thumbnailKey: true,
|
||||
piclockRefKey: true,
|
||||
versionId: true,
|
||||
},
|
||||
});
|
||||
@@ -204,7 +174,6 @@ export async function inheritPreviewMedia(
|
||||
deliveryMovPath: donor.deliveryMovPath,
|
||||
previewMovKey: donor.previewMovKey,
|
||||
thumbnailKey: donor.thumbnailKey,
|
||||
piclockRefKey: donor.piclockRefKey,
|
||||
// The Version row stays attached to the export that actually produced the
|
||||
// media; this one only points at the same files.
|
||||
versionId: donor.versionId,
|
||||
@@ -298,13 +267,9 @@ export interface FinalizeInput {
|
||||
thumbnailKey?: string;
|
||||
metadataKey?: string;
|
||||
logFileKey?: string;
|
||||
/** Full-duration, ungraded picture-lock reference MP4 — a diagnostic sidecar, not a Version. */
|
||||
piclockRefKey?: string;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -473,12 +438,7 @@ 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}` : "") +
|
||||
(artifacts.piclockRefKey
|
||||
? " — picture-lock reference built"
|
||||
: body.piclockSkippedReason
|
||||
? ` — picture lock skipped (${body.piclockSkippedReason})`
|
||||
: ""),
|
||||
(taskRenamed ? ` — task renamed to ${taskTitle}` : ""),
|
||||
},
|
||||
{
|
||||
...(versionId ? { versionId } : {}),
|
||||
@@ -486,7 +446,6 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi
|
||||
...(artifacts.previewMovKey ? { previewMovKey: artifacts.previewMovKey } : {}),
|
||||
...(artifacts.thumbnailKey ? { thumbnailKey: artifacts.thumbnailKey } : {}),
|
||||
...(artifacts.metadataKey ? { metadataKey: artifacts.metadataKey } : {}),
|
||||
...(artifacts.piclockRefKey ? { piclockRefKey: artifacts.piclockRefKey } : {}),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ export const pipelineConfigSchema = z.object({
|
||||
templateAep: z.string().max(1000).optional().nullable(),
|
||||
/** Where per-shot pre-renders live, imported by Import Renders. */
|
||||
rendersRoot: z.string().max(1000).optional().nullable(),
|
||||
/** Bare footage root (e.g. "V:/_FOOTAGE") the panel's Pull Picture Lock reads
|
||||
* from at `{footageRoot}/{showId}/_PICLOCKS` — same convention as the panel's
|
||||
* own `getShowFootageRoot()`. */
|
||||
footageRoot: z.string().max(1000).optional().nullable(),
|
||||
/** Overrides for templates that depart from the {showId}_* convention. */
|
||||
overlayComp: z.string().max(200).optional().nullable(),
|
||||
previewComp: z.string().max(200).optional().nullable(),
|
||||
@@ -38,7 +34,6 @@ export type PipelineConfigInput = z.infer<typeof pipelineConfigSchema>;
|
||||
export interface ResolvedTemplates {
|
||||
templateAep: string;
|
||||
rendersRoot: string;
|
||||
footageRoot: string;
|
||||
overlayComp: string;
|
||||
previewComp: string;
|
||||
lutComp: string;
|
||||
@@ -83,11 +78,10 @@ export async function resolveProjectTemplates(project: {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const [defaultAep, defaultMov, defaultMp4, defaultFootageRoot] = await Promise.all([
|
||||
const [defaultAep, defaultMov, defaultMp4] = await Promise.all([
|
||||
getConfigValue("preview.templateAep"),
|
||||
getConfigValue("preview.movTemplate"),
|
||||
getConfigValue("preview.mp4Template"),
|
||||
getConfigValue("preview.footageRoot"),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -95,7 +89,6 @@ export async function resolveProjectTemplates(project: {
|
||||
// No sensible studio-wide default: a renders root that points at another
|
||||
// show's folder is worse than an empty one the panel can report.
|
||||
rendersRoot: pick("rendersRoot", ""),
|
||||
footageRoot: pick("footageRoot", defaultFootageRoot),
|
||||
overlayComp: pick("overlayComp", convention.overlayComp),
|
||||
previewComp: pick("previewComp", convention.previewComp),
|
||||
lutComp: pick("lutComp", convention.lutComp),
|
||||
|
||||
Reference in New Issue
Block a user