feat(pipeline): render queue, worker service and automated preview generation
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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ExportStatus" AS ENUM ('QUEUED', 'RENDERING', 'RENDER_FAILED', 'VALIDATING', 'VALIDATION_FAILED', 'GENERATING_PREVIEW', 'PREVIEW_FAILED', 'READY_FOR_QC', 'QC_FAILED', 'READY_FOR_DELIVERY', 'PACKAGED', 'DELIVERED', 'SUPERSEDED', 'ARCHIVED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RenderJobStatus" AS ENUM ('QUEUED', 'CLAIMED', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED', 'EXPIRED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RenderJobType" AS ENUM ('AE_RENDER', 'PREVIEW_ONLY', 'DELIVERY_BUILD');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ValidationStatus" AS ENUM ('PASS', 'FAIL', 'WARN', 'SKIPPED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "QCResult" AS ENUM ('PASS', 'FAIL');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "MachineStatus" AS ENUM ('ONLINE', 'OFFLINE', 'DISABLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "DeliveryStatus" AS ENUM ('DRAFT', 'QUEUED', 'BUILDING', 'READY', 'DELIVERED', 'FAILED', 'CANCELLED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "deliveryConfig" JSONB;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "exports" (
|
||||
"id" TEXT NOT NULL,
|
||||
"shotId" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"taskId" TEXT,
|
||||
"versionId" TEXT,
|
||||
"versionNumber" INTEGER NOT NULL,
|
||||
"versionString" TEXT NOT NULL,
|
||||
"status" "ExportStatus" NOT NULL DEFAULT 'QUEUED',
|
||||
"statusChangedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"aepPath" TEXT NOT NULL,
|
||||
"compName" TEXT NOT NULL,
|
||||
"rendererType" TEXT NOT NULL,
|
||||
"outputDir" TEXT NOT NULL,
|
||||
"outputPattern" TEXT NOT NULL,
|
||||
"frameStart" INTEGER NOT NULL,
|
||||
"frameEnd" INTEGER NOT NULL,
|
||||
"fps" DOUBLE PRECISION NOT NULL,
|
||||
"width" INTEGER NOT NULL,
|
||||
"height" INTEGER NOT NULL,
|
||||
"colorspace" TEXT,
|
||||
"deliveryMovPath" TEXT,
|
||||
"previewMovKey" TEXT,
|
||||
"thumbnailKey" TEXT,
|
||||
"metadataKey" TEXT,
|
||||
"exrFileCount" INTEGER,
|
||||
"exrTotalBytes" BIGINT,
|
||||
"checksum" TEXT,
|
||||
"submittedById" TEXT,
|
||||
"submittedByName" TEXT,
|
||||
"supersededById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "exports_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "render_jobs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" "RenderJobType" NOT NULL DEFAULT 'AE_RENDER',
|
||||
"exportId" TEXT,
|
||||
"deliveryId" TEXT,
|
||||
"attempt" INTEGER NOT NULL DEFAULT 1,
|
||||
"maxAttempts" INTEGER NOT NULL DEFAULT 3,
|
||||
"status" "RenderJobStatus" NOT NULL DEFAULT 'QUEUED',
|
||||
"priority" INTEGER NOT NULL DEFAULT 50,
|
||||
"manifest" JSONB NOT NULL,
|
||||
"machineId" TEXT,
|
||||
"claimedAt" TIMESTAMP(3),
|
||||
"leaseExpiresAt" TIMESTAMP(3),
|
||||
"startedAt" TIMESTAMP(3),
|
||||
"finishedAt" TIMESTAMP(3),
|
||||
"progress" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"currentFrame" INTEGER,
|
||||
"totalFrames" INTEGER,
|
||||
"etaSeconds" INTEGER,
|
||||
"exitCode" INTEGER,
|
||||
"errorMessage" TEXT,
|
||||
"logTail" TEXT,
|
||||
"logFileKey" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "render_jobs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "export_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"exportId" TEXT NOT NULL,
|
||||
"fromStatus" TEXT,
|
||||
"toStatus" TEXT NOT NULL,
|
||||
"actorType" TEXT NOT NULL,
|
||||
"actorId" TEXT,
|
||||
"note" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "export_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "machines" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"hostname" TEXT NOT NULL,
|
||||
"status" "MachineStatus" NOT NULL DEFAULT 'OFFLINE',
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"lastSeenAt" TIMESTAMP(3),
|
||||
"workerVersion" TEXT,
|
||||
"aeVersion" TEXT,
|
||||
"capabilities" JSONB,
|
||||
"availability" JSONB,
|
||||
"renderNowUntil" TIMESTAMP(3),
|
||||
"apiKeyHash" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "machines_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "worker_heartbeats" (
|
||||
"id" TEXT NOT NULL,
|
||||
"machineId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"cpuPercent" DOUBLE PRECISION,
|
||||
"memPercent" DOUBLE PRECISION,
|
||||
"diskFreeGb" DOUBLE PRECISION,
|
||||
"currentJobId" TEXT,
|
||||
|
||||
CONSTRAINT "worker_heartbeats_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "exports_status_idx" ON "exports"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "exports_projectId_status_idx" ON "exports"("projectId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "exports_shotId_versionNumber_key" ON "exports"("shotId", "versionNumber");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "render_jobs_status_priority_createdAt_idx" ON "render_jobs"("status", "priority", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "render_jobs_machineId_status_idx" ON "render_jobs"("machineId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "export_events_exportId_createdAt_idx" ON "export_events"("exportId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "machines_name_key" ON "machines"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "worker_heartbeats_machineId_createdAt_idx" ON "worker_heartbeats"("machineId", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "exports" ADD CONSTRAINT "exports_shotId_fkey" FOREIGN KEY ("shotId") REFERENCES "shots"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "render_jobs" ADD CONSTRAINT "render_jobs_exportId_fkey" FOREIGN KEY ("exportId") REFERENCES "exports"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "render_jobs" ADD CONSTRAINT "render_jobs_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES "machines"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "export_events" ADD CONSTRAINT "export_events_exportId_fkey" FOREIGN KEY ("exportId") REFERENCES "exports"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "worker_heartbeats" ADD CONSTRAINT "worker_heartbeats_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES "machines"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Per-submission slate fields (RenderPipeline2 §9): authored per Export rather
|
||||
-- than per Shot, because they change from one delivery to the next. New exports
|
||||
-- default to the previous export's values.
|
||||
ALTER TABLE "exports" ADD COLUMN "vfxScope" TEXT;
|
||||
ALTER TABLE "exports" ADD COLUMN "submissionNote" TEXT;
|
||||
@@ -113,6 +113,72 @@ enum ProjectType {
|
||||
EPISODIC
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// RENDER PIPELINE ENUMS (RenderPipeline2 §5.1)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
enum ExportStatus {
|
||||
QUEUED
|
||||
RENDERING
|
||||
RENDER_FAILED
|
||||
VALIDATING
|
||||
VALIDATION_FAILED
|
||||
GENERATING_PREVIEW
|
||||
PREVIEW_FAILED
|
||||
READY_FOR_QC
|
||||
QC_FAILED
|
||||
READY_FOR_DELIVERY
|
||||
PACKAGED
|
||||
DELIVERED
|
||||
SUPERSEDED
|
||||
ARCHIVED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum RenderJobStatus {
|
||||
QUEUED
|
||||
CLAIMED
|
||||
RUNNING
|
||||
COMPLETED
|
||||
FAILED
|
||||
CANCELLED
|
||||
EXPIRED
|
||||
}
|
||||
|
||||
enum RenderJobType {
|
||||
AE_RENDER
|
||||
PREVIEW_ONLY
|
||||
DELIVERY_BUILD
|
||||
}
|
||||
|
||||
enum ValidationStatus {
|
||||
PASS
|
||||
FAIL
|
||||
WARN
|
||||
SKIPPED
|
||||
}
|
||||
|
||||
enum QCResult {
|
||||
PASS
|
||||
FAIL
|
||||
}
|
||||
|
||||
enum MachineStatus {
|
||||
ONLINE
|
||||
OFFLINE
|
||||
DISABLED
|
||||
}
|
||||
|
||||
enum DeliveryStatus {
|
||||
DRAFT
|
||||
QUEUED
|
||||
BUILDING
|
||||
READY
|
||||
DELIVERED
|
||||
FAILED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum TakeQuality {
|
||||
HERO
|
||||
GOOD
|
||||
@@ -292,6 +358,10 @@ model Project {
|
||||
episodeDueDates EpisodeDueDate[]
|
||||
shootDays ShootDay[]
|
||||
|
||||
/// Per-production delivery naming/layout templates (RenderPipeline2 §11.2);
|
||||
/// falls back to SystemConfig defaults when null
|
||||
deliveryConfig Json?
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
|
||||
@@ -359,6 +429,7 @@ model Shot {
|
||||
footagePlates FootagePlate[]
|
||||
references ShotReference[]
|
||||
loggedTakes Take[] @relation("TakeToShot")
|
||||
exports Export[]
|
||||
|
||||
@@unique([projectId, shotCode])
|
||||
@@map("shots")
|
||||
@@ -752,6 +823,162 @@ model TakeAttachment {
|
||||
@@map("take_attachments")
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// RENDER PIPELINE MODELS (RenderPipeline2 §5)
|
||||
// Phases 1 + 2: Export, RenderJob, ExportEvent, Machine, WorkerHeartbeat.
|
||||
// ValidationResult / QCReview / DeliveryPackage / DeliveryItem arrive in
|
||||
// later phases as additive migrations.
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// One row per "Queue Export" click — carries the §4 state machine.
|
||||
model Export {
|
||||
id String @id @default(cuid())
|
||||
shotId String
|
||||
shot Shot @relation(fields: [shotId], references: [id])
|
||||
projectId String
|
||||
taskId String? // comp task the preview Version attaches to
|
||||
versionId String? // preview Version created at READY_FOR_QC (Phase 4)
|
||||
versionNumber Int // 4
|
||||
versionString String // "v004"
|
||||
status ExportStatus @default(QUEUED)
|
||||
statusChangedAt DateTime @default(now())
|
||||
|
||||
// Manifest (denormalised for querying; full manifest JSON on RenderJob)
|
||||
aepPath String
|
||||
compName String
|
||||
rendererType String // "aerender"
|
||||
outputDir String // render root for this export
|
||||
outputPattern String // "UNG_106_010_020_cmp_TT_v004.[####].exr"
|
||||
frameStart Int
|
||||
frameEnd Int
|
||||
fps Float
|
||||
width Int
|
||||
height Int
|
||||
colorspace String? // expected, e.g. "ACES - ACEScg"
|
||||
|
||||
// Artifacts
|
||||
deliveryMovPath String? // slate/burn-in delivery MOV on the SAN (§9)
|
||||
previewMovKey String? // web H.264 transcode in object storage
|
||||
thumbnailKey String?
|
||||
metadataKey String? // metadata JSON in object storage
|
||||
exrFileCount Int?
|
||||
exrTotalBytes BigInt?
|
||||
checksum String? // sequence-level digest (xxHash of per-file hashes)
|
||||
|
||||
// Slate fields authored per submission (not per shot): they change from one
|
||||
// delivery to the next, so each Export carries its own and new exports
|
||||
// default to the previous export's values.
|
||||
vfxScope String? @db.Text
|
||||
submissionNote String? @db.Text
|
||||
|
||||
submittedById String? // resolved User, else null
|
||||
submittedByName String? // free text from panel config as fallback
|
||||
supersededById String? // newer Export that replaced this one
|
||||
|
||||
renderJobs RenderJob[]
|
||||
events ExportEvent[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([shotId, versionNumber])
|
||||
@@index([status])
|
||||
@@index([projectId, status])
|
||||
@@map("exports")
|
||||
}
|
||||
|
||||
/// One execution attempt of work by a worker. Also used (type DELIVERY_BUILD)
|
||||
/// for delivery package builds — one queue/claim/lease/heartbeat mechanism.
|
||||
model RenderJob {
|
||||
id String @id @default(cuid())
|
||||
type RenderJobType @default(AE_RENDER)
|
||||
exportId String? // set for AE_RENDER / PREVIEW_ONLY
|
||||
export Export? @relation(fields: [exportId], references: [id])
|
||||
deliveryId String? // set for DELIVERY_BUILD (Phase 5)
|
||||
attempt Int @default(1)
|
||||
maxAttempts Int @default(3)
|
||||
status RenderJobStatus @default(QUEUED)
|
||||
priority Int @default(50) // lower = sooner; <= 20 is urgent
|
||||
manifest Json // full Render Manifest snapshot (§6.2)
|
||||
|
||||
machineId String?
|
||||
machine Machine? @relation(fields: [machineId], references: [id])
|
||||
claimedAt DateTime?
|
||||
leaseExpiresAt DateTime? // claim + leaseSeconds; renewed by progress reports
|
||||
startedAt DateTime?
|
||||
finishedAt DateTime?
|
||||
|
||||
progress Float @default(0) // 0..1
|
||||
currentFrame Int?
|
||||
totalFrames Int?
|
||||
etaSeconds Int?
|
||||
exitCode Int?
|
||||
errorMessage String?
|
||||
logTail String? @db.Text // last ~200 lines of aerender output
|
||||
logFileKey String? // full log uploaded to object storage on finish/fail
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status, priority, createdAt])
|
||||
@@index([machineId, status])
|
||||
@@map("render_jobs")
|
||||
}
|
||||
|
||||
/// Append-only audit log of Export state transitions (§5.9)
|
||||
model ExportEvent {
|
||||
id String @id @default(cuid())
|
||||
exportId String
|
||||
export Export @relation(fields: [exportId], references: [id])
|
||||
fromStatus String?
|
||||
toStatus String
|
||||
actorType String // "WORKER" | "USER" | "SYSTEM"
|
||||
actorId String? // machineId or userId
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([exportId, createdAt])
|
||||
@@map("export_events")
|
||||
}
|
||||
|
||||
/// A render-capable machine (artist workstation or future render node) (§5.6)
|
||||
model Machine {
|
||||
id String @id @default(cuid())
|
||||
name String @unique // "RENDER-01"
|
||||
hostname String
|
||||
status MachineStatus @default(OFFLINE)
|
||||
enabled Boolean @default(true) // admin kill-switch: disabled machines cannot claim
|
||||
lastSeenAt DateTime?
|
||||
workerVersion String?
|
||||
aeVersion String? // "2026 (24.x)"
|
||||
capabilities Json? // { maxConcurrentJobs: 1, tools: {...} }
|
||||
availability Json? // §7.10: { mode, windows, allowUrgentAnytime } — null = ALWAYS
|
||||
renderNowUntil DateTime? // manual "Render Now" override; claims allowed until this time
|
||||
apiKeyHash String? // optional per-machine key (dormant; §15 — shared key for now)
|
||||
|
||||
renderJobs RenderJob[]
|
||||
heartbeats WorkerHeartbeat[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("machines")
|
||||
}
|
||||
|
||||
model WorkerHeartbeat {
|
||||
id String @id @default(cuid())
|
||||
machineId String
|
||||
machine Machine @relation(fields: [machineId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
cpuPercent Float?
|
||||
memPercent Float?
|
||||
diskFreeGb Float?
|
||||
currentJobId String?
|
||||
|
||||
@@index([machineId, createdAt])
|
||||
@@map("worker_heartbeats")
|
||||
}
|
||||
|
||||
model SketchTemplate {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
|
||||
Reference in New Issue
Block a user