using System.Text.RegularExpressions;
namespace VFXReviewWorker;
///
/// Parses aerender stdout line-by-line (§7.3): PROGRESS lines yield the
/// current frame; ERROR lines are collected; deterministic errors (missing
/// footage/comp/project) mark the job non-retryable — humans fix the comp.
///
public static partial class ProgressParser
{
// e.g. "PROGRESS: 0:00:02:03 (51): 0 Seconds" / "PROGRESS: 2 (2): ..."
[GeneratedRegex(@"^PROGRESS:.*\((\d+)\)", RegexOptions.Compiled)]
private static partial Regex ProgressLine();
[GeneratedRegex(@"aerender\s+(SYNTAX\s+)?ERROR|^ERROR:", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
private static partial Regex ErrorLine();
private static readonly string[] NonRetryablePatterns =
{
"missing footage",
"layer source file missing",
"file is missing",
"no comp was found",
"no composition",
"project file could not be opened",
"can not be opened",
"cannot be opened",
"the file format module could not parse the file",
"no render settings template",
"no output module template",
"syntax error",
"illegal argument",
};
/// Returns the rendered-frame ordinal (1-based within the range) or null.
public static int? ParseFrame(string line)
{
var m = ProgressLine().Match(line);
return m.Success && int.TryParse(m.Groups[1].Value, out var f) ? f : null;
}
public static bool IsErrorLine(string line) => ErrorLine().IsMatch(line);
/// Deterministic failures re-render identically — don't burn retries on them.
public static bool IsNonRetryableError(string line)
{
foreach (var p in NonRetryablePatterns)
{
if (line.Contains(p, StringComparison.OrdinalIgnoreCase)) return true;
}
return false;
}
}
/// ETA = rolling average seconds/frame × frames remaining (§7.3).
public sealed class EtaCalculator
{
private readonly int _windowSize;
private readonly Queue _frameSeconds = new();
private DateTimeOffset? _lastFrameAt;
public EtaCalculator(int windowSize = 10) => _windowSize = windowSize;
public void RecordFrame(DateTimeOffset now)
{
if (_lastFrameAt is { } last)
{
_frameSeconds.Enqueue((now - last).TotalSeconds);
while (_frameSeconds.Count > _windowSize) _frameSeconds.Dequeue();
}
_lastFrameAt = now;
}
public int? EstimateSeconds(int framesRemaining)
{
if (_frameSeconds.Count == 0 || framesRemaining <= 0) return null;
return (int)Math.Round(_frameSeconds.Average() * framesRemaining);
}
}