cc89415a29
One "Queue Export" click now renders the EXR sequence, then rebuilds the shot headlessly with the studio slate/overlay template to produce the delivery MOV and review MP4. Implements RenderPipeline2 phases 1-2 plus the preview stage. Server: - New models Export, RenderJob, ExportEvent, Machine, WorkerHeartbeat, plus Project.deliveryConfig and per-submission slate fields (Export.vfxScope, Export.submissionNote, inherited from the shot's previous export). Both migrations are purely additive; no existing column is touched. - lib/render-pipeline: server-enforced state machine, transactional version increment with supersede, atomic FOR UPDATE SKIP LOCKED claim gated by machine availability windows, and a lease reaper run from instrumentation.ts. - /api/ext/* endpoints for the panel and workers; session-auth mirrors under /api/render and /api/machines for the web UI. - Pipeline pages: render queue, export detail, machine monitoring, plus an Exports tab on shot detail. RenderWorker (.NET 8 Windows service, new): - Registration, heartbeat as cancel channel, claim loop, aerender runner with progress parsing and stall watchdog, crash recovery and disk-spooled reporting that survives server downtime. - Preview stage: headless AE assembles the preview comp into a throwaway AEP with both output modules queued, then a single aerender pass renders them. Preview jobs are not claimed while an interactive AE session is open, so an artist's project is never taken over. AE panel: Queue Export with live status polling, urgent flag, retry, and the VFX Scope / Submission Note fields. Every existing panel action is unchanged. Preview chaining ships disabled behind SystemConfig preview.enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
import { db } from "@/lib/db";
|
|
|
|
/**
|
|
* Pipeline-wide settings live in the existing SystemConfig key/value table
|
|
* (RenderPipeline2 §14.1) so the fleet can be tuned without redeploys.
|
|
*/
|
|
export const PIPELINE_CONFIG_DEFAULTS = {
|
|
"render.pollSeconds": 10,
|
|
"render.heartbeatSeconds": 30,
|
|
"render.leaseSeconds": 300,
|
|
"render.maxAttempts": 3,
|
|
"render.stallTimeoutSeconds": 600,
|
|
"render.urgentPriorityThreshold": 20,
|
|
"render.outputRoot": "//SAN/renders",
|
|
"validation.sampleEvery": 25,
|
|
"preview.maxWidth": 1920,
|
|
// Preview stage (§9): headless AE rebuild of the render with slate/burn-ins
|
|
"preview.enabled": "true",
|
|
"preview.templateAep": "X:/shared_projects_2026/UNGO_VFX/production/working_files/UNG_VFXOVERLAY_SLATE_TEMPLATE.aep",
|
|
"preview.templateComp": "UNG_EXPORT_TEMPLATE",
|
|
"preview.overlayComp": "UNG_VFX_OVERLAY",
|
|
"preview.lutComp": "_SHOW LUT",
|
|
"preview.movTemplate": "4444 Tri",
|
|
"preview.mp4Template": "REVIEW_PREVIEW",
|
|
// Essential Property names on the NETFLIX_SLATE layer for the per-submission
|
|
// fields. If a name is wrong the build logs the template's actual property
|
|
// names as a warning, so it is a config fix rather than a code change.
|
|
"preview.slateScopeProp": "Scope",
|
|
"preview.slateSubmissionProp": "Notes",
|
|
} as const;
|
|
|
|
export type PipelineConfigKey = keyof typeof PIPELINE_CONFIG_DEFAULTS;
|
|
|
|
export async function getConfigValue(key: PipelineConfigKey): Promise<string> {
|
|
const row = await db.systemConfig.findUnique({ where: { key } });
|
|
return row?.value ?? String(PIPELINE_CONFIG_DEFAULTS[key]);
|
|
}
|
|
|
|
export async function getConfigNumber(key: PipelineConfigKey): Promise<number> {
|
|
const raw = await getConfigValue(key);
|
|
const n = Number(raw);
|
|
return Number.isFinite(n) ? n : Number(PIPELINE_CONFIG_DEFAULTS[key]);
|
|
}
|
|
|
|
export async function getConfigBoolean(key: PipelineConfigKey): Promise<boolean> {
|
|
const raw = (await getConfigValue(key)).trim().toLowerCase();
|
|
return raw === "true" || raw === "1" || raw === "yes";
|
|
}
|
|
|
|
/** Preview-stage settings handed to the worker inside the PREVIEW_ONLY manifest. */
|
|
export async function getPreviewConfig() {
|
|
const [
|
|
enabled, templateAep, templateComp, overlayComp, lutComp, movTemplate, mp4Template,
|
|
slateScopeProp, slateSubmissionProp,
|
|
] = await Promise.all([
|
|
getConfigBoolean("preview.enabled"),
|
|
getConfigValue("preview.templateAep"),
|
|
getConfigValue("preview.templateComp"),
|
|
getConfigValue("preview.overlayComp"),
|
|
getConfigValue("preview.lutComp"),
|
|
getConfigValue("preview.movTemplate"),
|
|
getConfigValue("preview.mp4Template"),
|
|
getConfigValue("preview.slateScopeProp"),
|
|
getConfigValue("preview.slateSubmissionProp"),
|
|
]);
|
|
return {
|
|
enabled, templateAep, templateComp, overlayComp, lutComp, movTemplate, mp4Template,
|
|
slateScopeProp, slateSubmissionProp,
|
|
};
|
|
}
|
|
|
|
/** Worker-facing config bundle returned by E6 registration. */
|
|
export async function getWorkerConfig() {
|
|
const [pollSeconds, heartbeatSeconds, leaseSeconds, maxAttempts, stallTimeoutSeconds] =
|
|
await Promise.all([
|
|
getConfigNumber("render.pollSeconds"),
|
|
getConfigNumber("render.heartbeatSeconds"),
|
|
getConfigNumber("render.leaseSeconds"),
|
|
getConfigNumber("render.maxAttempts"),
|
|
getConfigNumber("render.stallTimeoutSeconds"),
|
|
]);
|
|
return { pollSeconds, heartbeatSeconds, leaseSeconds, maxAttempts, stallTimeoutSeconds };
|
|
}
|