Files
vfxreview/lib/render-pipeline/project-templates.ts
T
twotalesanimation caaa333f02
Deploy / deploy (push) Failing after 19m11s
feat(pipeline): render-derived shot thumbnails, and bulk shot delete
Two independent features landing together:

Generated shot thumbnails — a shot can now carry a thumbnail promoted
from a render export (Shot.generatedThumbnailUrl/generatedThumbnailFrom)
alongside the existing brief-supplied thumbnailUrl, which always takes
display precedence (lib/shot-thumbnail.ts) so a promoted frame can only
ever fill a gap, never overwrite the brief. Shows with
pipelineConfig.autoShotThumbnail promote automatically on every
finished export (lib/render-pipeline/preview.ts); promoteExportThumbnail/
clearGeneratedThumbnail (actions/shots.ts) are the manual equivalent for
everyone else. Worker-side seek logic for picking the promoted frame is
covered by RenderWorker/VFXReviewWorker.Tests/ThumbnailSeekTests.cs.

Bulk shot delete — the shot-status page's selection bar gets a "Bulk
Actions" dropdown (alongside the existing Change Due Date) with Delete
Shots. bulkDeleteShots() (actions/shots.ts) deletes one at a time rather
than in a single statement: exports_shotId_fkey is ON DELETE RESTRICT
by design, so a shot with renders or deliveries on record is protected
and gets skipped and reported by name rather than aborting the whole
selection or being silently destroyed along with its history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:06:55 +02:00

120 lines
4.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(),
/**
* Promote each finished export's thumbnail onto its shot
* (`Shot.generatedThumbnailUrl`). Off by default and deliberately per-show:
* productions that ship a still with every shot already have a reference
* frame, and the renders should not compete with it. Turn it on for shows
* that arrive with no stills at all, where an empty grid is the alternative.
*
* Never touches `Shot.thumbnailUrl` — see lib/shot-thumbnail.ts.
*/
autoShotThumbnail: z.boolean().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",
};
}
/**
* Whether finished exports promote their thumbnail onto the shot. Defaults to
* false, including when `pipelineConfig` is malformed — a broken config must
* not start rewriting shot thumbnails.
*/
export function autoShotThumbnailEnabled(project: { pipelineConfig?: unknown }): boolean {
const parsed = pipelineConfigSchema.safeParse(project.pipelineConfig ?? {});
return parsed.success ? parsed.data.autoShotThumbnail === true : false;
}
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,
};
}