Implement real Seriatim merge stage
This commit is contained in:
@@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -195,6 +196,8 @@ func writeValidConfigFiles(t *testing.T, workspaceRoot string, transcribeURL ...
|
||||
if len(transcribeURL) > 0 && strings.TrimSpace(transcribeURL[0]) != "" {
|
||||
url = transcribeURL[0]
|
||||
}
|
||||
seriatimBinary := writeSeriatimAppTestWrapper(t)
|
||||
t.Setenv("GO_WANT_APP_SERIATIM_HELPER", "1")
|
||||
|
||||
pipelineYAML := `workspace:
|
||||
root: ` + workspaceRoot + `
|
||||
@@ -207,7 +210,7 @@ whisperx:
|
||||
retry_delay: 1ms
|
||||
concurrency: 1
|
||||
seriatim:
|
||||
binary: seriatim
|
||||
binary: ` + seriatimBinary + `
|
||||
timeout: 10m
|
||||
output_schema: seriatim-intermediate
|
||||
coalesce_gap: 3.0
|
||||
@@ -237,7 +240,7 @@ inputs:
|
||||
t.Fatalf("write session config: %v", err)
|
||||
}
|
||||
|
||||
mustWriteTestFile(t, filepath.Join(dir, "speakers.yml"), "alice: alice.flac\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
||||
@@ -269,3 +272,74 @@ func mustWriteTestFile(t *testing.T, path, contents string) {
|
||||
t.Fatalf("write %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeSeriatimAppTestWrapper(t *testing.T) string {
|
||||
t.Helper()
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatalf("os.Executable() error = %v", err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "seriatim-helper-wrapper.sh")
|
||||
content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestSeriatimAppHelper -- \"$@\"\n"
|
||||
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
||||
t.Fatalf("WriteFile(%q): %v", path, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestSeriatimAppHelper(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_APP_SERIATIM_HELPER") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
args := os.Args
|
||||
start := -1
|
||||
for i := range args {
|
||||
if args[i] == "--" {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if start < 0 || start >= len(args) {
|
||||
_, _ = os.Stderr.WriteString("missing -- args separator\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
mergeArgs := args[start:]
|
||||
|
||||
outputPath := appSeriatimFlagValue(mergeArgs, "--output-file")
|
||||
reportPath := appSeriatimFlagValue(mergeArgs, "--report-file")
|
||||
if strings.TrimSpace(outputPath) == "" {
|
||||
_, _ = os.Stderr.WriteString("missing --output-file\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir output dir: %v\n", err))
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := os.WriteFile(outputPath, []byte(`{"schema":"seriatim-intermediate","segments":[]}`), 0o644); err != nil {
|
||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("write output: %v\n", err))
|
||||
os.Exit(2)
|
||||
}
|
||||
if strings.TrimSpace(reportPath) != "" {
|
||||
if err := os.MkdirAll(filepath.Dir(reportPath), 0o755); err != nil {
|
||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir report dir: %v\n", err))
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := os.WriteFile(reportPath, []byte(`{"report":true}`), 0o644); err != nil {
|
||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("write report: %v\n", err))
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
_, _ = os.Stdout.WriteString("seriatim helper stdout\n")
|
||||
_, _ = os.Stderr.WriteString("seriatim helper stderr\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func appSeriatimFlagValue(args []string, name string) string {
|
||||
for i := 0; i < len(args)-1; i++ {
|
||||
if args[i] == name {
|
||||
return args[i+1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ func TestResumeStartsAfterCompletedStages(t *testing.T) {
|
||||
if err := store.Save(context.Background(), manifestPath, m); err != nil {
|
||||
t.Fatalf("save manifest: %v", err)
|
||||
}
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "raw", "alice.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "autocorrect.yml"), "[]\n")
|
||||
|
||||
var out bytes.Buffer
|
||||
err := Resume(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath}, &out)
|
||||
|
||||
@@ -59,7 +59,11 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage
|
||||
env.WhisperX = client
|
||||
}
|
||||
if env.Seriatim == nil {
|
||||
env.Seriatim = &seriatim.NoopRunner{}
|
||||
runner, err := buildDefaultSeriatimRunner(env.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initialize seriatim runner: %w", err)
|
||||
}
|
||||
env.Seriatim = runner
|
||||
}
|
||||
if env.Audita == nil {
|
||||
env.Audita = &audita.NoopRunner{}
|
||||
@@ -179,6 +183,40 @@ func buildDefaultWhisperXClient(cfg *config.Config) (whisperx.Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func buildDefaultSeriatimRunner(cfg *config.Config) (seriatim.Runner, error) {
|
||||
if cfg == nil || cfg.Pipeline == nil {
|
||||
return &seriatim.NoopRunner{}, nil
|
||||
}
|
||||
|
||||
s := cfg.Pipeline.Seriatim
|
||||
if strings.TrimSpace(s.Binary) == "" || strings.TrimSpace(s.Timeout) == "" || strings.TrimSpace(s.OutputSchema) == "" {
|
||||
// Compatibility fallback for tests or internal call paths that bypass config validation/defaults.
|
||||
return &seriatim.NoopRunner{}, nil
|
||||
}
|
||||
|
||||
report := false
|
||||
if s.Report != nil {
|
||||
report = *s.Report
|
||||
}
|
||||
runner, err := seriatim.NewSubprocessRunnerFromConfigValues(
|
||||
s.Binary,
|
||||
s.Timeout,
|
||||
s.OutputSchema,
|
||||
s.CoalesceGap,
|
||||
report,
|
||||
seriatim.EnvConfig{
|
||||
OverlapWordRunGap: s.Env.OverlapWordRunGap,
|
||||
OverlapWordRunReorderWindow: s.Env.OverlapWordRunReorderWindow,
|
||||
BackchannelMaxDuration: s.Env.BackchannelMaxDuration,
|
||||
FillerMaxDuration: s.Env.FillerMaxDuration,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("from pipeline.seriatim: %w", err)
|
||||
}
|
||||
return runner, nil
|
||||
}
|
||||
|
||||
func loadOrCreateManifest(ctx context.Context, store manifest.Store, path, sessionID string) (*manifest.Manifest, error) {
|
||||
exists, err := fileExists(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -84,6 +84,21 @@ func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
if name == "merge" {
|
||||
if sr.Metadata == nil || sr.Metadata["stage"] != "merge" {
|
||||
t.Fatalf("merge metadata missing stage=merge: %#v", sr.Metadata)
|
||||
}
|
||||
if len(sr.Outputs) == 0 {
|
||||
t.Fatalf("merge outputs missing")
|
||||
}
|
||||
if len(sr.Logs) == 0 {
|
||||
t.Fatalf("merge logs missing")
|
||||
}
|
||||
if len(sr.GeneratedConfigs) == 0 {
|
||||
t.Fatalf("merge generated configs missing")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if sr.Metadata == nil || sr.Metadata["placeholder"] != true {
|
||||
t.Fatalf("stage %q missing placeholder metadata", name)
|
||||
}
|
||||
@@ -286,6 +301,25 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) {
|
||||
t.Fatalf("seed manifest: %v", err)
|
||||
}
|
||||
}
|
||||
if tc.name == "merge" {
|
||||
paths, ensureErr := artifactStore.EnsureLayout(cfg.Session.SessionID)
|
||||
if ensureErr != nil {
|
||||
t.Fatalf("EnsureLayout() error = %v", ensureErr)
|
||||
}
|
||||
rawPath := filepath.Join(paths.TranscriptsRawDir, "alice.json")
|
||||
if err := os.MkdirAll(filepath.Dir(rawPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir raw dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(rawPath, []byte(`{"segments":[]}`), 0o644); err != nil {
|
||||
t.Fatalf("write raw transcript: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(paths.InputsDir, "speakers.yml"), []byte("match: []\n"), 0o644); err != nil {
|
||||
t.Fatalf("write speakers: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(paths.InputsDir, "autocorrect.yml"), []byte("rules: []\n"), 0o644); err != nil {
|
||||
t.Fatalf("write autocorrect: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, runErr := executeStages(context.Background(), cfg, []stage.Stage{selected}, RunOptions{Env: tc.env})
|
||||
if runErr == nil {
|
||||
|
||||
Reference in New Issue
Block a user