기초
This commit is contained in:
@@ -19,6 +19,7 @@ internal static class LiveCutValidation
|
||||
Console.WriteLine($"- Image Root: {options.ImageRootPath}");
|
||||
Console.WriteLine($"- Output: {options.OutputPath}");
|
||||
Console.WriteLine($"- Include VideoWall: {(options.IncludeVideoWall ? "yes" : "no")}");
|
||||
Console.WriteLine($"- Capture Mode: {options.CaptureMode}");
|
||||
|
||||
var logService = new LogService();
|
||||
var cutDebugStateStore = new CutDebugStateStore();
|
||||
@@ -70,6 +71,9 @@ internal static class LiveCutValidation
|
||||
CutName = item.Cut.Name,
|
||||
Channel = item.Template.RecommendedChannel.ToString(),
|
||||
Phase = preElection ? BroadcastPhase.PreElection.ToString() : BroadcastPhase.Counting.ToString(),
|
||||
CaptureMode = options.CaptureMode.ToString(),
|
||||
CaptureComparable = options.CaptureMode == LiveCutCaptureMode.Scene ||
|
||||
(pgmWindow is not null && item.Template.RecommendedChannel != BroadcastChannel.VideoWall),
|
||||
OutputVisibleInPgm = pgmWindow is not null &&
|
||||
item.Template.RecommendedChannel != BroadcastChannel.VideoWall
|
||||
};
|
||||
@@ -81,8 +85,22 @@ internal static class LiveCutValidation
|
||||
await OutAllAsync(adapter).ConfigureAwait(false);
|
||||
await Task.Delay(options.BetweenDelayMs).ConfigureAwait(false);
|
||||
|
||||
var snapshotA = CreateSnapshot(item.Template.Name, index, variant: 0, preElection, options.SwapTopTwoCandidates);
|
||||
var snapshotB = CreateSnapshot(item.Template.Name, index, variant: 1, preElection, options.SwapTopTwoCandidates);
|
||||
var snapshotA = CreateSnapshot(
|
||||
item.Template.Name,
|
||||
index,
|
||||
variant: 0,
|
||||
preElection,
|
||||
options.SwapTopTwoCandidates,
|
||||
options.CycleTopThreeCandidates,
|
||||
options.StressTopRankValues);
|
||||
var snapshotB = CreateSnapshot(
|
||||
item.Template.Name,
|
||||
index,
|
||||
variant: 1,
|
||||
preElection,
|
||||
options.SwapTopTwoCandidates,
|
||||
options.CycleTopThreeCandidates,
|
||||
options.StressTopRankValues);
|
||||
|
||||
await adapter.ApplyCutAsync(item.Template.RecommendedChannel, item.Template, item.Cut, snapshotA, station, options.ImageRootPath, CancellationToken.None).ConfigureAwait(false);
|
||||
await adapter.PrepareAsync(item.Template.RecommendedChannel, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -90,7 +108,14 @@ internal static class LiveCutValidation
|
||||
await Task.Delay(ResolveTemplateOnAirDelayMs(item.Template, options.OnAirDelayMs)).ConfigureAwait(false);
|
||||
|
||||
result.CaptureAPath = Path.Combine(options.OutputPath, $"{index + 1:000}_{SanitizeFileName(item.Template.Name)}_A.png");
|
||||
result.HashA = CapturePgm(pgmWindow, result.CaptureAPath, !result.OutputVisibleInPgm);
|
||||
result.HashA = await CaptureValidationImageAsync(
|
||||
adapter,
|
||||
pgmWindow,
|
||||
item,
|
||||
options,
|
||||
result.CaptureAPath,
|
||||
result.OutputVisibleInPgm,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
await adapter.ApplyCutAsync(item.Template.RecommendedChannel, item.Template, item.Cut, snapshotB, station, options.ImageRootPath, CancellationToken.None).ConfigureAwait(false);
|
||||
await adapter.PrepareAsync(item.Template.RecommendedChannel, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -98,11 +123,18 @@ internal static class LiveCutValidation
|
||||
await Task.Delay(ResolveTemplateOnAirDelayMs(item.Template, options.OnAirDelayMs)).ConfigureAwait(false);
|
||||
|
||||
result.CaptureBPath = Path.Combine(options.OutputPath, $"{index + 1:000}_{SanitizeFileName(item.Template.Name)}_B.png");
|
||||
result.HashB = CapturePgm(pgmWindow, result.CaptureBPath, !result.OutputVisibleInPgm);
|
||||
result.HashB = await CaptureValidationImageAsync(
|
||||
adapter,
|
||||
pgmWindow,
|
||||
item,
|
||||
options,
|
||||
result.CaptureBPath,
|
||||
result.OutputVisibleInPgm,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
result.VisualChanged = !string.Equals(result.HashA, result.HashB, StringComparison.OrdinalIgnoreCase);
|
||||
result.Success = true;
|
||||
result.Detail = result.OutputVisibleInPgm
|
||||
? (result.VisualChanged ? "A/B capture changed" : "A/B capture hash identical")
|
||||
result.Detail = result.CaptureComparable
|
||||
? (result.VisualChanged ? $"{options.CaptureMode} A/B capture changed" : $"{options.CaptureMode} A/B capture hash identical")
|
||||
: "VideoWall output is not visible in the current PGM window";
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -145,7 +177,7 @@ internal static class LiveCutValidation
|
||||
|
||||
var successCount = results.Count(result => result.Success);
|
||||
var changedCount = results.Count(result => result.Success && result.VisualChanged);
|
||||
var unchangedCount = results.Count(result => result.Success && result.OutputVisibleInPgm && !result.VisualChanged);
|
||||
var unchangedCount = results.Count(result => result.Success && result.CaptureComparable && !result.VisualChanged);
|
||||
var failureCount = results.Count(result => !result.Success);
|
||||
|
||||
Console.WriteLine();
|
||||
@@ -753,6 +785,7 @@ internal static class LiveCutValidation
|
||||
var value when value.StartsWith("개표율", StringComparison.OrdinalIgnoreCase) => 3,
|
||||
var value when value.StartsWith("선거구명", StringComparison.OrdinalIgnoreCase) => 4,
|
||||
var value when value.StartsWith("시도명", StringComparison.OrdinalIgnoreCase) => 4,
|
||||
var value when value.StartsWith("의석수", StringComparison.OrdinalIgnoreCase) => 5,
|
||||
var value when value.StartsWith("득표수", StringComparison.OrdinalIgnoreCase) => 5,
|
||||
var value when value.StartsWith("정당명", StringComparison.OrdinalIgnoreCase) => 6,
|
||||
var value when value.StartsWith("유확당", StringComparison.OrdinalIgnoreCase) => 7,
|
||||
@@ -898,7 +931,7 @@ internal static class LiveCutValidation
|
||||
yield return new CutDebugItemDescriptor($"{prefix}01", CutDebugItemKind.TextValue, "common");
|
||||
}
|
||||
|
||||
foreach (var prefix in new[] { "순위", "기호", "기호텍스트", "후보명", "정당명", "득표수", "득표율", "표차", "득표차", "선거구명", "시도명", "개표율", "투표율" })
|
||||
foreach (var prefix in new[] { "순위", "기호", "기호텍스트", "후보명", "정당명", "의석수", "득표수", "득표율", "표차", "득표차", "선거구명", "시도명", "개표율", "투표율" })
|
||||
{
|
||||
for (var slot = 1; slot <= slotCount; slot++)
|
||||
{
|
||||
@@ -945,6 +978,11 @@ internal static class LiveCutValidation
|
||||
|
||||
private static int ResolveDebugSlotCount(FormatTemplateDefinition template)
|
||||
{
|
||||
if (ScheduleTemplatePolicy.IsCouncilSeatTableFormat(template.Name))
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
|
||||
var source = $"{template.Name} {template.Id}";
|
||||
var topRankMatch = System.Text.RegularExpressions.Regex.Match(source, @"1-(\d+)위");
|
||||
if (topRankMatch.Success && int.TryParse(topRankMatch.Groups[1].Value, out var topRankSlots))
|
||||
@@ -1349,7 +1387,14 @@ internal static class LiveCutValidation
|
||||
templateName.StartsWith("사전_", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static ElectionDataSnapshot CreateSnapshot(string templateName, int index, int variant, bool preElection, bool swapTopTwoCandidates)
|
||||
private static ElectionDataSnapshot CreateSnapshot(
|
||||
string templateName,
|
||||
int index,
|
||||
int variant,
|
||||
bool preElection,
|
||||
bool swapTopTwoCandidates,
|
||||
bool cycleTopThreeCandidates = false,
|
||||
bool stressTopRankValues = false)
|
||||
{
|
||||
var metadata = BuildScenarioMetadata(templateName, index, variant);
|
||||
return new ElectionDataSnapshot
|
||||
@@ -1360,7 +1405,9 @@ internal static class LiveCutValidation
|
||||
DistrictCode = metadata.DistrictCode,
|
||||
RegionName = metadata.RegionName,
|
||||
ElectionDistrictName = metadata.ElectionDistrictName,
|
||||
Candidates = preElection ? Array.Empty<CandidateEntry>() : CreateCandidates(templateName, metadata, variant, swapTopTwoCandidates),
|
||||
Candidates = preElection
|
||||
? Array.Empty<CandidateEntry>()
|
||||
: CreateCandidates(templateName, metadata, variant, swapTopTwoCandidates, cycleTopThreeCandidates, stressTopRankValues),
|
||||
TotalExpectedVotes = metadata.TotalExpectedVotes,
|
||||
TurnoutVotes = metadata.TurnoutVotes,
|
||||
CountedVotesFromApi = metadata.CountedVotes,
|
||||
@@ -1374,11 +1421,27 @@ internal static class LiveCutValidation
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CandidateEntry> CreateCandidates(string templateName, ScenarioMetadata metadata, int variant, bool swapTopTwoCandidates)
|
||||
private static IReadOnlyList<CandidateEntry> CreateCandidates(
|
||||
string templateName,
|
||||
ScenarioMetadata metadata,
|
||||
int variant,
|
||||
bool swapTopTwoCandidates,
|
||||
bool cycleTopThreeCandidates,
|
||||
bool stressTopRankValues)
|
||||
{
|
||||
if (ScheduleTemplatePolicy.IsCouncilSeatTableFormat(templateName))
|
||||
{
|
||||
return CreateCouncilSeatCandidates(templateName, metadata, variant, stressTopRankValues);
|
||||
}
|
||||
|
||||
var candidateNames = ResolveCandidateNames(templateName);
|
||||
var parties = ResolveParties(candidateNames.Length);
|
||||
var shares = ResolveVoteShares(templateName, candidateNames.Length, variant);
|
||||
if (stressTopRankValues && variant % 2 == 1)
|
||||
{
|
||||
shares = StressTopRankShares(shares);
|
||||
}
|
||||
|
||||
var automaticJudgement = ResolveAutomaticJudgement(templateName);
|
||||
var identityOrder = Enumerable.Range(0, candidateNames.Length).ToArray();
|
||||
|
||||
@@ -1387,6 +1450,11 @@ internal static class LiveCutValidation
|
||||
(identityOrder[0], identityOrder[1]) = (identityOrder[1], identityOrder[0]);
|
||||
}
|
||||
|
||||
if (cycleTopThreeCandidates && variant % 2 == 1)
|
||||
{
|
||||
CycleTopCandidateIdentities(identityOrder);
|
||||
}
|
||||
|
||||
var candidates = new List<CandidateEntry>(candidateNames.Length);
|
||||
for (var index = 0; index < candidateNames.Length; index++)
|
||||
{
|
||||
@@ -1408,6 +1476,99 @@ internal static class LiveCutValidation
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CandidateEntry> CreateCouncilSeatCandidates(
|
||||
string templateName,
|
||||
ScenarioMetadata metadata,
|
||||
int variant,
|
||||
bool stressTopRankValues)
|
||||
{
|
||||
var parties = ResolveParties(6);
|
||||
var totalSeats = ResolveCouncilSeatPattern(templateName, variant, stressTopRankValues)
|
||||
.Take(parties.Length)
|
||||
.ToArray();
|
||||
var proportionalSeats = ResolveCouncilSeatProportionalPattern(templateName, variant, stressTopRankValues)
|
||||
.Take(parties.Length)
|
||||
.ToArray();
|
||||
var candidates = new List<CandidateEntry>(parties.Length * 2);
|
||||
|
||||
for (var partyIndex = 0; partyIndex < parties.Length; partyIndex++)
|
||||
{
|
||||
var party = parties[partyIndex];
|
||||
var proportionalCount = Math.Min(totalSeats[partyIndex], proportionalSeats.ElementAtOrDefault(partyIndex));
|
||||
var districtCount = Math.Max(0, totalSeats[partyIndex] - proportionalCount);
|
||||
candidates.Add(CreateCouncilSeatSummaryCandidate($"SEAT:D:{partyIndex + 1:00}", party, partyIndex, districtCount));
|
||||
|
||||
if (proportionalCount > 0)
|
||||
{
|
||||
candidates.Add(CreateCouncilSeatSummaryCandidate($"SEAT:P:{partyIndex + 1:00}", party, partyIndex, proportionalCount));
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static CandidateEntry CreateCouncilSeatSummaryCandidate(string candidateCode, string party, int partyIndex, int seatCount)
|
||||
{
|
||||
return new CandidateEntry
|
||||
{
|
||||
CandidateCode = candidateCode,
|
||||
BallotNumber = (partyIndex + 1).ToString(),
|
||||
Name = party,
|
||||
Party = party,
|
||||
ColorParty = party,
|
||||
VoteRate = seatCount,
|
||||
VoteCount = seatCount,
|
||||
HasImage = false,
|
||||
ManualJudgement = CandidateJudgement.None,
|
||||
AutomaticJudgement = CandidateJudgement.Elected
|
||||
};
|
||||
}
|
||||
|
||||
private static int[] ResolveCouncilSeatPattern(string templateName, int variant, bool stressTopRankValues)
|
||||
{
|
||||
if (templateName.Contains("기초의원", StringComparison.Ordinal))
|
||||
{
|
||||
return stressTopRankValues && variant % 2 == 1
|
||||
? [34, 21, 13, 7, 3, 1]
|
||||
: [29, 25, 9, 5, 2, 1];
|
||||
}
|
||||
|
||||
return stressTopRankValues && variant % 2 == 1
|
||||
? [22, 17, 9, 4, 2, 1]
|
||||
: [18, 15, 6, 3, 1, 1];
|
||||
}
|
||||
|
||||
private static int[] ResolveCouncilSeatProportionalPattern(string templateName, int variant, bool stressTopRankValues)
|
||||
{
|
||||
if (templateName.Contains("기초의원", StringComparison.Ordinal))
|
||||
{
|
||||
return stressTopRankValues && variant % 2 == 1
|
||||
? [5, 3, 2, 1, 0, 0]
|
||||
: [4, 3, 1, 0, 0, 0];
|
||||
}
|
||||
|
||||
return stressTopRankValues && variant % 2 == 1
|
||||
? [4, 3, 2, 1, 0, 0]
|
||||
: [3, 3, 1, 0, 0, 0];
|
||||
}
|
||||
|
||||
private static void CycleTopCandidateIdentities(int[] identityOrder)
|
||||
{
|
||||
var topCount = Math.Min(3, identityOrder.Length);
|
||||
if (topCount < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var first = identityOrder[0];
|
||||
for (var index = 0; index < topCount - 1; index++)
|
||||
{
|
||||
identityOrder[index] = identityOrder[index + 1];
|
||||
}
|
||||
|
||||
identityOrder[topCount - 1] = first;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PreElectionHistoricalTurnoutEntry> CreateHistoricalTurnout(ScenarioMetadata metadata, int variant)
|
||||
{
|
||||
// The historical turnout scenes currently ship with a fixed line/marker drawing in the source tscn.
|
||||
@@ -1549,6 +1710,36 @@ internal static class LiveCutValidation
|
||||
return shares;
|
||||
}
|
||||
|
||||
private static double[] StressTopRankShares(double[] shares)
|
||||
{
|
||||
var stressed = shares.ToArray();
|
||||
if (stressed.Length >= 3)
|
||||
{
|
||||
var trailingTotal = Math.Round(stressed.Skip(3).Sum(), 1, MidpointRounding.AwayFromZero);
|
||||
var topTotal = Math.Max(0d, 100d - trailingTotal);
|
||||
stressed[0] = Math.Round(topTotal * 0.46d, 1, MidpointRounding.AwayFromZero);
|
||||
stressed[1] = Math.Round(topTotal * 0.32d, 1, MidpointRounding.AwayFromZero);
|
||||
stressed[2] = Math.Round(topTotal - stressed[0] - stressed[1], 1, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
else if (stressed.Length == 2)
|
||||
{
|
||||
stressed[0] = 62.7d;
|
||||
stressed[1] = 37.3d;
|
||||
}
|
||||
else if (stressed.Length == 1)
|
||||
{
|
||||
stressed[0] = 99.9d;
|
||||
}
|
||||
|
||||
var delta = Math.Round(100d - stressed.Sum(), 1, MidpointRounding.AwayFromZero);
|
||||
if (stressed.Length > 0)
|
||||
{
|
||||
stressed[0] = Math.Round(stressed[0] + delta, 1, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
return stressed;
|
||||
}
|
||||
|
||||
private static CandidateJudgement ResolveAutomaticJudgement(string templateName)
|
||||
{
|
||||
if (templateName.Contains("당선", StringComparison.Ordinal))
|
||||
@@ -1599,6 +1790,11 @@ internal static class LiveCutValidation
|
||||
return new ScenarioMetadata("광역의원", "44001", "충남 제1선거구", "충남", "충남도의원 제1선거구", totalExpectedVotes / 3, turnoutVotes / 3, countedVotes / 3, countedRate, 55.4 + (seed % 3) + (variant * 1.6), DateTimeOffset.Now.AddMinutes(seed + variant), seed);
|
||||
}
|
||||
|
||||
if (templateName.Contains("보궐선거", StringComparison.Ordinal))
|
||||
{
|
||||
return new ScenarioMetadata("국회의원", "2411502", "평택 을", "경기", "평택 을", totalExpectedVotes / 2, turnoutVotes / 2, countedVotes / 2, countedRate, 56.5 + (seed % 4) + (variant * 1.5), DateTimeOffset.Now.AddMinutes(seed + variant), seed);
|
||||
}
|
||||
|
||||
return new ScenarioMetadata("광역단체장", "44", "충청남도", "충남", "충남도지사", totalExpectedVotes, turnoutVotes, countedVotes, countedRate, 57.3 + (seed % 5) + (variant * 1.8), DateTimeOffset.Now.AddMinutes(seed + variant), seed);
|
||||
}
|
||||
|
||||
@@ -1615,6 +1811,57 @@ internal static class LiveCutValidation
|
||||
return ComputeSha256(outputPath);
|
||||
}
|
||||
|
||||
private static async Task<string> CaptureValidationImageAsync(
|
||||
ITornado3Adapter adapter,
|
||||
PgmWindow? pgmWindow,
|
||||
LiveCutWorkItem item,
|
||||
LiveCutValidationOptions options,
|
||||
string outputPath,
|
||||
bool outputVisibleInPgm,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (options.CaptureMode == LiveCutCaptureMode.Pgm)
|
||||
{
|
||||
return CapturePgm(pgmWindow, outputPath, !outputVisibleInPgm);
|
||||
}
|
||||
|
||||
if (adapter is not KarismaTornado3Adapter karismaAdapter)
|
||||
{
|
||||
throw new InvalidOperationException("Scene capture mode requires the Karisma adapter.");
|
||||
}
|
||||
|
||||
var (width, height) = ResolveSceneCaptureSize(item.Template);
|
||||
await karismaAdapter.SavePendingSceneImageAsync(
|
||||
item.Template.RecommendedChannel,
|
||||
outputPath,
|
||||
width,
|
||||
height,
|
||||
frame: -1,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return ComputeSha256(outputPath);
|
||||
}
|
||||
|
||||
private static (int Width, int Height) ResolveSceneCaptureSize(FormatTemplateDefinition template)
|
||||
{
|
||||
var sourceWidth = template.SceneWidth.GetValueOrDefault(1920);
|
||||
var sourceHeight = template.SceneHeight.GetValueOrDefault(1080);
|
||||
if (sourceWidth <= 0 || sourceHeight <= 0)
|
||||
{
|
||||
return (1280, 720);
|
||||
}
|
||||
|
||||
const int maxWidth = 1280;
|
||||
if (sourceWidth <= maxWidth)
|
||||
{
|
||||
return (sourceWidth, sourceHeight);
|
||||
}
|
||||
|
||||
var scale = maxWidth / (double)sourceWidth;
|
||||
return (maxWidth, Math.Max(1, (int)Math.Round(sourceHeight * scale, MidpointRounding.AwayFromZero)));
|
||||
}
|
||||
|
||||
private static Bitmap CaptureWindowBitmap(IntPtr handle, Rect bounds)
|
||||
{
|
||||
var width = Math.Max(1, bounds.Width);
|
||||
@@ -1657,7 +1904,7 @@ internal static class LiveCutValidation
|
||||
var summaryPath = Path.Combine(options.OutputPath, "summary.md");
|
||||
|
||||
var csv = new StringBuilder();
|
||||
csv.AppendLine("Index,TemplateId,TemplateName,CutName,Channel,Phase,Success,VisualChanged,OutputVisibleInPgm,CaptureAPath,CaptureBPath,HashA,HashB,Detail");
|
||||
csv.AppendLine("Index,TemplateId,TemplateName,CutName,Channel,Phase,CaptureMode,CaptureComparable,Success,VisualChanged,OutputVisibleInPgm,CaptureAPath,CaptureBPath,HashA,HashB,Detail");
|
||||
foreach (var result in results)
|
||||
{
|
||||
csv.AppendLine(string.Join(",",
|
||||
@@ -1667,6 +1914,8 @@ internal static class LiveCutValidation
|
||||
Csv(result.CutName),
|
||||
Csv(result.Channel),
|
||||
Csv(result.Phase),
|
||||
Csv(result.CaptureMode),
|
||||
Csv(result.CaptureComparable.ToString()),
|
||||
Csv(result.Success.ToString()),
|
||||
Csv(result.VisualChanged.ToString()),
|
||||
Csv(result.OutputVisibleInPgm.ToString()),
|
||||
@@ -1682,7 +1931,7 @@ internal static class LiveCutValidation
|
||||
|
||||
var successCount = results.Count(result => result.Success);
|
||||
var changedCount = results.Count(result => result.Success && result.VisualChanged);
|
||||
var unchanged = results.Where(result => result.Success && result.OutputVisibleInPgm && !result.VisualChanged).ToList();
|
||||
var unchanged = results.Where(result => result.Success && result.CaptureComparable && !result.VisualChanged).ToList();
|
||||
var failures = results.Where(result => !result.Success).ToList();
|
||||
|
||||
var summary = new StringBuilder();
|
||||
@@ -1691,6 +1940,7 @@ internal static class LiveCutValidation
|
||||
summary.AppendLine($"- Run At: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss zzz}");
|
||||
summary.AppendLine($"- Image Root: {options.ImageRootPath}");
|
||||
summary.AppendLine($"- Output: {options.OutputPath}");
|
||||
summary.AppendLine($"- Capture Mode: {options.CaptureMode}");
|
||||
summary.AppendLine($"- Success: {successCount}/{results.Count}");
|
||||
summary.AppendLine($"- Visual Changed: {changedCount}");
|
||||
summary.AppendLine($"- Unchanged Captures: {unchanged.Count}");
|
||||
@@ -1820,6 +2070,9 @@ internal static class LiveCutValidation
|
||||
public int BetweenDelayMs { get; init; } = 250;
|
||||
public bool IncludeVideoWall { get; init; }
|
||||
public bool SwapTopTwoCandidates { get; init; }
|
||||
public LiveCutCaptureMode CaptureMode { get; init; } = LiveCutCaptureMode.Pgm;
|
||||
public bool CycleTopThreeCandidates { get; init; }
|
||||
public bool StressTopRankValues { get; init; }
|
||||
|
||||
public static LiveCutValidationOptions Parse(string[] args)
|
||||
{
|
||||
@@ -1836,7 +2089,7 @@ internal static class LiveCutValidation
|
||||
switch (args[index])
|
||||
{
|
||||
case "--image-root":
|
||||
options = options with { ImageRootPath = RequireValue(args, ref index, "--image-root") };
|
||||
_ = RequireValue(args, ref index, "--image-root");
|
||||
break;
|
||||
case "--output":
|
||||
options = options with { OutputPath = RequireValue(args, ref index, "--output") };
|
||||
@@ -1859,6 +2112,15 @@ internal static class LiveCutValidation
|
||||
case "--swap-top-two":
|
||||
options = options with { SwapTopTwoCandidates = true };
|
||||
break;
|
||||
case "--cycle-top-three":
|
||||
options = options with { CycleTopThreeCandidates = true };
|
||||
break;
|
||||
case "--stress-top-ranks":
|
||||
options = options with { StressTopRankValues = true };
|
||||
break;
|
||||
case "--capture-mode":
|
||||
options = options with { CaptureMode = ParseCaptureMode(RequireValue(args, ref index, "--capture-mode")) };
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown option: {args[index]}");
|
||||
}
|
||||
@@ -1866,7 +2128,7 @@ internal static class LiveCutValidation
|
||||
|
||||
return options with
|
||||
{
|
||||
ImageRootPath = Path.GetFullPath(options.ImageRootPath),
|
||||
ImageRootPath = Path.GetFullPath(TornadoPathResolver.GetDefaultT3CutPath()),
|
||||
OutputPath = Path.GetFullPath(options.OutputPath),
|
||||
StationLogoPath = Path.GetFullPath(options.StationLogoPath)
|
||||
};
|
||||
@@ -1882,6 +2144,16 @@ internal static class LiveCutValidation
|
||||
|
||||
return args[index];
|
||||
}
|
||||
|
||||
private static LiveCutCaptureMode ParseCaptureMode(string raw)
|
||||
{
|
||||
return raw.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"pgm" or "window" => LiveCutCaptureMode.Pgm,
|
||||
"scene" or "save-scene-image" => LiveCutCaptureMode.Scene,
|
||||
_ => throw new ArgumentException($"Unknown capture mode: {raw}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record CutDebugCoverageOptions
|
||||
@@ -1908,7 +2180,7 @@ internal static class LiveCutValidation
|
||||
switch (args[index])
|
||||
{
|
||||
case "--image-root":
|
||||
options = options with { ImageRootPath = RequireValue(args, ref index, "--image-root") };
|
||||
_ = RequireValue(args, ref index, "--image-root");
|
||||
break;
|
||||
case "--output":
|
||||
options = options with { OutputPath = RequireValue(args, ref index, "--output") };
|
||||
@@ -1935,7 +2207,7 @@ internal static class LiveCutValidation
|
||||
|
||||
return options with
|
||||
{
|
||||
ImageRootPath = Path.GetFullPath(options.ImageRootPath),
|
||||
ImageRootPath = Path.GetFullPath(TornadoPathResolver.GetDefaultT3CutPath()),
|
||||
OutputPath = Path.GetFullPath(options.OutputPath)
|
||||
};
|
||||
}
|
||||
@@ -1985,7 +2257,7 @@ internal static class LiveCutValidation
|
||||
switch (args[index])
|
||||
{
|
||||
case "--image-root":
|
||||
options = options with { ImageRootPath = RequireValue(args, ref index, "--image-root") };
|
||||
_ = RequireValue(args, ref index, "--image-root");
|
||||
break;
|
||||
case "--output":
|
||||
options = options with { OutputPath = RequireValue(args, ref index, "--output") };
|
||||
@@ -2036,7 +2308,7 @@ internal static class LiveCutValidation
|
||||
|
||||
return options with
|
||||
{
|
||||
ImageRootPath = Path.GetFullPath(options.ImageRootPath),
|
||||
ImageRootPath = Path.GetFullPath(TornadoPathResolver.GetDefaultT3CutPath()),
|
||||
OutputPath = Path.GetFullPath(options.OutputPath),
|
||||
StationLogoPath = Path.GetFullPath(options.StationLogoPath)
|
||||
};
|
||||
@@ -2087,6 +2359,12 @@ internal static class LiveCutValidation
|
||||
Replace
|
||||
}
|
||||
|
||||
private enum LiveCutCaptureMode
|
||||
{
|
||||
Pgm,
|
||||
Scene
|
||||
}
|
||||
|
||||
private readonly record struct CutDebugReplacementAssets(string MagentaPath, string CyanPath);
|
||||
|
||||
private sealed class CutDebugSweepResult
|
||||
@@ -2163,6 +2441,8 @@ internal static class LiveCutValidation
|
||||
public string CutName { get; init; } = string.Empty;
|
||||
public string Channel { get; init; } = string.Empty;
|
||||
public string Phase { get; init; } = string.Empty;
|
||||
public string CaptureMode { get; set; } = string.Empty;
|
||||
public bool CaptureComparable { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public bool VisualChanged { get; set; }
|
||||
public bool OutputVisibleInPgm { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user