Files
vfxreview/lib/render-pipeline/project-templates.ts
T
twotalesanimation bada122f42
Deploy / deploy (push) Successful in 7m4s
feat(pipeline): per-project AE templates, and a workspace that fits any show
Initialise Workspace built UNG_101..UNG_117 from a hardcoded range and
imported one hardcoded AEP, and fourteen findComp calls named UNG's comps
literally. None of it worked for a second show.

Comp names now follow a convention derived from the show id —
{showId}_VFX_OVERLAY, {showId}_EXPORT_TEMPLATE, _SHOW LUT — which
reproduces UNG's existing names exactly, so nothing changes for the show
it was written for. A project whose template departs from the pattern can
override any of the three.

Paths cannot be derived: UNG's show id is UNG while its folder is
UNGO_VFX. Those live on Project.pipelineConfig and are edited under
Project → Settings → Pipeline. They are resolved server-side and handed
to the panel with the project list, and read by buildPreviewManifest for
the headless build, so the panel and the farm cannot disagree about which
template — or which show LUT — a shot was built with. That mattered more
than the folder names: a preview rendered through another show's LUT is
quietly the wrong colour rather than obviously broken.

Episode folders come from the project's episode list, falling back to the
folders under its footage root for a show whose episodes are not entered
yet.

Panel 2.5.0. Adds migration 20260814010000_project_pipeline_config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 08:24:40 +02:00

100 lines
3.8 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(),
/** 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;
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] = await Promise.all([
getConfigValue("preview.templateAep"),
getConfigValue("preview.movTemplate"),
getConfigValue("preview.mp4Template"),
]);
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", ""),
overlayComp: pick("overlayComp", convention.overlayComp),
previewComp: pick("previewComp", convention.previewComp),
lutComp: pick("lutComp", convention.lutComp),
movTemplate: pick("movTemplate", defaultMov),
mp4Template: pick("mp4Template", defaultMp4),
explicit,
};
}