using Microsoft.Extensions.Logging.Abstractions;
namespace VFXReviewWorker;
///
/// `VFXReviewWorker.exe --validate <renderDir> [--plate <plateFile>] [--fps 24]`
///
/// Runs the validation engine (§8) over a folder of already-rendered EXRs and
/// prints the report, without a server, a job or a queue. Two uses: proving the
/// checks against real work before the stage goes live, and answering "is this
/// delivery good?" for a sequence that predates the pipeline.
///
public static class ValidateCommand
{
public static int Run(string[] args)
{
var dir = args.SkipWhile(a => !a.Equals("--validate", StringComparison.OrdinalIgnoreCase)).Skip(1).FirstOrDefault();
if (string.IsNullOrWhiteSpace(dir) || dir.StartsWith('-'))
{
Console.Error.WriteLine("Usage: VFXReviewWorker.exe --validate [--plate ] [--fps 24] [--sample 25]");
return 2;
}
if (!Directory.Exists(dir))
{
Console.Error.WriteLine($"Not a directory: {dir}");
return 2;
}
var plate = ArgValue(args, "--plate");
var fps = double.TryParse(ArgValue(args, "--fps"), out var f) ? f : 24;
var sample = int.TryParse(ArgValue(args, "--sample"), out var s) ? s : 25;
var files = Directory.EnumerateFiles(dir, "*.exr").OrderBy(x => x, StringComparer.Ordinal).ToList();
if (files.Count == 0)
{
Console.Error.WriteLine($"No .exr files in {dir}");
return 2;
}
// Infer what the manifest would have said from the files themselves.
var firstName = Path.GetFileName(files[0]);
var frames = files.Select(x => SequencePattern.FrameFromName(Path.GetFileName(x))).Where(x => x is not null).Select(x => x!.Value).ToList();
var prefix = firstName.Contains('.') ? firstName[..firstName.IndexOf('.')] : Path.GetFileNameWithoutExtension(firstName);
var header = ExrHeader.TryRead(files[0]);
var manifest = new RenderManifest
{
ShotCode = prefix.Split("_cmp_")[0],
OutputDir = dir,
OutputPattern = $"{prefix}.[#####].exr",
FrameStart = frames.Count > 0 ? frames.Min() : 1,
FrameEnd = frames.Count > 0 ? frames.Max() : files.Count,
Fps = fps,
Width = header?.DataWidth ?? 0,
Height = header?.DataHeight ?? 0,
Source = plate is null ? null : new ManifestSource
{
TimingPlate = new PlateRef { Path = plate, FrameStart = SequencePattern.FrameFromName(Path.GetFileName(plate)) },
StartSource = "supplied on the command line",
},
};
var options = new WorkerOptions { ServerUrl = "n/a", ApiKey = "n/a" };
var stage = new ValidationStage(options, NullLogger.Instance);
var outcome = stage.Run(manifest, dir, new ServerConfig { ValidationSampleEvery = sample });
Console.WriteLine();
Console.WriteLine($"{files.Count} files in {dir}");
Console.WriteLine($"inferred range {manifest.FrameStart}-{manifest.FrameEnd} at {fps} fps, pattern {manifest.OutputPattern}");
if (plate is not null) Console.WriteLine($"timing plate {plate}");
Console.WriteLine();
foreach (var c in outcome.Checks)
{
var mark = c.Status switch { "PASS" => " ok ", "FAIL" => " FAIL ", "WARN" => " warn ", _ => " skip " };
Console.WriteLine($"[{mark}] {c.CheckName,-24} {Describe(c)}");
}
Console.WriteLine();
Console.WriteLine($"checksum (xxh64): {outcome.Checksum ?? "(not computed)"}");
var failed = outcome.Checks.Count(c => c.Status == "FAIL");
Console.WriteLine(failed == 0 ? "PASSED" : $"FAILED — {failed} check(s)");
return failed == 0 ? 0 : 1;
}
private static string Describe(ValidationCheck c)
{
if (!string.IsNullOrWhiteSpace(c.Message)) return c.Message!;
if (c.Expected is null && c.Actual is null) return "";
return c.Expected == c.Actual ? c.Actual ?? "" : $"expected {c.Expected}, got {c.Actual}";
}
private static string? ArgValue(string[] args, string name)
{
var i = Array.FindIndex(args, a => a.Equals(name, StringComparison.OrdinalIgnoreCase));
return i >= 0 && i + 1 < args.Length ? args[i + 1] : null;
}
}