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; }
}
/// Server-supplied tuning (E6) — fleet-tunable without touching installs.
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;
///
/// 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.
///
public bool ValidationEnabled { get; set; } = true;
public int ValidationSampleEvery { get; set; } = 25;
/// Head handles the plates carry; only used for the API timecode fallback (§8.1a).
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 string? DeliveryId { get; set; }
/// Set for jobs with no shot behind them; shown instead of a shot code.
public string? Label { get; set; }
public DateTimeOffset? LeaseExpiresAt { get; set; }
/// Manifest shape depends on the job type — deserialize per stage.
public JsonElement Manifest { get; set; }
public RenderManifest AsRenderManifest() =>
Manifest.Deserialize(WorkerOptions.JsonOpts)
?? throw new InvalidOperationException("Job manifest is not a render manifest");
public PreviewManifest AsPreviewManifest() =>
Manifest.Deserialize(WorkerOptions.JsonOpts)
?? throw new InvalidOperationException("Job manifest is not a preview manifest");
public DeliveryManifest AsDeliveryManifest() =>
Manifest.Deserialize(WorkerOptions.JsonOpts)
?? throw new InvalidOperationException("Job manifest is not a delivery manifest");
public BlenderManifest AsBlenderManifest() =>
Manifest.Deserialize(WorkerOptions.JsonOpts)
?? throw new InvalidOperationException("Job manifest is not a Blender manifest");
}
///
/// A standalone Blender render. Overrides are all optional: an unset value
/// leaves whatever the .blend was saved with, which is the point — the file is
/// expected to be ready to render.
///
public sealed class BlenderManifest
{
public string Stage { get; set; } = "BLENDER";
public string BlendFile { get; set; } = "";
public string? Scene { get; set; }
public int FrameStart { get; set; }
public int FrameEnd { get; set; }
public string? OutputPath { get; set; }
public string? Engine { get; set; }
public string? Device { get; set; }
public int? Samples { get; set; }
public int? ResolutionPercent { get; set; }
public string? BlenderVersion { get; set; }
public int TotalFrames => FrameEnd - FrameStart + 1;
}
/// DELIVERY_BUILD manifest (server-built, RenderPipeline2 §11).
public sealed class DeliveryManifest
{
public string Stage { get; set; } = "DELIVERY";
///
/// "EXR_DELIVERY" builds the per-shot folders of EXRs + MOVs; "MOV_REVIEW"
/// builds a flat folder of preview MOVs for a formal review (§11.6). One
/// job type and one claim path either way — only what lands on disk differs.
///
public string Kind { get; set; } = "EXR_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";
/// "auto" verifies copies by hash and links by identity; "always-hash" reads everything back.
public string VerifyMode { get; set; } = "auto";
public string ChecksumAlgo { get; set; } = "xxh64";
public List ReportFormats { get; set; } = new();
public List 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; } = "";
///
/// Server-computed: "{clientCode}_{versionString}" when the shot has a
/// client naming code, else the studio's own "{shotCode}_cmp_TT_{versionString}".
/// The delivered EXR sequence and MOV are named from this, not the
/// internal render filename they're copied from.
///
public string DeliveredBaseName { get; set; } = "";
/// When this export's validation ran — lets a linked package detect a source changed since QC.
public DateTimeOffset? ValidatedAt { get; set; }
///
/// True on a MOV_REVIEW item: there is no EXR sequence, so SourceDir,
/// OutputPattern, FileCount and Checksum are empty and DeliveryMovPath is
/// the whole deliverable.
///
public bool IsMovOnly => string.IsNullOrWhiteSpace(SourceDir);
}
/// PREVIEW_ONLY manifest (server-built, RenderPipeline2 §9).
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; } = "";
/// Production name, burned into the slate and the overlay.
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; }
}
/// Result JSON written by vfxr_build_preview.jsx.
public sealed class PreviewBuildResult
{
public bool Ok { get; set; }
public string? Error { get; set; }
public List 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; }
///
/// Seconds of slate ahead of picture in the preview comp (the SHOT layer's
/// in-point). Null on builds predating this field, or when the template has
/// no SHOT layer — the thumbnail then falls back to scanning from frame 0.
///
public double? PictureStartSeconds { get; set; }
}
/// The Render Manifest (§6.2) — the fields the render and validation stages need; the rest is preserved raw.
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; }
/// Comp timeline range handed to aerender (-s/-e).
public int FrameStart { get; set; }
public int FrameEnd { get; set; }
///
/// 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.
///
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? Extra { get; set; }
public int TotalFrames => FrameEnd - FrameStart + 1;
}
/// Per-project expectations the validation engine checks against (§8.1).
public sealed class ManifestExpected
{
public string? Colorspace { get; set; }
public string? BitDepth { get; set; }
public string? ExrCompression { get; set; }
public bool? Alpha { get; set; }
/// The API's editorial in-point — the rank 3 timecode fallback only (§8.1a).
public string? TimecodeStart { get; set; }
}
/// Plate provenance resolved by the panel from the comp's footage layers (§6.2).
public sealed class ManifestSource
{
public PlateRef? TimingPlate { get; set; }
public List 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 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 RenderJobs { get; set; } = new();
}
public sealed class RenderJobDetail
{
public string Id { get; set; } = "";
public string Status { get; set; } = "";
}
/// Durable lifecycle report persisted to the spool directory (§7.6).
public sealed class SpooledReport
{
public string Kind { get; set; } = ""; // "fail" | "complete-render"
public string JobId { get; set; } = "";
public Dictionary Payload { get; set; } = new();
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
}
/// current-job.json — crash recovery state (§7.5).
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; }
}