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,79 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace VFXReviewWorker;
|
||||
|
||||
/// <summary>
|
||||
/// Machine-local config (RenderPipeline2 §7.8) — deliberately minimal;
|
||||
/// everything tunable lives in server SystemConfig and arrives via E6.
|
||||
/// Default path: C:\ProgramData\VFXReviewWorker\config.json
|
||||
/// </summary>
|
||||
public sealed class WorkerOptions
|
||||
{
|
||||
public string ServerUrl { get; set; } = "";
|
||||
public string ApiKey { get; set; } = "";
|
||||
public string MachineName { get; set; } = Environment.MachineName;
|
||||
public string AerenderPath { get; set; } = @"C:\Program Files\Adobe\Adobe After Effects 2026\Support Files\aerender.exe";
|
||||
/// <summary>AfterFX.com — the console AE used for the headless preview build. Defaults beside aerender.</summary>
|
||||
public string? AfterFxPath { get; set; }
|
||||
/// <summary>vfxr_build_preview.jsx — defaults to scripts\ beside the worker exe.</summary>
|
||||
public string? PreviewScriptPath { get; set; }
|
||||
public string? FfmpegPath { get; set; }
|
||||
public string? OiiotoolPath { get; set; }
|
||||
public List<PathMapping> PathMappings { get; set; } = new();
|
||||
public string WorkerVersion { get; set; } = "1.0.0";
|
||||
public string? AeVersion { get; set; }
|
||||
|
||||
public static string DataDir =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "VFXReviewWorker");
|
||||
|
||||
public static string DefaultConfigPath => Path.Combine(DataDir, "config.json");
|
||||
public static string LogsDir => Path.Combine(DataDir, "logs");
|
||||
public static string SpoolDir => Path.Combine(DataDir, "spool");
|
||||
public static string ScratchDir => Path.Combine(DataDir, "scratch");
|
||||
public static string StateFilePath => Path.Combine(DataDir, "current-job.json");
|
||||
|
||||
/// <summary>AfterFX.com sits beside aerender.exe in a standard AE install.</summary>
|
||||
public string ResolveAfterFxPath()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(AfterFxPath)) return AfterFxPath;
|
||||
var dir = Path.GetDirectoryName(AerenderPath);
|
||||
return dir is null ? "AfterFX.com" : Path.Combine(dir, "AfterFX.com");
|
||||
}
|
||||
|
||||
public string ResolvePreviewScriptPath()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(PreviewScriptPath)) return PreviewScriptPath;
|
||||
return Path.Combine(AppContext.BaseDirectory, "scripts", "vfxr_build_preview.jsx");
|
||||
}
|
||||
|
||||
public static WorkerOptions Load(string? path = null)
|
||||
{
|
||||
path ??= DefaultConfigPath;
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
$"Worker config not found at {path}. Create it with serverUrl, apiKey and machineName (see RenderWorker/README.md).");
|
||||
}
|
||||
var json = File.ReadAllText(path);
|
||||
var opts = JsonSerializer.Deserialize<WorkerOptions>(json, JsonOpts)
|
||||
?? throw new InvalidOperationException($"Could not parse {path}");
|
||||
if (string.IsNullOrWhiteSpace(opts.ServerUrl)) throw new InvalidOperationException("config.json: serverUrl is required");
|
||||
if (string.IsNullOrWhiteSpace(opts.ApiKey)) throw new InvalidOperationException("config.json: apiKey is required");
|
||||
if (string.IsNullOrWhiteSpace(opts.MachineName)) opts.MachineName = Environment.MachineName;
|
||||
return opts;
|
||||
}
|
||||
|
||||
public static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class PathMapping
|
||||
{
|
||||
public string From { get; set; } = "";
|
||||
public string To { get; set; } = "";
|
||||
}
|
||||
Reference in New Issue
Block a user