caaa333f02
Deploy / deploy (push) Failing after 19m11s
Two independent features landing together: Generated shot thumbnails — a shot can now carry a thumbnail promoted from a render export (Shot.generatedThumbnailUrl/generatedThumbnailFrom) alongside the existing brief-supplied thumbnailUrl, which always takes display precedence (lib/shot-thumbnail.ts) so a promoted frame can only ever fill a gap, never overwrite the brief. Shows with pipelineConfig.autoShotThumbnail promote automatically on every finished export (lib/render-pipeline/preview.ts); promoteExportThumbnail/ clearGeneratedThumbnail (actions/shots.ts) are the manual equivalent for everyone else. Worker-side seek logic for picking the promoted frame is covered by RenderWorker/VFXReviewWorker.Tests/ThumbnailSeekTests.cs. Bulk shot delete — the shot-status page's selection bar gets a "Bulk Actions" dropdown (alongside the existing Change Due Date) with Delete Shots. bulkDeleteShots() (actions/shots.ts) deletes one at a time rather than in a single statement: exports_shotId_fkey is ON DELETE RESTRICT by design, so a shot with renders or deliveries on record is protected and gets skipped and reported by name rather than aborting the whole selection or being silently destroyed along with its history. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
327 lines
12 KiB
C#
327 lines
12 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 string? DeliveryId { get; set; }
|
|
/// <summary>Set for jobs with no shot behind them; shown instead of a shot code.</summary>
|
|
public string? Label { 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");
|
|
|
|
public BlenderManifest AsBlenderManifest() =>
|
|
Manifest.Deserialize<BlenderManifest>(WorkerOptions.JsonOpts)
|
|
?? throw new InvalidOperationException("Job manifest is not a Blender manifest");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <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>
|
|
/// 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.
|
|
/// </summary>
|
|
public string DeliveredBaseName { 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>
|
|
/// 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.
|
|
/// </summary>
|
|
public double? PictureStartSeconds { 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; }
|
|
}
|