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,138 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Headless AE spike / debug tool for the preview build stage.
|
||||
|
||||
.DESCRIPTION
|
||||
Runs vfxr_build_preview.jsx exactly the way the RenderWorker does, but
|
||||
standalone, so the risky part (AfterFX.com -noui -r under a service
|
||||
account) can be proven before wiring the whole pipeline together.
|
||||
|
||||
It does NOT render - it builds the preview comp, queues the MOV + MP4
|
||||
output modules and saves a temp AEP, then prints the build result.
|
||||
|
||||
IMPORTANT: close After Effects first. `AfterFX.com -r` hands the script to
|
||||
an already-running instance, which would open the template in your session
|
||||
and then quit it.
|
||||
|
||||
.EXAMPLE
|
||||
.\test-preview-build.ps1 -ExrDir "V:\_EXPORTS\UNG\renders\UNG_S1\111\UNG_111_001_030\v002" `
|
||||
-ShotCode "UNG_111_001_030" -Version "v002"
|
||||
|
||||
.EXAMPLE
|
||||
# then render what it built, exactly as the worker would:
|
||||
& "C:\Program Files\Adobe\Adobe After Effects 2026\Support Files\aerender.exe" -project "$env:ProgramData\VFXReviewWorker\scratch\spike_preview.aep"
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ExrDir,
|
||||
[Parameter(Mandatory = $true)][string]$ShotCode,
|
||||
[string]$Version = "v001",
|
||||
[string]$TemplateAep = "X:/shared_projects_2026/UNGO_VFX/production/working_files/UNG_VFXOVERLAY_SLATE_TEMPLATE.aep",
|
||||
[string]$TemplateComp = "UNG_EXPORT_TEMPLATE",
|
||||
[string]$OverlayComp = "UNG_VFX_OVERLAY",
|
||||
[string]$LutComp = "_SHOW LUT",
|
||||
[string]$MovTemplate = "4444 Tri",
|
||||
[string]$Mp4Template = "REVIEW_PREVIEW",
|
||||
[string]$AfterFx = "C:\Program Files\Adobe\Adobe After Effects 2026\Support Files\AfterFX.com",
|
||||
[int]$FrameStart = 1001,
|
||||
[double]$Fps = 24,
|
||||
[int]$TimeoutSeconds = 300
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$running = Get-Process AfterFX -ErrorAction SilentlyContinue
|
||||
if ($running) {
|
||||
Write-Warning "After Effects is running (PID $($running.Id -join ', '))."
|
||||
Write-Warning "AfterFX.com -r would target that instance and quit it. Close AE first."
|
||||
return
|
||||
}
|
||||
|
||||
$scratch = Join-Path $env:ProgramData "VFXReviewWorker\scratch"
|
||||
if (-not (Test-Path $scratch)) { New-Item -ItemType Directory -Force -Path $scratch | Out-Null }
|
||||
|
||||
$scriptPath = Join-Path $PSScriptRoot "vfxr_build_preview.jsx"
|
||||
if (-not (Test-Path $scriptPath)) { throw "Build script not found: $scriptPath" }
|
||||
if (-not (Test-Path $AfterFx)) { throw "AfterFX.com not found: $AfterFx" }
|
||||
if (-not (Test-Path $ExrDir)) { throw "EXR folder not found: $ExrDir" }
|
||||
|
||||
$contextPath = Join-Path $scratch "spike_context.json"
|
||||
$resultPath = Join-Path $scratch "spike_result.json"
|
||||
$wrapperPath = Join-Path $scratch "spike_run.jsx"
|
||||
$tempAep = Join-Path $scratch "spike_preview.aep"
|
||||
$base = "${ShotCode}_cmp_TT_${Version}"
|
||||
|
||||
Remove-Item $resultPath -ErrorAction SilentlyContinue
|
||||
|
||||
$context = [ordered]@{
|
||||
jobId = "spike"
|
||||
exportId = "spike"
|
||||
shotCode = $ShotCode
|
||||
versionString = $Version
|
||||
outputDirLocal = $ExrDir
|
||||
templateAep = $TemplateAep
|
||||
templateComp = $TemplateComp
|
||||
overlayComp = $OverlayComp
|
||||
lutComp = $LutComp
|
||||
movTemplate = $MovTemplate
|
||||
mp4Template = $Mp4Template
|
||||
movOutput = (Join-Path $ExrDir "$base.mov")
|
||||
mp4Output = (Join-Path $ExrDir "$base.mp4")
|
||||
tempAep = $tempAep
|
||||
resultPath = $resultPath
|
||||
frameStart = $FrameStart
|
||||
fps = $Fps
|
||||
slate = [ordered]@{
|
||||
versionName = $base
|
||||
date = (Get-Date -Format "yyyy/MM/dd")
|
||||
description = "Spike test"
|
||||
notes = ""
|
||||
shotCode = $ShotCode
|
||||
episode = ""
|
||||
scene = ""
|
||||
}
|
||||
}
|
||||
$context | ConvertTo-Json -Depth 6 | Out-File -FilePath $contextPath -Encoding utf8
|
||||
|
||||
$ctxJs = ($contextPath -replace '\\', '/') | ConvertTo-Json
|
||||
$scriptJs = ($scriptPath -replace '\\', '/') | ConvertTo-Json
|
||||
$wrapper = @(
|
||||
"// generated by test-preview-build.ps1",
|
||||
("var VFXR_CONTEXT_PATH = {0};" -f $ctxJs),
|
||||
('$.evalFile({0});' -f $scriptJs)
|
||||
) -join "`r`n"
|
||||
$wrapper | Out-File -FilePath $wrapperPath -Encoding utf8
|
||||
|
||||
Write-Host "Launching headless AE build..." -ForegroundColor Cyan
|
||||
$proc = Start-Process -FilePath $AfterFx -ArgumentList @("-noui", "-r", $wrapperPath) -PassThru -NoNewWindow
|
||||
if (-not $proc.WaitForExit($TimeoutSeconds * 1000)) {
|
||||
Write-Warning "Timed out after $TimeoutSeconds s - killing AE."
|
||||
try { $proc.Kill($true) } catch {}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $resultPath)) {
|
||||
Write-Host "FAILED: no result file produced." -ForegroundColor Red
|
||||
Write-Host "AE could not run the script headlessly under this account. See RenderWorker/README.md fallbacks." -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
|
||||
$result = Get-Content $resultPath -Raw | ConvertFrom-Json
|
||||
if ($result.ok) {
|
||||
Write-Host "BUILD OK" -ForegroundColor Green
|
||||
Write-Host " temp AEP : $($result.tempAep)"
|
||||
Write-Host " preview comp: $($result.previewComp)"
|
||||
Write-Host (" frames : {0} ({1} x {2})" -f $result.frameCount, $result.width, $result.height)
|
||||
Write-Host (" queued : {0} output(s)" -f @($result.queued).Count)
|
||||
foreach ($q in @($result.queued)) {
|
||||
Write-Host (" - {0}: {1} -> {2}" -f $q.label, $q.template, $q.output)
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "Render it with:" -ForegroundColor Cyan
|
||||
Write-Host (' "{0}\aerender.exe" -project "{1}"' -f (Split-Path $AfterFx), $result.tempAep)
|
||||
} else {
|
||||
Write-Host "BUILD FAILED: $($result.error)" -ForegroundColor Red
|
||||
}
|
||||
if ($result.warnings -and $result.warnings.Count -gt 0) {
|
||||
Write-Host "Warnings:" -ForegroundColor Yellow
|
||||
$result.warnings | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
|
||||
}
|
||||
Reference in New Issue
Block a user