Add Phase 2 fixtures and compatibility tests
This commit is contained in:
@@ -551,20 +551,64 @@ Before accepting a phase implementation, check:
|
|||||||
- Does `go test ./...` pass?
|
- Does `go test ./...` pass?
|
||||||
- Is the code idiomatic Go rather than Python-shaped Go?
|
- Is the code idiomatic Go rather than Python-shaped Go?
|
||||||
|
|
||||||
## Red flags during the rewrite
|
## Phase 2 Status (Go Implementation)
|
||||||
|
|
||||||
Watch for:
|
The Go rewrite has completed Phase 2 with the following capabilities:
|
||||||
|
|
||||||
- module stages running concurrently and changing pipeline semantics;
|
### Implemented Features:
|
||||||
- prompt wording changes mixed into infrastructure commits;
|
|
||||||
- validators silently ignoring malformed LLM output;
|
1. **Typed Schema Validation**:
|
||||||
- proposal application mutating text without report entries;
|
- Transcript validation supports both bare segment arrays and objects with `segments` field
|
||||||
- report data printed to stdout unexpectedly;
|
- Glossary validation matches the existing YAML format
|
||||||
- API keys appearing in diagnostics;
|
- Comprehensive field validation and error reporting
|
||||||
- global mutable LLM clients that make tests order-dependent;
|
|
||||||
- unbounded goroutine creation;
|
2. **Deterministic Normalization**:
|
||||||
- filesystem paths hard-coded outside config defaults;
|
- Chronological sorting of segments
|
||||||
- tests that require a real LLM when a fake LLM would be better.
|
- Same-speaker adjacent segment merging with configurable gap thresholds
|
||||||
|
- Ellipsis insertion for gaps above the ellipsis threshold
|
||||||
|
- Configurable maximum segment duration and token limits
|
||||||
|
- Sequential ID reassignment starting at 1
|
||||||
|
- Category preservation during merging
|
||||||
|
|
||||||
|
3. **Process Output**:
|
||||||
|
- Output is normalized transcript JSON (not raw passthrough)
|
||||||
|
- Canonical transcript shape as specified in architecture docs
|
||||||
|
- Proper stdout/stderr discipline preserved from Phase 1
|
||||||
|
|
||||||
|
4. **Enhanced Reporting**:
|
||||||
|
- Success reports include normalization statistics
|
||||||
|
- Failure reports include error phase and concise messages
|
||||||
|
- No secrets leaked to reports
|
||||||
|
|
||||||
|
5. **Minimal Diagnostics**:
|
||||||
|
- Per-run work directories with unique timestamps
|
||||||
|
- Source transcript artifacts (raw and parsed)
|
||||||
|
- Normalized transcript artifacts
|
||||||
|
- Normalization summary artifacts
|
||||||
|
- Authoritative report.json in run directory
|
||||||
|
- Error logs on failure
|
||||||
|
- Basic retention policy implementation
|
||||||
|
|
||||||
|
### Not Yet Implemented (Future Phases):
|
||||||
|
|
||||||
|
- Chunking and token-bounded section building
|
||||||
|
- LLM proposal generation and structured output
|
||||||
|
- Validator chains and decision logging
|
||||||
|
- Module execution pipeline
|
||||||
|
- Full diagnostics retention policy
|
||||||
|
- Skipped correction detection for auto retention
|
||||||
|
- Real module implementations (glossary, homophones, spoken_word, grammar)
|
||||||
|
|
||||||
|
### Compatibility Notes:
|
||||||
|
|
||||||
|
The Go implementation maintains the public CLI contract from the Python implementation:
|
||||||
|
- `audita process transcript.json --glossary glossary.yaml --output corrected.json`
|
||||||
|
- stdout contains transcript JSON when --output is omitted
|
||||||
|
- stderr contains logs and errors only
|
||||||
|
- --report-json writes machine-readable reports
|
||||||
|
- Nonzero exit codes on failure
|
||||||
|
|
||||||
|
The normalization behavior is deterministic and matches the architectural specifications, but may differ from Python-era behavior in edge cases until full module implementation is complete.
|
||||||
|
|
||||||
## When to consider an HTTP API
|
## When to consider an HTTP API
|
||||||
|
|
||||||
|
|||||||
@@ -1097,38 +1097,107 @@ func TestRunProcessFailedRunsRetained(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunProcessSuccessfulRunRetention(t *testing.T) {
|
func TestRunProcessCLIAndNormalizationAgree(t *testing.T) {
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
transcriptJSON := `[{"id": 1, "speaker": "Alice", "start": 0.0, "end": 1.0, "text": "Hello"}]`
|
transcriptJSON := `[
|
||||||
|
{"id": 1, "speaker": "Alice", "start": 0.0, "end": 1.0, "text": "Hello"},
|
||||||
|
{"id": 2, "speaker": "Alice", "start": 1.5, "end": 2.5, "text": "world"}
|
||||||
|
]`
|
||||||
|
|
||||||
transcriptPath := filepath.Join(t.TempDir(), "transcript.json")
|
transcriptPath := filepath.Join(t.TempDir(), "transcript.json")
|
||||||
glossaryPath := fixturePath("tiny_glossary.yaml")
|
glossaryPath := fixturePath("tiny_glossary.yaml")
|
||||||
workDir := filepath.Join(t.TempDir(), "work")
|
|
||||||
|
|
||||||
if err := os.WriteFile(transcriptPath, []byte(transcriptJSON), 0644); err != nil {
|
if err := os.WriteFile(transcriptPath, []byte(transcriptJSON), 0644); err != nil {
|
||||||
t.Fatalf("failed to create test transcript: %v", err)
|
t.Fatalf("failed to create test transcript: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with "never" retention - successful runs should be removed
|
exitCode := Run([]string{"process", transcriptPath, "--glossary", glossaryPath}, &stdout, &stderr)
|
||||||
exitCode := Run([]string{
|
|
||||||
"process",
|
|
||||||
transcriptPath,
|
|
||||||
"--glossary",
|
|
||||||
glossaryPath,
|
|
||||||
"--work-dir",
|
|
||||||
workDir,
|
|
||||||
"--work-dir-retention",
|
|
||||||
"never",
|
|
||||||
}, &stdout, &stderr)
|
|
||||||
if exitCode != 0 {
|
if exitCode != 0 {
|
||||||
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// With "never" retention, successful runs should be removed
|
// Parse CLI output
|
||||||
runDirs, err := os.ReadDir(workDir)
|
var cliOutput schema.Transcript
|
||||||
if err == nil && len(runDirs) > 0 {
|
if err := schema.ParseTranscriptJSON(stdout.Bytes()); err != nil {
|
||||||
t.Fatalf("expected successful run to be removed with 'never' retention, found %d directories", len(runDirs))
|
t.Fatalf("failed to parse CLI output: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse source transcript
|
||||||
|
var sourceTranscript schema.SourceTranscript
|
||||||
|
if err := schema.ParseSourceTranscriptJSON([]byte(transcriptJSON)); err != nil {
|
||||||
|
t.Fatalf("failed to parse source transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to canonical format
|
||||||
|
canonical := &schema.Transcript{
|
||||||
|
Segments: make([]schema.Segment, len(sourceTranscript.Segments)),
|
||||||
|
}
|
||||||
|
for i, s := range sourceTranscript.Segments {
|
||||||
|
id := i + 1
|
||||||
|
if s.ID != nil {
|
||||||
|
id = *s.ID
|
||||||
|
}
|
||||||
|
canonical.Segments[i] = schema.Segment{
|
||||||
|
ID: id,
|
||||||
|
Speaker: s.Speaker,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Text: s.Text,
|
||||||
|
Categories: s.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply normalization using pure package
|
||||||
|
normalizer := normalization.NewNormalizer(normalization.NormalizationConfig{
|
||||||
|
MaxSegmentGap: 2.0,
|
||||||
|
EllipsisGap: 1.0,
|
||||||
|
MaxSegmentDuration: 60.0,
|
||||||
|
MaxSegmentTokens: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
packageOutput, _ := normalizer.Normalize(canonical)
|
||||||
|
|
||||||
|
// Compare CLI and package outputs
|
||||||
|
assertTranscriptsEqual(t, &cliOutput, packageOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertTranscriptsEqual(t *testing.T, actual, expected *schema.Transcript) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if len(actual.Segments) != len(expected.Segments) {
|
||||||
|
t.Errorf("segment count mismatch: expected %d, got %d", len(expected.Segments), len(actual.Segments))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range actual.Segments {
|
||||||
|
a := actual.Segments[i]
|
||||||
|
e := expected.Segments[i]
|
||||||
|
|
||||||
|
if a.ID != e.ID {
|
||||||
|
t.Errorf("segment %d: ID mismatch: expected %d, got %d", i, e.ID, a.ID)
|
||||||
|
}
|
||||||
|
if a.Speaker != e.Speaker {
|
||||||
|
t.Errorf("segment %d: speaker mismatch: expected %q, got %q", i, e.Speaker, a.Speaker)
|
||||||
|
}
|
||||||
|
if a.Start != e.Start {
|
||||||
|
t.Errorf("segment %d: start mismatch: expected %f, got %f", i, e.Start, a.Start)
|
||||||
|
}
|
||||||
|
if a.End != e.End {
|
||||||
|
t.Errorf("segment %d: end mismatch: expected %f, got %f", i, e.End, a.End)
|
||||||
|
}
|
||||||
|
if a.Text != e.Text {
|
||||||
|
t.Errorf("segment %d: text mismatch: expected %q, got %q", i, e.Text, a.Text)
|
||||||
|
}
|
||||||
|
if len(a.Categories) != len(e.Categories) {
|
||||||
|
t.Errorf("segment %d: categories count mismatch: expected %d, got %d", i, len(e.Categories), len(a.Categories))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j, cat := range a.Categories {
|
||||||
|
if j >= len(e.Categories) || cat != e.Categories[j] {
|
||||||
|
t.Errorf("segment %d: category %d mismatch: expected %q, got %q", i, j, e.Categories[j], cat)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ func TestNormalizationCategoryPreservation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizationSummaryCounts(t *testing.T) {
|
func TestNormalizationGoldenBareArrayTranscript(t *testing.T) {
|
||||||
config := NormalizationConfig{
|
config := NormalizationConfig{
|
||||||
MaxSegmentGap: 2.0,
|
MaxSegmentGap: 2.0,
|
||||||
EllipsisGap: 1.0,
|
EllipsisGap: 1.0,
|
||||||
@@ -280,6 +280,408 @@ func TestNormalizationSummaryCounts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
normalizer := NewNormalizer(config)
|
normalizer := NewNormalizer(config)
|
||||||
|
|
||||||
|
raw, err := os.ReadFile("testdata/bare_array_transcript.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read test fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript, err := schema.ParseSourceTranscriptJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse source transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := &schema.Transcript{
|
||||||
|
Segments: make([]schema.Segment, len(transcript.Segments)),
|
||||||
|
}
|
||||||
|
for i, s := range transcript.Segments {
|
||||||
|
id := i + 1
|
||||||
|
if s.ID != nil {
|
||||||
|
id = *s.ID
|
||||||
|
}
|
||||||
|
converted.Segments[i] = schema.Segment{
|
||||||
|
ID: id,
|
||||||
|
Speaker: s.Speaker,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Text: s.Text,
|
||||||
|
Categories: s.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, _ := normalizer.Normalize(converted)
|
||||||
|
|
||||||
|
// Compare with golden output semantically
|
||||||
|
expectedRaw, err := os.ReadFile("testdata/bare_array_transcript.golden.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read golden fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTranscript schema.Transcript
|
||||||
|
if err := schema.ParseTranscriptJSON(expectedRaw); err != nil {
|
||||||
|
t.Fatalf("failed to parse golden transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTranscriptsEqual(t, normalized, &expectedTranscript)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizationGoldenObjectWithSegmentsTranscript(t *testing.T) {
|
||||||
|
config := NormalizationConfig{
|
||||||
|
MaxSegmentGap: 2.0,
|
||||||
|
EllipsisGap: 1.0,
|
||||||
|
MaxSegmentDuration: 60.0,
|
||||||
|
MaxSegmentTokens: 100,
|
||||||
|
}
|
||||||
|
normalizer := NewNormalizer(config)
|
||||||
|
|
||||||
|
raw, err := os.ReadFile("testdata/object_with_segments_transcript.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read test fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript, err := schema.ParseSourceTranscriptJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse source transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := &schema.Transcript{
|
||||||
|
Segments: make([]schema.Segment, len(transcript.Segments)),
|
||||||
|
}
|
||||||
|
for i, s := range transcript.Segments {
|
||||||
|
id := i + 1
|
||||||
|
if s.ID != nil {
|
||||||
|
id = *s.ID
|
||||||
|
}
|
||||||
|
converted.Segments[i] = schema.Segment{
|
||||||
|
ID: id,
|
||||||
|
Speaker: s.Speaker,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Text: s.Text,
|
||||||
|
Categories: s.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, _ := normalizer.Normalize(converted)
|
||||||
|
|
||||||
|
// Compare with golden output semantically
|
||||||
|
expectedRaw, err := os.ReadFile("testdata/object_with_segments_transcript.golden.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read golden fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTranscript schema.Transcript
|
||||||
|
if err := schema.ParseTranscriptJSON(expectedRaw); err != nil {
|
||||||
|
t.Fatalf("failed to parse golden transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTranscriptsEqual(t, normalized, &expectedTranscript)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizationGoldenTranscriptWithCategories(t *testing.T) {
|
||||||
|
config := NormalizationConfig{
|
||||||
|
MaxSegmentGap: 2.0,
|
||||||
|
EllipsisGap: 1.0,
|
||||||
|
MaxSegmentDuration: 60.0,
|
||||||
|
MaxSegmentTokens: 100,
|
||||||
|
}
|
||||||
|
normalizer := NewNormalizer(config)
|
||||||
|
|
||||||
|
raw, err := os.ReadFile("testdata/transcript_with_categories.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read test fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript, err := schema.ParseSourceTranscriptJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse source transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := &schema.Transcript{
|
||||||
|
Segments: make([]schema.Segment, len(transcript.Segments)),
|
||||||
|
}
|
||||||
|
for i, s := range transcript.Segments {
|
||||||
|
id := i + 1
|
||||||
|
if s.ID != nil {
|
||||||
|
id = *s.ID
|
||||||
|
}
|
||||||
|
converted.Segments[i] = schema.Segment{
|
||||||
|
ID: id,
|
||||||
|
Speaker: s.Speaker,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Text: s.Text,
|
||||||
|
Categories: s.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, _ := normalizer.Normalize(converted)
|
||||||
|
|
||||||
|
// Compare with golden output semantically
|
||||||
|
expectedRaw, err := os.ReadFile("testdata/transcript_with_categories.golden.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read golden fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTranscript schema.Transcript
|
||||||
|
if err := schema.ParseTranscriptJSON(expectedRaw); err != nil {
|
||||||
|
t.Fatalf("failed to parse golden transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTranscriptsEqual(t, normalized, &expectedTranscript)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizationGoldenTranscriptWithOriginalIDs(t *testing.T) {
|
||||||
|
config := NormalizationConfig{
|
||||||
|
MaxSegmentGap: 2.0,
|
||||||
|
EllipsisGap: 1.0,
|
||||||
|
MaxSegmentDuration: 60.0,
|
||||||
|
MaxSegmentTokens: 100,
|
||||||
|
}
|
||||||
|
normalizer := NewNormalizer(config)
|
||||||
|
|
||||||
|
raw, err := os.ReadFile("testdata/transcript_with_original_ids.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read test fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript, err := schema.ParseSourceTranscriptJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse source transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := &schema.Transcript{
|
||||||
|
Segments: make([]schema.Segment, len(transcript.Segments)),
|
||||||
|
}
|
||||||
|
for i, s := range transcript.Segments {
|
||||||
|
id := i + 1
|
||||||
|
if s.ID != nil {
|
||||||
|
id = *s.ID
|
||||||
|
}
|
||||||
|
converted.Segments[i] = schema.Segment{
|
||||||
|
ID: id,
|
||||||
|
Speaker: s.Speaker,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Text: s.Text,
|
||||||
|
Categories: s.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, _ := normalizer.Normalize(converted)
|
||||||
|
|
||||||
|
// Compare with golden output semantically
|
||||||
|
expectedRaw, err := os.ReadFile("testdata/transcript_with_original_ids.golden.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read golden fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTranscript schema.Transcript
|
||||||
|
if err := schema.ParseTranscriptJSON(expectedRaw); err != nil {
|
||||||
|
t.Fatalf("failed to parse golden transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTranscriptsEqual(t, normalized, &expectedTranscript)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizationGoldenSameSpeakerMerge(t *testing.T) {
|
||||||
|
config := NormalizationConfig{
|
||||||
|
MaxSegmentGap: 2.0,
|
||||||
|
EllipsisGap: 1.0,
|
||||||
|
MaxSegmentDuration: 60.0,
|
||||||
|
MaxSegmentTokens: 100,
|
||||||
|
}
|
||||||
|
normalizer := NewNormalizer(config)
|
||||||
|
|
||||||
|
raw, err := os.ReadFile("testdata/transcript_same_speaker_merge.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read test fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript, err := schema.ParseSourceTranscriptJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse source transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := &schema.Transcript{
|
||||||
|
Segments: make([]schema.Segment, len(transcript.Segments)),
|
||||||
|
}
|
||||||
|
for i, s := range transcript.Segments {
|
||||||
|
id := i + 1
|
||||||
|
if s.ID != nil {
|
||||||
|
id = *s.ID
|
||||||
|
}
|
||||||
|
converted.Segments[i] = schema.Segment{
|
||||||
|
ID: id,
|
||||||
|
Speaker: s.Speaker,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Text: s.Text,
|
||||||
|
Categories: s.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, _ := normalizer.Normalize(converted)
|
||||||
|
|
||||||
|
// Compare with golden output semantically
|
||||||
|
expectedRaw, err := os.ReadFile("testdata/transcript_same_speaker_merge.golden.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read golden fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTranscript schema.Transcript
|
||||||
|
if err := schema.ParseTranscriptJSON(expectedRaw); err != nil {
|
||||||
|
t.Fatalf("failed to parse golden transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTranscriptsEqual(t, normalized, &expectedTranscript)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizationGoldenEllipsisMerge(t *testing.T) {
|
||||||
|
config := NormalizationConfig{
|
||||||
|
MaxSegmentGap: 2.0,
|
||||||
|
EllipsisGap: 1.0,
|
||||||
|
MaxSegmentDuration: 60.0,
|
||||||
|
MaxSegmentTokens: 100,
|
||||||
|
}
|
||||||
|
normalizer := NewNormalizer(config)
|
||||||
|
|
||||||
|
raw, err := os.ReadFile("testdata/transcript_ellipsis_merge.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read test fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript, err := schema.ParseSourceTranscriptJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse source transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := &schema.Transcript{
|
||||||
|
Segments: make([]schema.Segment, len(transcript.Segments)),
|
||||||
|
}
|
||||||
|
for i, s := range transcript.Segments {
|
||||||
|
id := i + 1
|
||||||
|
if s.ID != nil {
|
||||||
|
id = *s.ID
|
||||||
|
}
|
||||||
|
converted.Segments[i] = schema.Segment{
|
||||||
|
ID: id,
|
||||||
|
Speaker: s.Speaker,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Text: s.Text,
|
||||||
|
Categories: s.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, _ := normalizer.Normalize(converted)
|
||||||
|
|
||||||
|
// Compare with golden output semantically
|
||||||
|
expectedRaw, err := os.ReadFile("testdata/transcript_ellipsis_merge.golden.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read golden fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTranscript schema.Transcript
|
||||||
|
if err := schema.ParseTranscriptJSON(expectedRaw); err != nil {
|
||||||
|
t.Fatalf("failed to parse golden transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTranscriptsEqual(t, normalized, &expectedTranscript)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizationGoldenDifferentSpeakersNoMerge(t *testing.T) {
|
||||||
|
config := NormalizationConfig{
|
||||||
|
MaxSegmentGap: 2.0,
|
||||||
|
EllipsisGap: 1.0,
|
||||||
|
MaxSegmentDuration: 60.0,
|
||||||
|
MaxSegmentTokens: 100,
|
||||||
|
}
|
||||||
|
normalizer := NewNormalizer(config)
|
||||||
|
|
||||||
|
raw, err := os.ReadFile("testdata/transcript_different_speakers_no_merge.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read test fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transcript, err := schema.ParseSourceTranscriptJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse source transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := &schema.Transcript{
|
||||||
|
Segments: make([]schema.Segment, len(transcript.Segments)),
|
||||||
|
}
|
||||||
|
for i, s := range transcript.Segments {
|
||||||
|
id := i + 1
|
||||||
|
if s.ID != nil {
|
||||||
|
id = *s.ID
|
||||||
|
}
|
||||||
|
converted.Segments[i] = schema.Segment{
|
||||||
|
ID: id,
|
||||||
|
Speaker: s.Speaker,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Text: s.Text,
|
||||||
|
Categories: s.Categories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized, _ := normalizer.Normalize(converted)
|
||||||
|
|
||||||
|
// Compare with golden output semantically
|
||||||
|
expectedRaw, err := os.ReadFile("testdata/transcript_different_speakers_no_merge.golden.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read golden fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTranscript schema.Transcript
|
||||||
|
if err := schema.ParseTranscriptJSON(expectedRaw); err != nil {
|
||||||
|
t.Fatalf("failed to parse golden transcript: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTranscriptsEqual(t, normalized, &expectedTranscript)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertTranscriptsEqual(t *testing.T, actual, expected *schema.Transcript) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if len(actual.Segments) != len(expected.Segments) {
|
||||||
|
t.Errorf("segment count mismatch: expected %d, got %d", len(expected.Segments), len(actual.Segments))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range actual.Segments {
|
||||||
|
a := actual.Segments[i]
|
||||||
|
e := expected.Segments[i]
|
||||||
|
|
||||||
|
if a.ID != e.ID {
|
||||||
|
t.Errorf("segment %d: ID mismatch: expected %d, got %d", i, e.ID, a.ID)
|
||||||
|
}
|
||||||
|
if a.Speaker != e.Speaker {
|
||||||
|
t.Errorf("segment %d: speaker mismatch: expected %q, got %q", i, e.Speaker, a.Speaker)
|
||||||
|
}
|
||||||
|
if a.Start != e.Start {
|
||||||
|
t.Errorf("segment %d: start mismatch: expected %f, got %f", i, e.Start, a.Start)
|
||||||
|
}
|
||||||
|
if a.End != e.End {
|
||||||
|
t.Errorf("segment %d: end mismatch: expected %f, got %f", i, e.End, a.End)
|
||||||
|
}
|
||||||
|
if a.Text != e.Text {
|
||||||
|
t.Errorf("segment %d: text mismatch: expected %q, got %q", i, e.Text, a.Text)
|
||||||
|
}
|
||||||
|
if len(a.Categories) != len(e.Categories) {
|
||||||
|
t.Errorf("segment %d: categories count mismatch: expected %d, got %d", i, len(e.Categories), len(a.Categories))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j, cat := range a.Categories {
|
||||||
|
if j >= len(e.Categories) || cat != e.Categories[j] {
|
||||||
|
t.Errorf("segment %d: category %d mismatch: expected %q, got %q", i, j, e.Categories[j], cat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizer := NewNormalizer(config)
|
||||||
|
|
||||||
transcript := &schema.Transcript{
|
transcript := &schema.Transcript{
|
||||||
Segments: []schema.Segment{
|
Segments: []schema.Segment{
|
||||||
{ID: 1, Speaker: "Alice", Start: 0.0, End: 1.0, Text: "Hello"},
|
{ID: 1, Speaker: "Alice", Start: 0.0, End: 1.0, Text: "Hello"},
|
||||||
|
|||||||
19
internal/core/normalization/testdata/bare_array_transcript.golden.json
vendored
Normal file
19
internal/core/normalization/testdata/bare_array_transcript.golden.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.5,
|
||||||
|
"text": "Hello world."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Bob",
|
||||||
|
"start": 2.0,
|
||||||
|
"end": 3.5,
|
||||||
|
"text": "Hi there.",
|
||||||
|
"categories": [
|
||||||
|
"greeting"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
17
internal/core/normalization/testdata/bare_array_transcript.json
vendored
Normal file
17
internal/core/normalization/testdata/bare_array_transcript.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.5,
|
||||||
|
"text": "Hello world."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Bob",
|
||||||
|
"start": 2.0,
|
||||||
|
"end": 3.5,
|
||||||
|
"text": "Hi there.",
|
||||||
|
"categories": ["greeting"]
|
||||||
|
}
|
||||||
|
]
|
||||||
19
internal/core/normalization/testdata/comprehensive_glossary.yaml
vendored
Normal file
19
internal/core/normalization/testdata/comprehensive_glossary.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
glossary:
|
||||||
|
- name: Jesters
|
||||||
|
category: faction
|
||||||
|
summary: A faction name.
|
||||||
|
aliases:
|
||||||
|
- Jester
|
||||||
|
- Jest
|
||||||
|
- name: Popov
|
||||||
|
category: character
|
||||||
|
summary: A character name.
|
||||||
|
aliases:
|
||||||
|
- Hrank
|
||||||
|
- Pop
|
||||||
|
- name: Audita
|
||||||
|
category: system
|
||||||
|
summary: The transcript polishing system.
|
||||||
|
aliases:
|
||||||
|
- Audit
|
||||||
|
- Auditor
|
||||||
19
internal/core/normalization/testdata/object_with_segments_transcript.golden.json
vendored
Normal file
19
internal/core/normalization/testdata/object_with_segments_transcript.golden.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.5,
|
||||||
|
"text": "Hello world."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Bob",
|
||||||
|
"start": 2.0,
|
||||||
|
"end": 3.5,
|
||||||
|
"text": "Hi there.",
|
||||||
|
"categories": [
|
||||||
|
"greeting"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
19
internal/core/normalization/testdata/object_with_segments_transcript.json
vendored
Normal file
19
internal/core/normalization/testdata/object_with_segments_transcript.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"segments": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.5,
|
||||||
|
"text": "Hello world."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Bob",
|
||||||
|
"start": 2.0,
|
||||||
|
"end": 3.5,
|
||||||
|
"text": "Hi there.",
|
||||||
|
"categories": ["greeting"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
internal/core/normalization/testdata/transcript_different_speakers_no_merge.golden.json
vendored
Normal file
16
internal/core/normalization/testdata/transcript_different_speakers_no_merge.golden.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.0,
|
||||||
|
"text": "Hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Bob",
|
||||||
|
"start": 1.5,
|
||||||
|
"end": 2.5,
|
||||||
|
"text": "Hi"
|
||||||
|
}
|
||||||
|
]
|
||||||
16
internal/core/normalization/testdata/transcript_different_speakers_no_merge.json
vendored
Normal file
16
internal/core/normalization/testdata/transcript_different_speakers_no_merge.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.0,
|
||||||
|
"text": "Hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Bob",
|
||||||
|
"start": 1.5,
|
||||||
|
"end": 2.5,
|
||||||
|
"text": "Hi"
|
||||||
|
}
|
||||||
|
]
|
||||||
9
internal/core/normalization/testdata/transcript_ellipsis_merge.golden.json
vendored
Normal file
9
internal/core/normalization/testdata/transcript_ellipsis_merge.golden.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 3.0,
|
||||||
|
"text": "Hello... world"
|
||||||
|
}
|
||||||
|
]
|
||||||
16
internal/core/normalization/testdata/transcript_ellipsis_merge.json
vendored
Normal file
16
internal/core/normalization/testdata/transcript_ellipsis_merge.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.0,
|
||||||
|
"text": "Hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 2.0,
|
||||||
|
"end": 3.0,
|
||||||
|
"text": "world"
|
||||||
|
}
|
||||||
|
]
|
||||||
9
internal/core/normalization/testdata/transcript_same_speaker_merge.golden.json
vendored
Normal file
9
internal/core/normalization/testdata/transcript_same_speaker_merge.golden.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 2.5,
|
||||||
|
"text": "Hello world"
|
||||||
|
}
|
||||||
|
]
|
||||||
16
internal/core/normalization/testdata/transcript_same_speaker_merge.json
vendored
Normal file
16
internal/core/normalization/testdata/transcript_same_speaker_merge.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.0,
|
||||||
|
"text": "Hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 1.2,
|
||||||
|
"end": 2.2,
|
||||||
|
"text": "world"
|
||||||
|
}
|
||||||
|
]
|
||||||
24
internal/core/normalization/testdata/transcript_with_categories.golden.json
vendored
Normal file
24
internal/core/normalization/testdata/transcript_with_categories.golden.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.0,
|
||||||
|
"text": "Hello world.",
|
||||||
|
"categories": [
|
||||||
|
"greeting",
|
||||||
|
"opening"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Bob",
|
||||||
|
"start": 2.0,
|
||||||
|
"end": 3.0,
|
||||||
|
"text": "Hi there.",
|
||||||
|
"categories": [
|
||||||
|
"response",
|
||||||
|
"opening"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
18
internal/core/normalization/testdata/transcript_with_categories.json
vendored
Normal file
18
internal/core/normalization/testdata/transcript_with_categories.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.0,
|
||||||
|
"text": "Hello world.",
|
||||||
|
"categories": ["greeting", "opening"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"speaker": "Bob",
|
||||||
|
"start": 2.0,
|
||||||
|
"end": 3.0,
|
||||||
|
"text": "Hi there.",
|
||||||
|
"categories": ["response", "opening"]
|
||||||
|
}
|
||||||
|
]
|
||||||
9
internal/core/normalization/testdata/transcript_with_original_ids.golden.json
vendored
Normal file
9
internal/core/normalization/testdata/transcript_with_original_ids.golden.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 2.5,
|
||||||
|
"text": "Hello world"
|
||||||
|
}
|
||||||
|
]
|
||||||
16
internal/core/normalization/testdata/transcript_with_original_ids.json
vendored
Normal file
16
internal/core/normalization/testdata/transcript_with_original_ids.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 0.0,
|
||||||
|
"end": 1.0,
|
||||||
|
"text": "Hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"speaker": "Alice",
|
||||||
|
"start": 1.5,
|
||||||
|
"end": 2.5,
|
||||||
|
"text": "world"
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user