Repair stale transcription outputs when resuming
This commit is contained in:
@@ -222,6 +222,14 @@ func TestTranscribeResumeValidationUsesSpeakerEvidence(t *testing.T) {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
validation, err := (transcribeStage{}).ValidateResume(context.Background(), env, m)
|
||||
if err != nil || validation.Resumable {
|
||||
t.Fatalf("ValidateResume() = %#v, %v, want missing aggregate repaired", validation, err)
|
||||
}
|
||||
if len(m.Stages["transcribe"].Outputs) != 0 {
|
||||
t.Fatal("resume inspection mutated aggregate outputs")
|
||||
}
|
||||
m.Stages["transcribe"].Outputs = []manifest.ArtifactRecord{m.Stages["transcribe"].TranscribeSpeakers["alice"].Output}
|
||||
validation, err = (transcribeStage{}).ValidateResume(context.Background(), env, m)
|
||||
if err != nil || !validation.Resumable {
|
||||
t.Fatalf("ValidateResume() = %#v, %v, want resumable", validation, err)
|
||||
}
|
||||
@@ -280,6 +288,7 @@ func TestTranscribeReuseDoesNotDependOnRunDirectory(t *testing.T) {
|
||||
if _, err := (transcribeStage{}).Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
m.Stages["transcribe"].Outputs = []manifest.ArtifactRecord{m.Stages["transcribe"].TranscribeSpeakers["alice"].Output}
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
if err := os.RemoveAll(paths.RunsDir); err != nil {
|
||||
t.Fatalf("remove run directory: %v", err)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -44,6 +45,29 @@ func TestReconcileTranscribeSpeakersReusesUnchangedCompletedEvidence(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscribeResumeAcceptsEquivalentOutputsAndInspectsRemovalReadOnly(t *testing.T) {
|
||||
env, m := setupCurrentTranscribeReconciliation(t, []string{"alice.flac", "bob.flac"})
|
||||
record := m.Stages["transcribe"]
|
||||
record.Status = manifest.StatusSucceeded
|
||||
// Reverse speaker order and mix the supported local path representations.
|
||||
bob := record.TranscribeSpeakers["bob"].Output
|
||||
bob.LocalPath = filepath.Join(sessionPathsForEnv(env, m.SessionID).Root, filepath.FromSlash(bob.LocalPath))
|
||||
record.Outputs = []manifest.ArtifactRecord{bob, record.TranscribeSpeakers["alice"].Output}
|
||||
validation, err := (transcribeStage{}).ValidateResume(context.Background(), env, m)
|
||||
if err != nil || !validation.Resumable {
|
||||
t.Fatalf("equivalent aggregate: %#v, %v", validation, err)
|
||||
}
|
||||
m.Inputs = slices.DeleteFunc(m.Inputs, func(in manifest.InputRecord) bool { return filepath.Base(in.Path) == "bob.flac" })
|
||||
before := cloneManifestForTranscribeCheckpoint(m)
|
||||
validation, err = (transcribeStage{}).ValidateResume(context.Background(), env, m)
|
||||
if err != nil || validation.Resumable {
|
||||
t.Fatalf("removed speaker: %#v, %v", validation, err)
|
||||
}
|
||||
if !reflect.DeepEqual(m, before) {
|
||||
t.Fatal("resume inspection mutated manifest")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileTranscribeSpeakersReplacesOnlyChangedAudio(t *testing.T) {
|
||||
env, m := setupCurrentTranscribeReconciliation(t, []string{"alice.flac", "bob.flac"})
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,10 @@ func (transcribeStage) ValidateResume(
|
||||
return ResumeValidation{}, fmt.Errorf("transcribe resume: reconcile speakers: %w", err)
|
||||
}
|
||||
if len(reconciliation.work) == 0 {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
if !transcribeAggregateCurrent(m.Stages["transcribe"], reconciliation.current, paths) {
|
||||
return NonResumable("speaker collection or aggregate outputs require reconciliation"), nil
|
||||
}
|
||||
return Resumable(), nil
|
||||
}
|
||||
speakers := make([]string, 0, len(reconciliation.work))
|
||||
@@ -29,4 +34,27 @@ func (transcribeStage) ValidateResume(
|
||||
return NonResumable("speaker transcripts require execution: " + strings.Join(speakers, ", ")), nil
|
||||
}
|
||||
|
||||
// No HTTP work does not imply that the aggregate is current. The reconciled
|
||||
// collection is a subset of the stored records, so its cardinality also detects
|
||||
// removed speakers. Compare local output evidence independently of list order
|
||||
// and of the supported absolute/session-relative path representations.
|
||||
func transcribeAggregateCurrent(record *manifest.StageRecord, current map[string]manifest.TranscribeSpeakerRecord, paths artifacts.SessionPaths) bool {
|
||||
if len(record.TranscribeSpeakers) != len(current) || len(record.Outputs) != len(current) {
|
||||
return false
|
||||
}
|
||||
expected := make(map[string]manifest.ArtifactRecord, len(current))
|
||||
for _, speaker := range current {
|
||||
expected[artifacts.ResolveSessionLocalPathForRead(paths, speaker.Output.LocalPath)] = speaker.Output
|
||||
}
|
||||
for _, output := range record.Outputs {
|
||||
path := artifacts.ResolveSessionLocalPathForRead(paths, output.LocalPath)
|
||||
want, ok := expected[path]
|
||||
if !ok || output.Kind != want.Kind || output.Checksum != want.Checksum || output.ProducerRunID != want.ProducerRunID {
|
||||
return false
|
||||
}
|
||||
delete(expected, path)
|
||||
}
|
||||
return len(expected) == 0
|
||||
}
|
||||
|
||||
var _ ResumeValidator = transcribeStage{}
|
||||
|
||||
Reference in New Issue
Block a user