2e9fa45580
Deploy / deploy (push) Successful in 3m31s
Renders run 8 frames past the cut at each end. Directors were flagging glitches in unfixed handles as shot problems, so the player now marks them — without trimming or burning anything into the EXRs/MOV/MP4. - Version.handleHead/handleTail/pictureStartFrame, Export.handleHead/ handleTail; migration backfills pipeline exports and their preview versions from the manifest's handleFrames (else 8) - AE panel: "Includes handles [8] frames each end" -> manifest.review; older manifests fall back to source.handleFrames - finalize copies handles onto the Version; worker 1.6.4 reports the head slate length (pictureStartSeconds), else the player infers it as MP4 frames - EXR frameCount - Player: hatched HANDLE / SLATE regions and cut marks on the timeline, "HEAD/TAIL HANDLE · NOT IN EDIT" overlay on handle frames, HANDLE badge on notes, "Edit range only" toggle (default on in the client portal) - Staff Handles editor on the review page for manual uploads/corrections Also fixes two player bugs found while testing: timeToFrame floored float error (every seek landed a frame early), and J/reverse never ran (the keyboard effect's cleanup stopped it on the next render). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
5.4 KiB
TypeScript
139 lines
5.4 KiB
TypeScript
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<typeof plateRefSchema>;
|
|
|
|
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<typeof renderManifestSchema>;
|
|
|
|
/**
|
|
* 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}` };
|
|
}
|