Files
vfxreview/lib/render-pipeline/exports.ts
T
twotalesanimation 9a49cdc6a3
Deploy / deploy (push) Failing after 1m52s
RenderPipeline2
2026-08-01 15:44:26 +02:00

176 lines
5.1 KiB
TypeScript

import { z } from "zod";
import { db } from "@/lib/db";
import { Prisma } from "@prisma/client";
const exportManifestSchema = z.object({
shotCode: z.string().min(1),
projectCode: z.string().min(1),
aepPath: z.string().min(1),
compName: z.string().min(1),
rendererType: z.string().default("aerender"),
outputDir: z.string().optional(),
outputPattern: z.string().optional(),
frameStart: z.number().int().positive(),
frameEnd: z.number().int().positive(),
fps: z.number().positive(),
width: z.number().int().positive().optional(),
height: z.number().int().positive().optional(),
colorspace: z.string().optional(),
});
export function validateExportManifest(input: unknown) {
return exportManifestSchema.parse(input);
}
export function buildVersionString(versionNumber: number) {
return `v${versionNumber.toString().padStart(3, "0")}`;
}
export function deriveOutputBaseName(outputPattern: string) {
const match = outputPattern.match(/^(.*)\.(?:\[#+\]|\{#+\})\.([A-Za-z0-9]+)$/);
return match ? match[1] : outputPattern;
}
export async function createRenderExport(input: {
shotId: string;
projectId: string;
manifest: unknown;
submittedById?: string | null;
submittedByName?: string | null;
taskId?: string | null;
}) {
const manifest = validateExportManifest(input.manifest);
const shot = await db.shot.findUnique({
where: { id: input.shotId },
select: { id: true, shotVersion: true, exrOutput: true, projectId: true },
});
if (!shot) {
throw new Error("Shot not found");
}
if (shot.projectId !== input.projectId) {
throw new Error("Shot does not belong to the supplied project");
}
const versionNumber = parseInt((shot.shotVersion ?? "v001").replace(/^v/i, ""), 10) + 1;
const versionString = buildVersionString(versionNumber);
const outputPattern = manifest.outputPattern ?? `${manifest.compName}_TT_${versionString}.[####].exr`;
const outputDir = manifest.outputDir ?? `${shot.exrOutput ?? manifest.compName}_${versionString}`;
const result = await db.$transaction(async (tx) => {
const activeExports = await tx.export.findMany({
where: {
shotId: input.shotId,
status: { in: ["QUEUED", "RENDERING", "VALIDATING", "GENERATING_PREVIEW", "READY_FOR_QC", "READY_FOR_DELIVERY"] },
},
select: { id: true },
});
const exportRecord = await tx.export.create({
data: {
shotId: input.shotId,
projectId: input.projectId,
taskId: input.taskId ?? undefined,
versionNumber,
versionString,
status: "QUEUED",
aepPath: manifest.aepPath,
compName: manifest.compName,
rendererType: manifest.rendererType,
outputDir,
outputPattern,
frameStart: manifest.frameStart,
frameEnd: manifest.frameEnd,
fps: manifest.fps,
width: manifest.width ?? 1920,
height: manifest.height ?? 1080,
colorspace: manifest.colorspace,
submittedById: input.submittedById ?? undefined,
submittedByName: input.submittedByName ?? undefined,
},
});
await tx.exportEvent.createMany({
data: [
{
exportId: exportRecord.id,
toStatus: "QUEUED",
actorType: "USER",
actorId: input.submittedById ?? undefined,
note: "Queued from panel",
},
],
});
await tx.renderJob.create({
data: {
type: "AE_RENDER",
exportId: exportRecord.id,
attempt: 1,
maxAttempts: 3,
status: "QUEUED",
manifest: manifest as Prisma.JsonObject,
priority: 50,
},
});
await tx.shot.update({
where: { id: input.shotId },
data: {
shotVersion: versionString,
exrOutput: deriveOutputBaseName(outputPattern),
},
});
if (activeExports.length > 0) {
await tx.export.updateMany({
where: { id: { in: activeExports.map((item) => item.id) } },
data: {
status: "SUPERSEDED",
supersededById: exportRecord.id,
statusChangedAt: new Date(),
},
});
await tx.exportEvent.createMany({
data: activeExports.map((item) => ({
exportId: item.id,
fromStatus: "QUEUED",
toStatus: "SUPERSEDED",
actorType: "SYSTEM",
note: "Superseded by newer export",
})),
});
}
return { exportRecord, versionString };
});
return result;
}
export async function listExportsForQueue() {
return db.export.findMany({
where: { status: { notIn: ["SUPERSEDED", "ARCHIVED", "CANCELLED", "DELIVERED"] } },
orderBy: [{ createdAt: "desc" }],
include: {
shot: { select: { shotCode: true, shotVersion: true, project: { select: { code: true } } } },
renderJobs: { orderBy: [{ createdAt: "desc" }], take: 1 },
},
});
}
export async function getLatestExportForShot(shotId: string) {
return db.export.findFirst({
where: { shotId },
orderBy: [{ createdAt: "desc" }],
include: {
shot: { select: { shotCode: true, project: { select: { code: true } } } },
renderJobs: { orderBy: [{ createdAt: "desc" }], take: 1 },
validations: true,
},
});
}