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,189 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace VFXReviewWorker;
|
||||
|
||||
// Wire types for the /api/ext pipeline endpoints (RenderPipeline2 §6).
|
||||
|
||||
public sealed class RegisterResponse
|
||||
{
|
||||
public MachineInfo Machine { get; set; } = new();
|
||||
public ServerConfig Config { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class MachineInfo
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Server-supplied tuning (E6) — fleet-tunable without touching installs.</summary>
|
||||
public sealed class ServerConfig
|
||||
{
|
||||
public int PollSeconds { get; set; } = 10;
|
||||
public int HeartbeatSeconds { get; set; } = 30;
|
||||
public int LeaseSeconds { get; set; } = 300;
|
||||
public int MaxAttempts { get; set; } = 3;
|
||||
public int StallTimeoutSeconds { get; set; } = 600;
|
||||
}
|
||||
|
||||
public sealed class ClaimResponse
|
||||
{
|
||||
public ClaimedJob Job { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ClaimedJob
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Type { get; set; } = "AE_RENDER";
|
||||
public int Attempt { get; set; }
|
||||
public int MaxAttempts { get; set; }
|
||||
public string? ExportId { get; set; }
|
||||
public DateTimeOffset? LeaseExpiresAt { get; set; }
|
||||
|
||||
/// <summary>Manifest shape depends on the job type — deserialize per stage.</summary>
|
||||
public JsonElement Manifest { get; set; }
|
||||
|
||||
public RenderManifest AsRenderManifest() =>
|
||||
Manifest.Deserialize<RenderManifest>(WorkerOptions.JsonOpts)
|
||||
?? throw new InvalidOperationException("Job manifest is not a render manifest");
|
||||
|
||||
public PreviewManifest AsPreviewManifest() =>
|
||||
Manifest.Deserialize<PreviewManifest>(WorkerOptions.JsonOpts)
|
||||
?? throw new InvalidOperationException("Job manifest is not a preview manifest");
|
||||
}
|
||||
|
||||
/// <summary>PREVIEW_ONLY manifest (server-built, RenderPipeline2 §9).</summary>
|
||||
public sealed class PreviewManifest
|
||||
{
|
||||
public string Stage { get; set; } = "PREVIEW";
|
||||
public string ExportId { get; set; } = "";
|
||||
public string ShotCode { get; set; } = "";
|
||||
public string VersionString { get; set; } = "";
|
||||
public string OutputDir { get; set; } = "";
|
||||
public string OutputPattern { get; set; } = "";
|
||||
public int FrameStart { get; set; }
|
||||
public int FrameEnd { get; set; }
|
||||
public double Fps { get; set; } = 24;
|
||||
public string TemplateAep { get; set; } = "";
|
||||
public string TemplateComp { get; set; } = "";
|
||||
public string? OverlayComp { get; set; }
|
||||
public string? LutComp { get; set; }
|
||||
public string MovTemplate { get; set; } = "";
|
||||
public string Mp4Template { get; set; } = "";
|
||||
public string MovOutput { get; set; } = "";
|
||||
public string Mp4Output { get; set; } = "";
|
||||
public string? SlateScopeProp { get; set; }
|
||||
public string? SlateSubmissionProp { get; set; }
|
||||
public SlateInfo Slate { get; set; } = new();
|
||||
|
||||
public int TotalFrames => FrameEnd - FrameStart + 1;
|
||||
}
|
||||
|
||||
public sealed class SlateInfo
|
||||
{
|
||||
public string VersionName { get; set; } = "";
|
||||
public string Date { get; set; } = "";
|
||||
public string? Description { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public string ShotCode { get; set; } = "";
|
||||
public string? Episode { get; set; }
|
||||
public string? Scene { get; set; }
|
||||
public string? VfxScope { get; set; }
|
||||
public string? SubmissionNote { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Result JSON written by vfxr_build_preview.jsx.</summary>
|
||||
public sealed class PreviewBuildResult
|
||||
{
|
||||
public bool Ok { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public List<string> Warnings { get; set; } = new();
|
||||
public string? TempAep { get; set; }
|
||||
public string? PreviewComp { get; set; }
|
||||
public int? FrameCount { get; set; }
|
||||
public int? Width { get; set; }
|
||||
public int? Height { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>The Render Manifest (§6.2) — only the fields the Phase 2 render stage needs; the rest is preserved raw.</summary>
|
||||
public sealed class RenderManifest
|
||||
{
|
||||
public string AepPath { get; set; } = "";
|
||||
public string CompName { get; set; } = "";
|
||||
public string OutputDir { get; set; } = "";
|
||||
public string OutputPattern { get; set; } = "";
|
||||
public string? OutputModuleTemplate { get; set; }
|
||||
public string? RenderSettingsTemplate { get; set; }
|
||||
public int FrameStart { get; set; }
|
||||
public int FrameEnd { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, JsonElement>? Extra { get; set; }
|
||||
|
||||
public int TotalFrames => FrameEnd - FrameStart + 1;
|
||||
}
|
||||
|
||||
public sealed class ProgressResponse
|
||||
{
|
||||
public bool Ok { get; set; }
|
||||
public bool CancelRequested { get; set; }
|
||||
public DateTimeOffset? LeaseExpiresAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class HeartbeatResponse
|
||||
{
|
||||
public bool Ok { get; set; }
|
||||
public List<WorkerCommand> Commands { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class WorkerCommand
|
||||
{
|
||||
public string Type { get; set; } = "";
|
||||
public string? JobId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PresignResponse
|
||||
{
|
||||
public string PresignedUrl { get; set; } = "";
|
||||
public string Key { get; set; } = "";
|
||||
public string Url { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class ExportDetailResponse
|
||||
{
|
||||
public ExportDetail Export { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ExportDetail
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Status { get; set; } = "";
|
||||
public List<RenderJobDetail> RenderJobs { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class RenderJobDetail
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string Status { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>Durable lifecycle report persisted to the spool directory (§7.6).</summary>
|
||||
public sealed class SpooledReport
|
||||
{
|
||||
public string Kind { get; set; } = ""; // "fail" | "complete-render"
|
||||
public string JobId { get; set; } = "";
|
||||
public Dictionary<string, object?> Payload { get; set; } = new();
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>current-job.json — crash recovery state (§7.5).</summary>
|
||||
public sealed class CurrentJobState
|
||||
{
|
||||
public string JobId { get; set; } = "";
|
||||
public string? ExportId { get; set; } = "";
|
||||
public string MachineId { get; set; } = "";
|
||||
public string OutputDirLocal { get; set; } = "";
|
||||
public DateTimeOffset ClaimedAt { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user