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:
@@ -108,6 +108,7 @@ export function ExportDetailClient({ exportId }: { exportId: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [overrideOpen, setOverrideOpen] = useState(false);
|
||||
const [overrideReason, setOverrideReason] = useState("");
|
||||
const [overrideSkipPreview, setOverrideSkipPreview] = useState(false);
|
||||
const [overriding, setOverriding] = useState(false);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
@@ -135,7 +136,7 @@ export function ExportDetailClient({ exportId }: { exportId: string }) {
|
||||
const res = await fetch(`/api/render/exports/${exportId}/override-validation`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ reason: overrideReason }),
|
||||
body: JSON.stringify({ reason: overrideReason, skipPreview: overrideSkipPreview }),
|
||||
});
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
@@ -240,6 +241,21 @@ export function ExportDetailClient({ exportId }: { exportId: string }) {
|
||||
placeholder="e.g. plate unreachable from the render node; timecode checked by hand against the EDL"
|
||||
className="w-full rounded-md border border-zinc-800 bg-zinc-950 px-3 py-2 text-sm text-zinc-200 placeholder:text-zinc-600"
|
||||
/>
|
||||
<label className="flex items-start gap-2 text-xs text-zinc-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={overrideSkipPreview}
|
||||
onChange={(ev) => setOverrideSkipPreview(ev.target.checked)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span>
|
||||
Skip the preview build and reuse the previous export's review media.
|
||||
<span className="block text-zinc-500">
|
||||
For EXRs framed differently from the review — unscaled or un-punched-in — where a
|
||||
preview rebuilt from these frames would be the wrong picture.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -254,6 +270,7 @@ export function ExportDetailClient({ exportId }: { exportId: string }) {
|
||||
onClick={() => {
|
||||
setOverrideOpen(false);
|
||||
setOverrideReason("");
|
||||
setOverrideSkipPreview(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createExport } from "@/lib/render-pipeline/exports";
|
||||
|
||||
// ── POST /api/ext/exports (E1 — Queue Export) ────────────────────────────────
|
||||
//
|
||||
// Body: { manifest: RenderManifest, submittedByEmail?, priority?, force? }
|
||||
// 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.
|
||||
|
||||
@@ -20,6 +20,7 @@ export async function POST(req: NextRequest) {
|
||||
force?: boolean;
|
||||
vfxScope?: string | null;
|
||||
submissionNote?: string | null;
|
||||
skipPreview?: boolean;
|
||||
};
|
||||
try {
|
||||
body = await req.json();
|
||||
@@ -38,6 +39,7 @@ export async function POST(req: NextRequest) {
|
||||
// undefined inherits the shot's previous export values
|
||||
vfxScope: body.vfxScope,
|
||||
submissionNote: body.submissionNote,
|
||||
skipPreview: body.skipPreview === true,
|
||||
});
|
||||
return NextResponse.json(result, { status: 201 });
|
||||
} catch (err) {
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function POST(
|
||||
|
||||
const result = await overrideValidation(exportId, {
|
||||
reason: parsed.data.reason,
|
||||
skipPreview: parsed.data.skipPreview,
|
||||
userId: user.id,
|
||||
userName: user.name ?? user.email ?? null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user