feb1c7b3e9
Completes RenderPipeline2 phases 3-5. Additive throughout: with validation.enabled=false the pipeline behaves exactly as before. Validation (§8) runs on the worker straight after a render, while it still holds the job and the files are local. Everything is read from the EXR header — one 64 KB read per sampled frame, no pixel decode — so the engine needs no oiiotool. The render job deliberately stays open through the stage, so a worker that dies there is reaped and retried instead of stranding the export in VALIDATING. Timecode is the primary check and is anchored on the plate, never the API: measured on the live show, Shot.timecodeStart is the editorial in-point and sits one handle-length (8f) ahead of the plate, so checking against it would pass the defective exports and fail the correct ones. Reference precedence is manifest timing plate -> plate found on disk -> API value, and the API rank is WARN-only. The panel now resolves plate provenance by walking the comp's own footage layers, so the worker never guesses which element carries the comp's timing. Verified against real studio renders via the new --validate mode: it catches both defect classes seen in shipped work — the +8 handle offset and frame-derived timecode (00:00:41:17 = frame 1001 at 24fps). QC (§10) is one service behind two front doors, API-key for the panel and session for the web. PASS means ready to upload and nothing else; FAIL sets the comp task to CHANGES. The panel gains a QC section that imports the real EXR sequence at full res rather than the H.264. Delivery (§11) rides the existing queue as a DELIVERY_BUILD job: hard links with a copy fallback, verification by re-hashing delivered bytes against the validation-time digest, manifest.json and the CSV report. fsutil hardlink list is unavailable over SMB, so link identity is never queried — reading the bytes back covers links and copies alike. Also fixes ApiClient.ProgressAsync throwing on transport failure, which would have aborted work in flight during a brief server outage; progress reporting is best-effort by contract. Removes the stale RenderWorker/ rule from the root .gitignore. The worker's source is tracked like the rest of the repo and its own .gitignore already covers bin/, obj/ and publish/; the rule silently excluded new files while the existing 20 stayed tracked. Not yet applied: migration 20260813000000 was hand-written because the dev database was unavailable — run prisma migrate deploy and verify before relying on it. None of this has run against a live server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
271 lines
9.4 KiB
C#
271 lines
9.4 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Validation stage (§8). The server decides whether it runs, so turning it
|
|
/// off is a config change there rather than a redeploy of every worker.
|
|
/// </summary>
|
|
public bool ValidationEnabled { get; set; } = true;
|
|
public int ValidationSampleEvery { get; set; } = 25;
|
|
/// <summary>Head handles the plates carry; only used for the API timecode fallback (§8.1a).</summary>
|
|
public int ValidationHandleFrames { get; set; } = 8;
|
|
}
|
|
|
|
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");
|
|
|
|
public DeliveryManifest AsDeliveryManifest() =>
|
|
Manifest.Deserialize<DeliveryManifest>(WorkerOptions.JsonOpts)
|
|
?? throw new InvalidOperationException("Job manifest is not a delivery manifest");
|
|
}
|
|
|
|
/// <summary>DELIVERY_BUILD manifest (server-built, RenderPipeline2 §11).</summary>
|
|
public sealed class DeliveryManifest
|
|
{
|
|
public string Stage { get; set; } = "DELIVERY";
|
|
public string PackageId { get; set; } = "";
|
|
public string PackageName { get; set; } = "";
|
|
public int PackageNumber { get; set; }
|
|
public string Episode { get; set; } = "";
|
|
public string PackageDir { get; set; } = "";
|
|
public string ReportFile { get; set; } = "";
|
|
public bool IncludeMovs { get; set; } = true;
|
|
public string CopyMode { get; set; } = "auto";
|
|
public string ChecksumAlgo { get; set; } = "xxh64";
|
|
public List<string> ReportFormats { get; set; } = new();
|
|
public List<DeliveryManifestItem> Items { get; set; } = new();
|
|
}
|
|
|
|
public sealed class DeliveryManifestItem
|
|
{
|
|
public string ItemId { get; set; } = "";
|
|
public string ExportId { get; set; } = "";
|
|
public string ShotCode { get; set; } = "";
|
|
public string VersionString { get; set; } = "";
|
|
public string SourceDir { get; set; } = "";
|
|
public string OutputPattern { get; set; } = "";
|
|
public int FrameStart { get; set; }
|
|
public int FrameEnd { get; set; }
|
|
public int? FileCount { get; set; }
|
|
public string? Checksum { get; set; }
|
|
public string? DeliveryMovPath { get; set; }
|
|
public string ShotDir { get; set; } = "";
|
|
}
|
|
|
|
/// <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) — the fields the render and validation stages need; the rest is preserved raw.</summary>
|
|
public sealed class RenderManifest
|
|
{
|
|
public string ShotCode { get; set; } = "";
|
|
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; }
|
|
public double Fps { get; set; } = 24;
|
|
public int Width { get; set; }
|
|
public int Height { get; set; }
|
|
public ManifestExpected? Expected { get; set; }
|
|
public ManifestSource? Source { get; set; }
|
|
|
|
[JsonExtensionData]
|
|
public Dictionary<string, JsonElement>? Extra { get; set; }
|
|
|
|
public int TotalFrames => FrameEnd - FrameStart + 1;
|
|
}
|
|
|
|
/// <summary>Per-project expectations the validation engine checks against (§8.1).</summary>
|
|
public sealed class ManifestExpected
|
|
{
|
|
public string? Colorspace { get; set; }
|
|
public string? BitDepth { get; set; }
|
|
public string? ExrCompression { get; set; }
|
|
public bool? Alpha { get; set; }
|
|
/// <summary>The API's editorial in-point — the rank 3 timecode fallback only (§8.1a).</summary>
|
|
public string? TimecodeStart { get; set; }
|
|
}
|
|
|
|
/// <summary>Plate provenance resolved by the panel from the comp's footage layers (§6.2).</summary>
|
|
public sealed class ManifestSource
|
|
{
|
|
public PlateRef? TimingPlate { get; set; }
|
|
public List<PlateRef> Plates { get; set; } = new();
|
|
public string? StartSource { get; set; }
|
|
public int? StartFrames { get; set; }
|
|
public int? HandleFrames { get; set; }
|
|
}
|
|
|
|
public sealed class PlateRef
|
|
{
|
|
public string? Element { get; set; }
|
|
public string Path { get; set; } = "";
|
|
public int? FrameStart { get; set; }
|
|
public int? FrameEnd { get; set; }
|
|
}
|
|
|
|
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; }
|
|
}
|