feat(pipeline): skip the preview build and inherit the previous one
Deploy / deploy (push) Successful in 7m8s
Deploy / deploy (push) Successful in 7m8s
Some exports are deliberately framed differently from their review — unscaled or un-punched-in EXRs — and a preview rebuilt from those frames would be the wrong picture. The previous export's MOV is the correct one. skipPreview renders the EXRs and goes straight to READY_FOR_QC. Available at Queue Export (a "No preview" checkbox in the panel, E1 field) and as an option when overriding a validation failure. The inheritance is as important as the skip: deliveryMovPath, the review MP4, the thumbnail and the Version are carried over from the newest earlier export of that shot which has them. Without it the delivery builder — which takes its MOV from deliveryMovPath — would package EXRs with no MOV at all. Where there is nothing to inherit, the export still reaches QC and the timeline says it has no review media. QC is untouched by any of this: a skipped preview still needs a human pass before the export can be delivered. Panel 2.3.0. Adds migration 20260814000000_export_skip_preview. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,13 @@ export interface CreateExportInput {
|
||||
*/
|
||||
vfxScope?: string | null;
|
||||
submissionNote?: string | null;
|
||||
/**
|
||||
* Render the EXRs but build no preview, inheriting the previous export's
|
||||
* review media. For deliverables framed differently from the review — an
|
||||
* unscaled or un-punched-in export — rebuilding the preview from these
|
||||
* frames would produce the wrong MOV.
|
||||
*/
|
||||
skipPreview?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,6 +204,7 @@ export async function createExport(input: CreateExportInput) {
|
||||
colorspace: manifest.expected?.colorspace ?? null,
|
||||
vfxScope,
|
||||
submissionNote,
|
||||
skipPreview: input.skipPreview === true,
|
||||
submittedById: submittedBy?.id ?? null,
|
||||
submittedByName: submittedBy?.name ?? input.submittedByEmail ?? null,
|
||||
},
|
||||
@@ -238,7 +246,8 @@ export async function createExport(input: CreateExportInput) {
|
||||
actorType: "USER",
|
||||
actorId: submittedBy?.id ?? null,
|
||||
note:
|
||||
`Queue Export ${versionString}${input.force ? " (force)" : ""} — ` +
|
||||
`Queue Export ${versionString}${input.force ? " (force)" : ""}` +
|
||||
`${input.skipPreview ? " (skip preview)" : ""} — ` +
|
||||
`${provenance.ok ? "" : "WARNING: "}${provenance.note}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { applyTransition } from "./transitions";
|
||||
import { getConfigBoolean, getConfigNumber } from "./config";
|
||||
import { machineCanClaim } from "./machines";
|
||||
import { reapExpiredLeases } from "./reaper";
|
||||
import { buildPreviewManifest, createPreviewJob } from "./preview";
|
||||
import { buildPreviewManifest, createPreviewJob, inheritPreviewMedia } from "./preview";
|
||||
|
||||
const JOB_TYPES: RenderJobType[] = ["AE_RENDER", "PREVIEW_ONLY", "DELIVERY_BUILD"];
|
||||
|
||||
@@ -313,7 +313,7 @@ export async function completeRender(
|
||||
// Validation disabled: hand off to the preview stage (headless AE rebuild
|
||||
// → delivery MOV + review MP4) exactly as before Phase 3.
|
||||
const previewEnabled = await getConfigBoolean("preview.enabled");
|
||||
if (previewEnabled) {
|
||||
if (previewEnabled && !exportRow.skipPreview) {
|
||||
const manifest = await buildPreviewManifest(exportRow);
|
||||
const created = await createPreviewJob(tx, exportRow, manifest, job.maxAttempts, job.priority);
|
||||
previewJob = { id: created.id, type: created.type };
|
||||
@@ -324,11 +324,14 @@ export async function completeRender(
|
||||
}, stats);
|
||||
exportResult = { id: exp.id, status: exp.status };
|
||||
} else {
|
||||
const inherited = exportRow.skipPreview
|
||||
? await inheritPreviewMedia(tx, exportRow)
|
||||
: { data: {}, note: "preview stage disabled" };
|
||||
const exp = await applyTransition(tx, job.export, "READY_FOR_QC", {
|
||||
type: "WORKER",
|
||||
id: machineId,
|
||||
note: `Render complete in ${body.renderSeconds ?? "?"}s (preview stage disabled)`,
|
||||
}, stats);
|
||||
note: `Render complete in ${body.renderSeconds ?? "?"}s — ${inherited.note}`,
|
||||
}, { ...stats, ...inherited.data });
|
||||
exportResult = { id: exp.id, status: exp.status };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,59 @@ export async function buildPreviewManifest(exportRow: ExportForPreview): Promise
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Carries the previous export's review media onto one that skipped its own
|
||||
* preview build (§9).
|
||||
*
|
||||
* An export whose framing differs from the review framing — unscaled or
|
||||
* un-punched-in EXRs — must not have a preview rebuilt from its own frames:
|
||||
* the earlier, correctly-framed MOV *is* the right preview. Inheriting it also
|
||||
* keeps the delivery package whole, since the builder takes its MOV from
|
||||
* `deliveryMovPath`.
|
||||
*
|
||||
* Returns the fields to apply and a note describing what happened, so the
|
||||
* export's timeline records where its media came from.
|
||||
*/
|
||||
export async function inheritPreviewMedia(
|
||||
tx: Prisma.TransactionClient,
|
||||
exportRow: { id: string; shotId: string }
|
||||
): Promise<{ data: Prisma.ExportUpdateInput; note: string }> {
|
||||
const donor = await tx.export.findFirst({
|
||||
where: {
|
||||
shotId: exportRow.shotId,
|
||||
id: { not: exportRow.id },
|
||||
OR: [{ previewMovKey: { not: null } }, { deliveryMovPath: { not: null } }],
|
||||
},
|
||||
orderBy: { versionNumber: "desc" },
|
||||
select: {
|
||||
versionString: true,
|
||||
deliveryMovPath: true,
|
||||
previewMovKey: true,
|
||||
thumbnailKey: true,
|
||||
versionId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!donor) {
|
||||
return {
|
||||
data: {},
|
||||
note: "no preview built and no earlier export to inherit one from — this export has no review media",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
deliveryMovPath: donor.deliveryMovPath,
|
||||
previewMovKey: donor.previewMovKey,
|
||||
thumbnailKey: donor.thumbnailKey,
|
||||
// The Version row stays attached to the export that actually produced the
|
||||
// media; this one only points at the same files.
|
||||
versionId: donor.versionId,
|
||||
},
|
||||
note: `preview skipped — review media inherited from ${donor.versionString}`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Creates the PREVIEW_ONLY job that follows a successful EXR render. */
|
||||
export async function createPreviewJob(
|
||||
tx: Prisma.TransactionClient,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { db } from "@/lib/db";
|
||||
import { PipelineError } from "./errors";
|
||||
import { applyTransition } from "./transitions";
|
||||
import { getConfigBoolean, getConfigNumber, getConfigValue, getPreviewConfig } from "./config";
|
||||
import { buildPreviewManifest, createPreviewJob } from "./preview";
|
||||
import { buildPreviewManifest, createPreviewJob, inheritPreviewMedia } from "./preview";
|
||||
|
||||
/**
|
||||
* Validation engine results (RenderPipeline2 §8). The worker runs the checks —
|
||||
@@ -172,13 +172,16 @@ export async function reportValidation(jobId: string, body: ValidationReport) {
|
||||
|
||||
// Passed: hand off to the preview stage, the same handoff completeRender
|
||||
// used to make directly before validation existed.
|
||||
if (!previewEnabled) {
|
||||
if (!previewEnabled || exportRow.skipPreview) {
|
||||
const inherited = exportRow.skipPreview
|
||||
? await inheritPreviewMedia(tx, exportRow)
|
||||
: { data: {}, note: "preview stage disabled" };
|
||||
const updated = await applyTransition(
|
||||
tx,
|
||||
exportRow,
|
||||
"READY_FOR_QC",
|
||||
{ type: "WORKER", id: body.machineId, note: `Validation passed (${summary}); preview stage disabled` },
|
||||
stats
|
||||
{ type: "WORKER", id: body.machineId, note: `Validation passed (${summary}); ${inherited.note}` },
|
||||
{ ...stats, ...inherited.data }
|
||||
);
|
||||
return {
|
||||
export: { id: updated.id, status: updated.status },
|
||||
@@ -221,6 +224,13 @@ export const overrideValidationSchema = z.object({
|
||||
* that decision the next person cannot reconstruct.
|
||||
*/
|
||||
reason: z.string().trim().min(3, "A reason is required").max(2000),
|
||||
/**
|
||||
* Go straight to QC, inheriting the previous export's review media instead
|
||||
* of building one. Correct when this export is framed differently from the
|
||||
* review — unscaled or un-punched-in frames — so a preview rebuilt from it
|
||||
* would be the wrong picture.
|
||||
*/
|
||||
skipPreview: z.boolean().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -239,7 +249,7 @@ export const overrideValidationSchema = z.object({
|
||||
*/
|
||||
export async function overrideValidation(
|
||||
exportId: string,
|
||||
input: { reason: string; userId: string; userName?: string | null }
|
||||
input: { reason: string; userId: string; userName?: string | null; skipPreview?: boolean }
|
||||
) {
|
||||
const previewEnabled = (await getPreviewConfig()).enabled;
|
||||
const maxAttempts = await getConfigNumber("render.maxAttempts");
|
||||
@@ -268,12 +278,18 @@ export async function overrideValidation(
|
||||
`Validation overridden by ${who}: ${input.reason.trim()}` +
|
||||
(waved.length ? ` — waved: ${waved.join(", ")}` : "");
|
||||
|
||||
if (!previewEnabled) {
|
||||
const updated = await applyTransition(tx, exportRow, "READY_FOR_QC", {
|
||||
type: "USER",
|
||||
id: input.userId,
|
||||
note: `${note} (preview stage disabled)`,
|
||||
});
|
||||
const skipPreview = input.skipPreview === true || exportRow.skipPreview;
|
||||
if (!previewEnabled || skipPreview) {
|
||||
const inherited = skipPreview
|
||||
? await inheritPreviewMedia(tx, exportRow)
|
||||
: { data: {}, note: "preview stage disabled" };
|
||||
const updated = await applyTransition(
|
||||
tx,
|
||||
exportRow,
|
||||
"READY_FOR_QC",
|
||||
{ type: "USER", id: input.userId, note: `${note} — ${inherited.note}` },
|
||||
inherited.data
|
||||
);
|
||||
return {
|
||||
export: { id: updated.id, status: updated.status },
|
||||
overridden: waved,
|
||||
|
||||
Reference in New Issue
Block a user