Files
vfxreview/lib/render-pipeline/project-templates.ts
T
twotalesanimation 86962eb4bd
Deploy / deploy (push) Successful in 10m37s
feat(pipeline): automate picture-lock trimming in the preview build
Previews used to ship the full padded (handle-included) render to
clients, with an artist manually pulling picture lock and hand-trimming
the comp to match. The automated preview stage now trims the client
comp to the picture-lock edit itself (a fixed handle count off each
end, per the studio's existing convention) and separately builds an
ungraded, full-duration picture-lock reference MP4 as a diagnostic
sidecar to check back against. A new "No Picture Lock" panel checkbox
is the single override for shots where picture lock is missing or
wrong, falling back to today's untrimmed behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 12:59:05 +02:00

107 lines
4.2 KiB
TypeScript

import { z } from "zod";
import { getConfigValue } from "./config";
/**
* Per-production AE workspace configuration.
*
* Comp names follow a convention derived from the show — `UNG_VFX_OVERLAY`,
* `UNG_EXPORT_TEMPLATE`, `_SHOW LUT` — so a project that names its template
* comps to the pattern needs no configuration at all. Anything that cannot be
* derived is stated explicitly: paths, above all, since a show's folder rarely
* matches its show id (UNG lives under `UNGO_VFX`).
*
* Resolved server-side and handed to both the panel (via the ext projects list)
* and the worker (inside the preview manifest), so the two cannot disagree
* about which template a render was built from.
*/
export const pipelineConfigSchema = z.object({
/** The show's slate/overlay/LUT template AEP, imported by Initialise Workspace. */
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(),
lutComp: z.string().max(200).optional().nullable(),
/** AE output-module templates, when this show does not use the studio defaults. */
movTemplate: z.string().max(200).optional().nullable(),
mp4Template: z.string().max(200).optional().nullable(),
});
export type PipelineConfigInput = z.infer<typeof pipelineConfigSchema>;
export interface ResolvedTemplates {
templateAep: string;
rendersRoot: string;
footageRoot: string;
overlayComp: string;
previewComp: string;
lutComp: string;
movTemplate: string;
mp4Template: string;
/** Which values came from the project rather than a convention or a default. */
explicit: string[];
}
/** The comp-name convention. UNG's existing templates already match it exactly. */
export function conventionalNames(showId: string) {
const prefix = showId?.trim() || "SHOW";
return {
overlayComp: `${prefix}_VFX_OVERLAY`,
previewComp: `${prefix}_EXPORT_TEMPLATE`,
// The LUT comp is named the same in every show's template; what differs is
// the LUT inside it, which is why the template AEP is per-project.
lutComp: "_SHOW LUT",
};
}
export async function resolveProjectTemplates(project: {
showId: string | null;
code: string;
pipelineConfig?: unknown;
}): Promise<ResolvedTemplates> {
const parsed = pipelineConfigSchema.safeParse(project.pipelineConfig ?? {});
const cfg: PipelineConfigInput = parsed.success ? parsed.data : {};
if (!parsed.success) {
console.error(`[render-pipeline] pipelineConfig for ${project.code} is malformed — using defaults`);
}
const showId = project.showId?.trim() || project.code;
const convention = conventionalNames(showId);
const explicit: string[] = [];
const pick = <K extends keyof PipelineConfigInput>(key: K, fallback: string): string => {
const value = cfg[key];
if (typeof value === "string" && value.trim()) {
explicit.push(key);
return value.trim();
}
return fallback;
};
const [defaultAep, defaultMov, defaultMp4, defaultFootageRoot] = await Promise.all([
getConfigValue("preview.templateAep"),
getConfigValue("preview.movTemplate"),
getConfigValue("preview.mp4Template"),
getConfigValue("preview.footageRoot"),
]);
return {
templateAep: pick("templateAep", defaultAep),
// 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),
movTemplate: pick("movTemplate", defaultMov),
mp4Template: pick("mp4Template", defaultMp4),
explicit,
};
}