Checkpoint speaker transcriptions incrementally

This commit is contained in:
2026-09-21 16:20:53 +00:00
parent 26aa0cce29
commit 2ae63dbc2b
8 changed files with 793 additions and 70 deletions

View File

@@ -1082,7 +1082,7 @@ func TestExecuteStagesReportsSkippedStage(t *testing.T) {
t.Fatalf("Save manifest error = %v", err)
}
summary, err := executeStages(context.Background(), cfg, []stage.Stage{BuildFullPlan()[1]}, RunOptions{})
summary, err := executeStages(context.Background(), cfg, []stage.Stage{resultStage{name: "transcribe"}}, RunOptions{})
if err != nil {
t.Fatalf("executeStages() error = %v", err)
}
@@ -1302,7 +1302,7 @@ func TestExecuteStagesSkippedStagePreservesExistingOutputsProvenance(t *testing.
t.Fatalf("Save manifest error = %v", err)
}
summary, err := executeStages(context.Background(), cfg, []stage.Stage{BuildFullPlan()[1]}, RunOptions{})
summary, err := executeStages(context.Background(), cfg, []stage.Stage{resultStage{name: "transcribe"}}, RunOptions{})
if err != nil {
t.Fatalf("executeStages() error = %v", err)
}

View File

@@ -2,10 +2,17 @@ package app
import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
"gitea.maximumdirect.net/eric/narratio/internal/stage"
)
@@ -38,9 +45,89 @@ func seedCurrentSemanticEvidence(t *testing.T, cfg *config.Config, m *manifest.M
t.Fatalf("fingerprint stage %q: %v", name, err)
}
record.SemanticConfig = &fingerprint
if name == "transcribe" {
seedCurrentTranscribeEvidence(t, cfg, m, fingerprint)
}
}
}
func seedCurrentTranscribeEvidence(
t *testing.T,
cfg *config.Config,
m *manifest.Manifest,
semantic manifest.SemanticConfigFingerprint,
) {
t.Helper()
store := artifacts.NewLocalStore(cfg.Pipeline.Workspace.Root)
paths := store.SessionPathsFor(cfg.Session.Campaign, m.SessionID)
if strings.TrimSpace(m.RunID) == "" {
m.RunID = "20260503T010203Z-a1b2c3d4"
}
records := make(map[string]manifest.TranscribeSpeakerRecord)
for _, input := range m.Inputs {
if input.Kind != "audio" {
continue
}
audioPath := artifacts.ResolveSessionLocalPathForRead(paths, input.Path)
audioBytes, err := os.ReadFile(audioPath)
if err != nil {
t.Fatalf("read transcribe audio fixture %q: %v", audioPath, err)
}
audioRelative, err := pathsafe.SlashRelativeFromRoot(paths.Root, audioPath)
if err != nil {
t.Fatalf("relativize transcribe audio fixture %q: %v", audioPath, err)
}
speaker := strings.TrimSuffix(filepath.Base(audioPath), filepath.Ext(audioPath))
outputRelative := filepath.ToSlash(filepath.Join("transcripts", "raw", speaker+".json"))
outputPath, err := pathsafe.JoinSlashRelativeUnderRoot(paths.Root, outputRelative)
if err != nil {
t.Fatalf("resolve transcribe output fixture: %v", err)
}
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
mustWriteTestFile(t, outputPath, `{"segments":[]}`)
} else if err != nil {
t.Fatalf("inspect transcribe output fixture %q: %v", outputPath, err)
}
outputBytes, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("read transcribe output fixture %q: %v", outputPath, err)
}
audioDigest := sha256.Sum256(audioBytes)
outputDigest := sha256.Sum256(outputBytes)
fingerprint, err := stage.FingerprintSemanticConfig(
manifest.TranscribeFingerprintContractVersion,
struct {
SpeakerID string `json:"speaker_id"`
AudioPath string `json:"audio_path"`
AudioSHA256 string `json:"audio_sha256"`
AudioSize int64 `json:"audio_size"`
SemanticConfigVersion int `json:"semantic_config_version"`
SemanticConfigDigest string `json:"semantic_config_digest"`
}{
SpeakerID: speaker, AudioPath: audioRelative,
AudioSHA256: hex.EncodeToString(audioDigest[:]), AudioSize: int64(len(audioBytes)),
SemanticConfigVersion: semantic.Version, SemanticConfigDigest: semantic.Digest,
},
)
if err != nil {
t.Fatalf("fingerprint transcribe speaker fixture %q: %v", speaker, err)
}
output := manifest.ArtifactRecord{
Kind: "transcript_raw", LocalPath: outputRelative,
ProducerRunID: m.RunID, Checksum: hex.EncodeToString(outputDigest[:]),
}
records[speaker] = manifest.TranscribeSpeakerRecord{
FingerprintVersion: manifest.TranscribeFingerprintContractVersion,
Fingerprint: fingerprint.Digest, AudioPath: audioRelative,
AudioSHA256: hex.EncodeToString(audioDigest[:]), AudioSize: int64(len(audioBytes)),
Output: output, OutputSize: int64(len(outputBytes)), ProducerRunID: m.RunID,
}
}
record := m.Stages["transcribe"]
record.TranscribeStateVersion = manifest.TranscribeStateContractVersion
record.TranscribeSpeakers = records
}
func loadConfigForSemanticEvidence(t *testing.T, pipelinePath, campaignPath, sessionPath string) *config.Config {
t.Helper()
cfg, err := config.Load(pipelinePath, campaignPath, sessionPath)