import { z } from "zod"; /** * The Render Manifest (RenderPipeline2 §6.2). * Uploaded by the AE panel on Queue Export, snapshotted verbatim into * RenderJob.manifest, handed to the worker on claim. * * `outputDir` / `outputPattern` may be the literal string "auto" — the server * generates them from the shot's exrOutput convention and the NEW version * number it decides (§6.4 E1). */ /** * A plate element imported into the comp, resolved by the panel from the comp's * own footage layers — not re-derived from a path convention (§6.2). */ export const plateRefSchema = z.object({ element: z.string().optional(), /** First frame of the sequence on disk; the timecode reference file. */ path: z.string().min(1), frameStart: z.number().int().optional(), frameEnd: z.number().int().optional(), }); export type PlateRef = z.infer; export const renderManifestSchema = z .object({ manifestVersion: z.number().int().min(1).default(1), projectCode: z.string().min(1), shotCode: z.string().regex(/^[A-Za-z0-9_-]+$/, "shotCode contains invalid characters"), shotId: z.string().min(1), aepPath: z.string().min(1), compName: z.string().min(1), rendererType: z.string().default("aerender"), aeVersionHint: z.string().optional(), outputDir: z.string().min(1), outputPattern: z.string().min(1), outputModuleTemplate: z.string().optional(), renderSettingsTemplate: z.string().optional(), /** Comp timeline range handed to aerender. */ frameStart: z.number().int(), frameEnd: z.number().int(), /** * The number the first *output file* carries — the output module's starting * number (house convention 1001). Deliberately separate from frameStart: a * comp anchored to its plate's timecode starts in the millions, so file * numbering and comp frames are different coordinate systems. */ outputFrameStart: z.number().int().optional(), fps: z.number().positive(), width: z.number().int().positive(), height: z.number().int().positive(), expected: z .object({ colorspace: z.string().optional(), bitDepth: z.string().optional(), exrCompression: z.string().optional(), alpha: z.boolean().optional(), timecodeStart: z.string().optional(), }) .optional(), /** * Plate provenance for the timecode check (§8.1a). Optional so * manifestVersion 1 panels keep working — validation then degrades to a * worker-derived plate, and finally to the API value as a WARN. */ source: z .object({ /** * The element the comp's start time was anchored to (the panel's * primaryFootage). A shot's elements carry unrelated camera timecodes, * so the timing reference must be named rather than guessed. */ timingPlate: plateRefSchema.optional(), plates: z.array(plateRefSchema).optional(), /** * Verbatim from the panel's start-frame resolution, in the order it * ranks them: "EXR timecode …" (the plate's own header, best), * "footage metadata" (AE's read of it, trustworthy) or * "API timecode …" (fallback, suspect). Known at queue time, so a * suspect export is flagged before it burns render hours. */ startSource: z.string().optional(), startFrames: z.number().int().optional(), handleFrames: z.number().int().optional(), }) .optional(), preview: z .object({ template: z.string().optional(), burnins: z.boolean().optional(), }) .optional(), /** * Handles the render carries past the cut, from the panel's "Includes * handles" setting (0/0 = none). Review metadata only — the player flags * these frames; nothing is trimmed. Absent (older panels): falls back to * `source.handleFrames`, which every panel export has sent. */ review: z .object({ handleHead: z.number().int().min(0).max(1000), handleTail: z.number().int().min(0).max(1000), }) .optional(), }) .refine((m) => m.frameEnd >= m.frameStart, { message: "frameEnd must be >= frameStart", path: ["frameEnd"], }); export type RenderManifest = z.infer; /** * Queue-time read of the manifest's plate provenance (§8.1a). A comp whose * start time came from the API rather than the plate's own metadata is very * likely to fail the timecode check — worth recording before the render burns * machine hours, not after. */ export function describePlateProvenance(manifest: RenderManifest): { ok: boolean; note: string } { const src = manifest.source; if (!src?.timingPlate?.path) { return { ok: false, note: "no timing plate in manifest — timecode will be checked against a worker-derived plate", }; } // Only warn on a start source we positively know to be the API fallback. An // absent startSource means the comp predates this session (an AEP opened from // disk), which is normal — the worker still does the authoritative check. if (src.startSource && /\bAPI\b/i.test(src.startSource)) { return { ok: false, note: `comp start came from "${src.startSource}", not the plate's own metadata — timecode check likely to fail`, }; } return { ok: true, note: `timing plate ${src.timingPlate.element ?? src.timingPlate.path}` }; }