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:
@@ -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