af810258e6
Deploy / deploy (push) Successful in 6m25s
A 25-shot episode took about an hour to package when the linking itself is instant. All of it was verification: every delivered file was read back and hashed, and at ~100 MB/s over SMB one 2.8 GB shot costs 30 s. Re-hashing a hard link cannot disagree with validation. The entry is a second name for the same data, so it re-reads the exact bytes that were already hashed — half an hour per episode to prove something that cannot be false. Verification is now proportional to what was actually done: copies are read back in full, links carry over the validation-time digest. Linking has one gap hashing covered incidentally: a source changed after it passed QC, which a link would carry faithfully. Source timestamps are now checked against the export's validation time, which costs a stat per file rather than a full read. "verifyMode": "always-hash" restores unconditional read-back for a production that requires it. Worker 1.3.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
285 lines
10 KiB
C#
285 lines
10 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";
|
|
/// <summary>"auto" verifies copies by hash and links by identity; "always-hash" reads everything back.</summary>
|
|
public string VerifyMode { 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>When this export's validation ran — lets a linked package detect a source changed since QC.</summary>
|
|
public DateTimeOffset? ValidatedAt { 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; } = "";
|
|
/// <summary>Production name, burned into the slate and the overlay.</summary>
|
|
public string? Show { 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; }
|
|
/// <summary>Comp timeline range handed to aerender (-s/-e).</summary>
|
|
public int FrameStart { get; set; }
|
|
public int FrameEnd { get; set; }
|
|
/// <summary>
|
|
/// The number the *first output file* carries — the output module's
|
|
/// starting number (house convention 1001), which is deliberately not the
|
|
/// comp's frame range: a comp anchored to the plate's timecode starts in
|
|
/// the millions. Null on manifests predating this field.
|
|
/// </summary>
|
|
public int? OutputFrameStart { 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; }
|
|
}
|