Show field-level inspection details
This commit is contained in:
@@ -1,10 +1,32 @@
|
||||
namespace TornadoAce_CJOnStyle.Domain
|
||||
{
|
||||
public sealed record InspectionFieldResult(
|
||||
string CueSheetField,
|
||||
string SceneObjectName,
|
||||
string ExpectedText,
|
||||
string ActualText,
|
||||
double MatchRate,
|
||||
string Severity,
|
||||
string Decision);
|
||||
|
||||
public sealed record InspectionResult(
|
||||
string CueSheetId,
|
||||
string ProgramTitle,
|
||||
string SceneName,
|
||||
double MatchRate,
|
||||
string Severity,
|
||||
string Decision);
|
||||
string Decision,
|
||||
IReadOnlyList<InspectionFieldResult> FieldResults)
|
||||
{
|
||||
public InspectionResult(
|
||||
string cueSheetId,
|
||||
string programTitle,
|
||||
string sceneName,
|
||||
double matchRate,
|
||||
string severity,
|
||||
string decision)
|
||||
: this(cueSheetId, programTitle, sceneName, matchRate, severity, decision, [])
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,18 +44,28 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
private InspectionResult Inspect(CueSheetRecord cueSheet, SceneDataSnapshot snapshot, SceneTemplateDefinition template)
|
||||
{
|
||||
var expectedObjects = BuildVisibleTextObjects(cueSheet, template);
|
||||
var fieldScores = expectedObjects
|
||||
.Select(expected =>
|
||||
var fieldResults = template.Mappings
|
||||
.Select(mapping =>
|
||||
{
|
||||
snapshot.VisibleTextObjects.TryGetValue(expected.Key, out var actualValue);
|
||||
var score = CalculateSimilarity(expected.Value, actualValue ?? string.Empty);
|
||||
return new FieldInspectionScore(expected.Key, score);
|
||||
var expectedValue = expectedObjects[mapping.SceneObjectName];
|
||||
snapshot.VisibleTextObjects.TryGetValue(mapping.SceneObjectName, out var actualValue);
|
||||
var score = CalculateSimilarity(expectedValue, actualValue ?? string.Empty);
|
||||
var severity = ResolveFieldSeverity(mapping.SceneObjectName, score);
|
||||
|
||||
return new InspectionFieldResult(
|
||||
mapping.CueSheetField,
|
||||
mapping.SceneObjectName,
|
||||
expectedValue,
|
||||
actualValue ?? string.Empty,
|
||||
score,
|
||||
severity,
|
||||
ResolveFieldDecision(severity));
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
var matchRate = fieldScores.Length == 0 ? 100 : fieldScores.Average(field => field.Score);
|
||||
var hasImportantMismatch = fieldScores.Any(field => field.Score < 99.5 && IsImportantSceneObject(field.SceneObjectName));
|
||||
var mismatchCount = fieldScores.Count(field => field.Score < 99.5);
|
||||
var matchRate = fieldResults.Length == 0 ? 100 : fieldResults.Average(field => field.MatchRate);
|
||||
var hasImportantMismatch = fieldResults.Any(field => field.Severity == "High");
|
||||
var mismatchCount = fieldResults.Count(field => field.MatchRate < 99.5);
|
||||
|
||||
var severity = hasImportantMismatch || mismatchCount > 1
|
||||
? "High"
|
||||
@@ -70,7 +80,7 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
_ => "1차 통과"
|
||||
};
|
||||
|
||||
return new InspectionResult(cueSheet.CueSheetId, cueSheet.ProgramTitle, snapshot.SceneName, matchRate, severity, decision);
|
||||
return new InspectionResult(cueSheet.CueSheetId, cueSheet.ProgramTitle, snapshot.SceneName, matchRate, severity, decision, fieldResults);
|
||||
}
|
||||
|
||||
private SceneDataSnapshot GenerateExtractedScene(CueSheetRecord cueSheet, SceneTemplateDefinition template)
|
||||
@@ -135,6 +145,26 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
|| sceneObjectName.Contains("COMPOSITION", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ResolveFieldSeverity(string sceneObjectName, double matchRate)
|
||||
{
|
||||
if (matchRate >= 99.5)
|
||||
{
|
||||
return "Low";
|
||||
}
|
||||
|
||||
return IsImportantSceneObject(sceneObjectName) ? "High" : "Medium";
|
||||
}
|
||||
|
||||
private static string ResolveFieldDecision(string severity)
|
||||
{
|
||||
return severity switch
|
||||
{
|
||||
"High" => "운영자 확인",
|
||||
"Medium" => "LLM 후보",
|
||||
_ => "일치"
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateSimilarity(string expected, string actual)
|
||||
{
|
||||
var normalizedExpected = Normalize(expected);
|
||||
@@ -186,8 +216,6 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
|
||||
return previousRow[target.Length];
|
||||
}
|
||||
|
||||
private sealed record FieldInspectionScore(string SceneObjectName, double Score);
|
||||
}
|
||||
|
||||
public sealed record CueSheetSelection(CueSheetRecord Record, int Position, int TotalCount);
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
LoadEndpoints();
|
||||
LoadRequirementCoverage();
|
||||
LoadInspectionQueue(initialInspectionSummary.Results);
|
||||
LoadInspectionDetails(initialInspectionSummary.Results);
|
||||
}
|
||||
|
||||
private string buildPhase = string.Empty;
|
||||
@@ -128,6 +129,8 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
|
||||
public ObservableCollection<InspectionQueueItemViewModel> InspectionQueue { get; } = [];
|
||||
|
||||
public ObservableCollection<InspectionDetailItemViewModel> InspectionDetails { get; } = [];
|
||||
|
||||
public string LoginOperatorName
|
||||
{
|
||||
get => loginOperatorName;
|
||||
@@ -209,6 +212,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
StatusCards[2].Metric = $"{summary.AverageMatchRate:0.0}%";
|
||||
StatusCards[2].Detail = $"확인 {summary.ReviewCount}건";
|
||||
LoadInspectionQueue(summary.Results);
|
||||
LoadInspectionDetails(summary.Results);
|
||||
}
|
||||
|
||||
private void LoadStatusCards()
|
||||
@@ -251,7 +255,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
RequirementCoverage.Add(new PocRequirementViewModel("RFP-03", "Data Field Mapping", "PoC", "Scene 스냅샷 생성", Brush(0xFF37D67A)));
|
||||
RequirementCoverage.Add(new PocRequirementViewModel("RFP-04", "Tornado Scene API", "PoC", "Text/Image 추출 샘플", Brush(0xFF37D67A)));
|
||||
RequirementCoverage.Add(new PocRequirementViewModel("RFP-05", "관리자 매핑 기능", "초안", "매핑표 UI", Brush(0xFFB48CFF)));
|
||||
RequirementCoverage.Add(new PocRequirementViewModel("RFP-06", "1·2단계 자동 검수", "PoC", "Diff + LLM 후보", Brush(0xFF45D6D1)));
|
||||
RequirementCoverage.Add(new PocRequirementViewModel("RFP-06", "1·2단계 자동 검수", "PoC", "필드 상세 Diff + LLM 후보", Brush(0xFF45D6D1)));
|
||||
}
|
||||
|
||||
private void LoadInspectionQueue(IEnumerable<InspectionResult> results)
|
||||
@@ -270,6 +274,31 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadInspectionDetails(IEnumerable<InspectionResult> results)
|
||||
{
|
||||
InspectionDetails.Clear();
|
||||
|
||||
var detailItems = results
|
||||
.SelectMany(result => result.FieldResults.Select(field => new { Result = result, Field = field }))
|
||||
.Where(item => item.Field.MatchRate < 99.5)
|
||||
.OrderBy(item => item.Field.MatchRate)
|
||||
.ThenBy(item => item.Result.CueSheetId)
|
||||
.Take(5);
|
||||
|
||||
foreach (var item in detailItems)
|
||||
{
|
||||
InspectionDetails.Add(new InspectionDetailItemViewModel(
|
||||
item.Result.CueSheetId,
|
||||
item.Field.SceneObjectName,
|
||||
item.Field.ExpectedText,
|
||||
item.Field.ActualText,
|
||||
$"{item.Field.MatchRate:0.0}%",
|
||||
item.Field.Severity,
|
||||
item.Field.Decision,
|
||||
SeverityBrush(item.Field.Severity)));
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySceneSnapshot(SceneDataSnapshot scene)
|
||||
{
|
||||
SceneSnapshotLabel = $"{scene.SceneName} / {scene.VisibleTextObjects.Count} text objects";
|
||||
|
||||
@@ -74,4 +74,14 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
string Severity,
|
||||
string Decision,
|
||||
SolidColorBrush SeverityBrush);
|
||||
|
||||
public sealed record InspectionDetailItemViewModel(
|
||||
string CueSheetId,
|
||||
string SceneObject,
|
||||
string ExpectedText,
|
||||
string ActualText,
|
||||
string MatchRate,
|
||||
string Severity,
|
||||
string Decision,
|
||||
SolidColorBrush SeverityBrush);
|
||||
}
|
||||
|
||||
@@ -529,38 +529,103 @@
|
||||
BorderBrush="{StaticResource PanelStrokeBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock Style="{StaticResource SectionTitleTextStyle}" Text="Scene 데이터 API 샘플" />
|
||||
<Border
|
||||
Padding="14"
|
||||
Background="#101419"
|
||||
BorderBrush="{StaticResource PanelStrokeBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="8">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" VerticalScrollMode="Auto">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock Style="{StaticResource SectionTitleTextStyle}" Text="Scene 데이터 API 샘플" />
|
||||
<Border
|
||||
Padding="14"
|
||||
Background="#101419"
|
||||
BorderBrush="{StaticResource PanelStrokeBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock
|
||||
Style="{StaticResource LabelTextStyle}"
|
||||
Text="{x:Bind ViewModel.SceneSnapshotLabel, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
FontFamily="Cascadia Mono"
|
||||
FontSize="13"
|
||||
Foreground="{StaticResource AccentCyanBrush}"
|
||||
Text="{x:Bind ViewModel.SceneSnapshotText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Style="{StaticResource LabelTextStyle}" Text="LLM 검수 정책" />
|
||||
<TextBlock
|
||||
Style="{StaticResource LabelTextStyle}"
|
||||
Text="{x:Bind ViewModel.SceneSnapshotLabel, Mode=OneWay}" />
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
FontSize="18"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Text="{x:Bind ViewModel.LlmPolicyText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
FontFamily="Cascadia Mono"
|
||||
FontSize="13"
|
||||
Foreground="{StaticResource AccentCyanBrush}"
|
||||
Text="{x:Bind ViewModel.SceneSnapshotText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
Style="{StaticResource BodyTextStyle}"
|
||||
Text="{x:Bind ViewModel.LlmPolicyDetail, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Style="{StaticResource LabelTextStyle}" Text="LLM 검수 정책" />
|
||||
<TextBlock
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
FontSize="18"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Text="{x:Bind ViewModel.LlmPolicyText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Style="{StaticResource BodyTextStyle}"
|
||||
Text="{x:Bind ViewModel.LlmPolicyDetail, Mode=OneWay}" />
|
||||
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{StaticResource PanelStrokeBrush}" />
|
||||
|
||||
<TextBlock Style="{StaticResource SectionTitleTextStyle}" Text="검수 상세" />
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.InspectionDetails, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:InspectionDetailItemViewModel">
|
||||
<Border
|
||||
Margin="0,0,0,10"
|
||||
Padding="10"
|
||||
Background="{StaticResource PanelAltBrush}"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="7">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock
|
||||
FontFamily="Cascadia Mono"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Text="{x:Bind CueSheetId}" />
|
||||
<TextBlock
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Text="{x:Bind SceneObject}" />
|
||||
</StackPanel>
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Padding="8,3"
|
||||
Background="{x:Bind SeverityBrush}"
|
||||
CornerRadius="8">
|
||||
<TextBlock
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
FontSize="12"
|
||||
Foreground="#071013"
|
||||
Text="{x:Bind Severity}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Style="{StaticResource LabelTextStyle}"
|
||||
Text="{x:Bind MatchRate}" />
|
||||
<TextBlock
|
||||
Style="{StaticResource BodyTextStyle}"
|
||||
Text="{x:Bind ExpectedText}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Style="{StaticResource BodyTextStyle}"
|
||||
Foreground="{StaticResource AccentCyanBrush}"
|
||||
Text="{x:Bind ActualText}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Style="{StaticResource LabelTextStyle}"
|
||||
Text="{x:Bind Decision}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
| RFP-03 | CG 포맷 제작 자동화, Data Field Mapping | 큐시트 필드와 Scene 객체 매핑 구현 | `SceneTemplateDefinition`, `PocAutomationEngine.GenerateScene` |
|
||||
| RFP-04 | Tornado Scene API | Scene visible Text/Image 스냅샷 샘플 구현 | `SceneDataSnapshot.ToPreviewJson` |
|
||||
| RFP-05 | 큐시트 데이터와 Tornado Scene 연동 관리자 기능 | 매핑표 UI 초안 구현 | `FieldMappings` 화면 |
|
||||
| RFP-06 | 자동 검수 1단계 Text Diffing | 문자열 정규화/유사도 기반 비교 구현 | `PocAutomationEngine.InspectWorkspace` |
|
||||
| RFP-07 | 자동 검수 2단계 LLM 중요도 피드백 | LLM 검토 후보 분류와 정책 문구 구현 | 검수 결과 `Severity`, `Decision`, LLM 정책 패널 |
|
||||
| RFP-06 | 자동 검수 1단계 Text Diffing | 필드별 기대값/실제값/일치율 비교 구현 | `PocAutomationEngine.InspectWorkspace`, `InspectionFieldResult` |
|
||||
| RFP-07 | 자동 검수 2단계 LLM 중요도 피드백 | LLM 검토 후보 분류와 제작자 상세 UI 구현 | 검수 결과 `Severity`, `Decision`, `InspectionDetails` |
|
||||
| RFP-08 | 제작자가 결과를 볼 수 있는 GUI | WinUI 3 운영 콘솔 초안 구현 | `MainPage.xaml` |
|
||||
|
||||
## 초기 PoC 완료 기준
|
||||
@@ -35,5 +35,5 @@
|
||||
- 큐시트/ERP Mock 데이터를 JSON 파일로 분리하여 실제 API 응답 구조와 맞춘다.
|
||||
- Tornado2/Tornado3 어댑터를 동일 인터페이스로 유지하고, 버전별 구현체를 분리한다.
|
||||
- Scene 내부 비노출 객체를 구분할 메타데이터 규칙을 정의한다.
|
||||
- 1차 검수 실패 사유를 필드 단위로 화면에 표시한다.
|
||||
- 1차 검수 실패 사유를 필드 단위로 누적하고 LLM 요청 후보로 전달한다.
|
||||
- LLM 요청/응답 스키마와 감사 로그 보관 정책을 별도 모델로 둔다.
|
||||
|
||||
Reference in New Issue
Block a user