feat(render-pipeline): sync task title to preview version, fill slate ShotName

finalizePreview now retitles the export's task so its version suffix matches the
preview that was just uploaded (UNG_102_019_010_cmp_TT_v004 -> _v005), which is
the rename the manual batch upload already does on rename-and-upload. The new
nextTaskTitle helper mirrors nextExrOutputBase so the task title, EXR base and
preview filename all derive the suffix the same way. It uses the export's
versionString rather than versionNumber, since the DB number is bumped to stay
monotonic per task while the filename follows the shot's shotVersion.

The rename only fires when a Version was actually created, runs in the same
transaction, and is recorded in the ExportEvent note; the finalize response now
returns the task id, title and whether it changed. Task status is deliberately
untouched (pipeline renders must not resurface in the review flow, S10.0).

The headless preview build was addressing the slate's identity fields by index
only, so ShotName was never filled on the farm. setSlateIdentity writes both the
named property and the index -- the name-with-index-fallback pattern the panel
already uses for VersionName -- and warns with the template's actual property
names when neither resolves, instead of failing silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-08-12 10:30:47 +02:00
parent c4abda2d63
commit 783e82eaf5
3 changed files with 98 additions and 5 deletions
+27 -4
View File
@@ -230,6 +230,26 @@
return names;
}
// Slate identity fields (version name, shot name). Current templates expose
// these under a stable name, older ones only positionally, and the two do
// not always line up — so write both. Setting the same property twice is
// harmless; the panel uses the same name-with-index-fallback pattern.
function setSlateIdentity(layer, propName, index, value, label) {
var byName;
var byIndex;
if (value === null || value === undefined || value === "") {
return; // leave the template's own default
}
byName = setEssentialProperty(layer, propName, value);
byIndex = setEssentialPropertyByIndex(layer, index, value);
if (!byName && !byIndex) {
warn(label + ": neither Essential Property \"" + propName + "\" nor index " +
index + " could be set. Available: " +
listEssentialPropertyNames(layer).join(", "));
}
}
// Per-submission fields (VFX Scope / Submission Note). The property names
// come from config; if one does not exist, report the template's actual
// property names so it can be corrected without another build.
@@ -439,12 +459,15 @@
var slateLayer = previewComp.layer("NETFLIX_SLATE");
if (slateLayer) {
// Index-addressed properties match the panel exactly (3 = version
// name, 10 = shot code); the rest are addressed by name.
setEssentialPropertyByIndex(slateLayer, 3, ctx.slate.versionName);
// Identity fields match the panel's indices (3 = version name,
// 10 = shot name) and are also written by name; the rest are
// addressed by name only.
// VersionName carries the full "{shotCode}_cmp_TT_{version}";
// ShotName is the bare shot code, with no version suffix.
setSlateIdentity(slateLayer, "VersionName", 3, ctx.slate.versionName, "Version Name");
setSlateIdentity(slateLayer, "ShotName", 10, ctx.slate.shotCode, "Shot Name");
setEssentialProperty(slateLayer, "Date", ctx.slate.date);
setEssentialProperty(slateLayer, "Desc", ctx.slate.description);
setEssentialPropertyByIndex(slateLayer, 10, ctx.slate.shotCode);
setEssentialProperty(slateLayer, "Episode", ctx.slate.episode);
setEssentialProperty(slateLayer, "Scene", ctx.slate.scene);
setEssentialProperty(slateLayer, "Frames", result.frameCount);
+32
View File
@@ -0,0 +1,32 @@
import { describe, it, expect } from "vitest";
import { nextTaskTitle } from "./preview";
describe("nextTaskTitle", () => {
it("swaps an existing version suffix", () => {
expect(nextTaskTitle("UNG_102_019_010_cmp_TT_v004", "v005")).toBe(
"UNG_102_019_010_cmp_TT_v005"
);
});
it("is idempotent when the title already matches", () => {
expect(nextTaskTitle("UNG_102_019_010_cmp_TT_v005", "v005")).toBe(
"UNG_102_019_010_cmp_TT_v005"
);
});
it("appends when the title carries no version yet", () => {
expect(nextTaskTitle("UNG_102_019_010_cmp_TT", "v005")).toBe(
"UNG_102_019_010_cmp_TT_v005"
);
});
it("only touches a trailing suffix, not a version inside the name", () => {
expect(nextTaskTitle("UNG_v001_019_010_cmp_TT_v004", "v005")).toBe(
"UNG_v001_019_010_cmp_TT_v005"
);
});
it("handles double-digit and uppercase suffixes", () => {
expect(nextTaskTitle("SHOT_cmp_TT_V12", "v013")).toBe("SHOT_cmp_TT_v013");
});
});
+39 -1
View File
@@ -129,6 +129,16 @@ export async function createPreviewJob(
});
}
/**
* `UNG_102_019_010_cmp_TT_v004` + v005 → `UNG_102_019_010_cmp_TT_v005`.
* Mirrors `nextExrOutputBase` (exports.ts) so the task title, the EXR base and
* the preview filename all carry the same version suffix.
*/
export function nextTaskTitle(current: string, versionString: string): string {
if (/_v\d+$/i.test(current)) return current.replace(/_v\d+$/i, `_${versionString}`);
return `${current}_${versionString}`;
}
export interface FinalizeInput {
artifacts?: {
deliveryMovPath?: string;
@@ -167,6 +177,7 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi
return {
export: { id: exportRow.id, status: exportRow.status },
version: { id: exportRow.versionId },
task: null,
alreadyApplied: true,
};
}
@@ -227,6 +238,29 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi
versionId = version.id;
}
// The task title carries the version it is showing (house convention:
// "UNG_102_019_010_cmp_TT_v004"). The preview just uploaded *is* the task's
// new latest version, so the title has to follow the file — the same rename
// the manual batch upload does on "rename-and-upload".
let taskTitle: string | null = null;
let taskRenamed = false;
if (versionId && exportRow.taskId) {
const task = await tx.task.findUnique({
where: { id: exportRow.taskId },
select: { title: true },
});
if (task) {
taskTitle = nextTaskTitle(task.title, exportRow.versionString);
taskRenamed = taskTitle !== task.title;
if (taskRenamed) {
await tx.task.update({
where: { id: exportRow.taskId },
data: { title: taskTitle },
});
}
}
}
const updated = await applyTransition(
tx,
exportRow,
@@ -234,7 +268,10 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi
{
type: "WORKER",
id: machineId,
note: `Preview complete in ${body.renderStats?.previewSeconds ?? "?"}s${versionId ? " — review version created" : " — no preview media reported"}`,
note:
`Preview complete in ${body.renderStats?.previewSeconds ?? "?"}s` +
(versionId ? " — review version created" : " — no preview media reported") +
(taskRenamed ? ` — task renamed to ${taskTitle}` : ""),
},
{
...(versionId ? { versionId } : {}),
@@ -248,6 +285,7 @@ export async function finalizePreview(jobId: string, machineId: string, body: Fi
return {
export: { id: updated.id, status: updated.status },
version: versionId ? { id: versionId } : null,
task: exportRow.taskId ? { id: exportRow.taskId, title: taskTitle, renamed: taskRenamed } : null,
alreadyApplied: false,
};
});