Allow checkpoints for empty audio accepted by WhisperX

This commit is contained in:
2026-09-22 00:53:28 +00:00
parent 54d335c820
commit 5dca34e0e0
5 changed files with 76 additions and 9 deletions

View File

@@ -18,6 +18,57 @@ import (
"gitea.maximumdirect.net/eric/narratio/internal/stage"
)
func TestExecuteStagesDelegatesEmptyAudioAcceptanceToWhisperX(t *testing.T) {
for _, rejected := range []bool{false, true} {
name := "accepted"
if rejected {
name = "rejected"
}
t.Run(name, func(t *testing.T) {
cfg, store := setupTranscribeRecoveryFixture(t, []string{"alice"})
paths := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root).SessionPathsFor(cfg.Session.Campaign, cfg.Session.SessionID)
if err := os.WriteFile(filepath.Join(paths.AudioDir, "alice.flac"), nil, 0o644); err != nil {
t.Fatal(err)
}
fake := &whisperx.FakeClient{}
if rejected {
fake.Err = errors.New("service rejected audio")
}
transcribe := mustSelectStage(t, "transcribe")
_, err := executeStages(context.Background(), cfg, []stage.Stage{transcribe}, RunOptions{
Env: &Env{ManifestStore: store, WhisperX: fake},
})
if !errors.Is(err, fake.Err) {
t.Fatalf("transcribe error = %v, want %v", err, fake.Err)
}
if len(fake.RequestsSnapshot()) != 1 {
t.Fatal("empty audio was not sent to service")
}
m := loadTranscribeRecoveryManifest(t, store, cfg)
if rejected {
if len(m.Stages["transcribe"].TranscribeSpeakers) != 0 {
t.Fatal("rejected audio was checkpointed")
}
return
}
record, ok := m.Stages["transcribe"].TranscribeSpeakers["alice"]
if !ok || record.AudioSize != 0 {
t.Fatalf("missing zero-size checkpoint: %#v", record)
}
retry := &whisperx.FakeClient{}
summary, err := executeStages(context.Background(), cfg, []stage.Stage{transcribe}, RunOptions{
Env: &Env{ManifestStore: store, WhisperX: retry},
})
if err != nil {
t.Fatal(err)
}
if len(summary.Skipped) != 1 || len(retry.RequestsSnapshot()) != 0 {
t.Fatal("successful empty-audio result was not reused")
}
})
}
}
func TestExecuteStagesRetriesOnlyUncommittedTranscribeSpeakers(t *testing.T) {
cfg, baseStore := setupTranscribeRecoveryFixture(t, []string{"a", "b", "c", "d"})
concurrency := 1

View File

@@ -92,8 +92,8 @@ func validateTranscribeSpeakerRecord(speakerID string, record TranscribeSpeakerR
if err := validateSHA256("audio_sha256", record.AudioSHA256); err != nil {
return err
}
if record.AudioSize <= 0 {
return fmt.Errorf("audio_size must be positive")
if record.AudioSize < 0 {
return fmt.Errorf("audio_size must not be negative")
}
if record.Output.Kind != "transcript_raw" {
return fmt.Errorf("output kind %q must be transcript_raw", record.Output.Kind)

View File

@@ -12,6 +12,8 @@ import (
func TestTranscribeSpeakerStateRoundTripsPopulatedAndVersionedEmptyCollections(t *testing.T) {
now := time.Date(2026, 9, 21, 12, 0, 0, 0, time.UTC)
store := &LocalStore{}
zeroAudio := validTranscribeSpeakerRecord("Alice Table 1")
zeroAudio.AudioSize = 0
for _, test := range []struct {
name string
@@ -24,6 +26,7 @@ func TestTranscribeSpeakerStateRoundTripsPopulatedAndVersionedEmptyCollections(t
},
},
{name: "versioned empty", records: map[string]TranscribeSpeakerRecord{}},
{name: "zero size audio", records: map[string]TranscribeSpeakerRecord{"Alice Table 1": zeroAudio}},
} {
t.Run(test.name, func(t *testing.T) {
m := New("session", now)
@@ -49,6 +52,9 @@ func TestTranscribeSpeakerStateRoundTripsPopulatedAndVersionedEmptyCollections(t
}
if len(test.records) != 0 {
got := stage.TranscribeSpeakers["Alice Table 1"]
if got.AudioSize != test.records["Alice Table 1"].AudioSize {
t.Fatalf("audio size = %d, want %d", got.AudioSize, test.records["Alice Table 1"].AudioSize)
}
if got.AudioPath != "inputs/audio/Alice Table 1.flac" || got.Output.LocalPath != "transcripts/raw/Alice Table 1.json" {
t.Fatalf("round-tripped record = %#v", got)
}
@@ -99,7 +105,7 @@ func TestValidateTranscribeSpeakerCollectionRejectsMalformedEvidence(t *testing.
{name: "unsafe audio path", speakerID: "alice", mutate: func(r *TranscribeSpeakerRecord) { r.AudioPath = "../alice.flac" }, want: "audio_path is unsafe"},
{name: "noncanonical audio path", speakerID: "alice", mutate: func(r *TranscribeSpeakerRecord) { r.AudioPath = "inputs/audio/../alice.flac" }, want: "not canonical"},
{name: "bad audio checksum", speakerID: "alice", mutate: func(r *TranscribeSpeakerRecord) { r.AudioSHA256 = strings.Repeat("G", 64) }, want: "audio_sha256 must be"},
{name: "bad audio size", speakerID: "alice", mutate: func(r *TranscribeSpeakerRecord) { r.AudioSize = 0 }, want: "audio_size must be positive"},
{name: "negative audio size", speakerID: "alice", mutate: func(r *TranscribeSpeakerRecord) { r.AudioSize = -1 }, want: "audio_size"},
{name: "bad output kind", speakerID: "alice", mutate: func(r *TranscribeSpeakerRecord) { r.Output.Kind = "transcript" }, want: "must be transcript_raw"},
{name: "unsafe output path", speakerID: "alice", mutate: func(r *TranscribeSpeakerRecord) { r.Output.LocalPath = "/tmp/alice.json" }, want: "output local_path is unsafe"},
{name: "bad output checksum", speakerID: "alice", mutate: func(r *TranscribeSpeakerRecord) { r.Output.Checksum = "bad" }, want: "output checksum must be"},