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; } }