Reintroduce normalize stage scaffold

This commit is contained in:
2026-05-10 21:26:53 +00:00
parent 7d61901585
commit 86d2dfa6c5
12 changed files with 272 additions and 94 deletions

View File

@@ -10,6 +10,7 @@ Implemented now:
- local workspace/session layout, locking, and manifest persistence - local workspace/session layout, locking, and manifest persistence
- resumable stage control (`run`, `plan`, `resume`, `run-stage`, `status`) - resumable stage control (`run`, `plan`, `resume`, `run-stage`, `status`)
- real `prepare`, `transcribe`, `merge`, `polish`, `trim`, and `analyze` stages - real `prepare`, `transcribe`, `merge`, `polish`, `trim`, and `analyze` stages
- placeholder `normalize` stage
- real WhisperX, Seriatim, and Audita adapters - real WhisperX, Seriatim, and Audita adapters
- real Scriptorium subprocess adapter - real Scriptorium subprocess adapter
- optional Scriptorium render diagnostics (`render_debug`) - optional Scriptorium render diagnostics (`render_debug`)
@@ -36,15 +37,17 @@ YAML decoding is strict (`KnownFields(true)`), so unknown fields fail fast.
2. `transcribe` 2. `transcribe`
3. `merge` 3. `merge`
4. `polish` 4. `polish`
5. `trim` 5. `normalize`
6. `analyze` 6. `trim`
7. `archive` 7. `analyze`
8. `notify` 8. `archive`
9. `notify`
## Transcript Tiers ## Transcript Tiers
- `transcripts/merged.json`: canonical deterministic merged transcript from Seriatim merge - `transcripts/merged.json`: canonical deterministic merged transcript from Seriatim merge
- `transcripts/processed.json`: full polished transcript from Audita polish - `transcripts/processed.json`: full polished transcript from Audita polish
- `transcripts/normalized.json`: placeholder normalized transcript (Seriatim schema pass-through for now)
- `transcripts/trimmed.json`: gameplay-only polished transcript from trim stage - `transcripts/trimmed.json`: gameplay-only polished transcript from trim stage
## Trim Configuration ## Trim Configuration
@@ -83,9 +86,9 @@ trim:
Trim behavior summary: Trim behavior summary:
- trim discovers and validates `transcripts/processed.json` - trim discovers and validates `transcripts/normalized.json`
- trim uses Scriptorium bounds (`dnd_session.bounds` by example config) to produce `artifacts/session_bounds.json` - trim uses Scriptorium bounds (`dnd_session.bounds` by example config) to produce `artifacts/session_bounds.json`
- bounds IDs are validated against the same processed transcript ID space that Seriatim trim will consume - bounds IDs are validated against the same normalized transcript ID space that Seriatim trim will consume
- trim converts bounds to Seriatim keep selector (for example `10-868`) and runs Seriatim trim - trim converts bounds to Seriatim keep selector (for example `10-868`) and runs Seriatim trim
- if trim is disabled, Narratio copies processed transcript to trimmed transcript and records `trim_action=copy_disabled` - if trim is disabled, Narratio copies processed transcript to trimmed transcript and records `trim_action=copy_disabled`
@@ -233,8 +236,8 @@ If prepared inputs or prompt/runtime config change, rerun the appropriate upstre
Examples: Examples:
- glossary/autocorrect/speaker-context changes: rerun at least `merge`, `polish`, `trim`, and `analyze` - glossary/autocorrect/speaker-context changes: rerun at least `merge`, `polish`, `normalize`, `trim`, and `analyze`
- trim bounds prompt/profile/config changes: rerun at least `trim` and `analyze` - trim bounds prompt/profile/config changes: rerun at least `normalize`, `trim`, and `analyze`
- session recap prompt/profile/input-source changes: rerun `analyze` - session recap prompt/profile/input-source changes: rerun `analyze`
## Roadmap ## Roadmap

View File

@@ -18,6 +18,7 @@ Implemented:
- real Seriatim subprocess adapter - real Seriatim subprocess adapter
- real Audita subprocess adapter - real Audita subprocess adapter
- real Scriptorium subprocess adapter - real Scriptorium subprocess adapter
- placeholder `normalize` stage producing `transcripts/normalized.json`
- real `trim` stage producing `transcripts/trimmed.json` - real `trim` stage producing `transcripts/trimmed.json`
- real `analyze` stage for initial `session_recap` generation - real `analyze` stage for initial `session_recap` generation
- optional Scriptorium render diagnostics (`render_debug`) before production run - optional Scriptorium render diagnostics (`render_debug`) before production run
@@ -38,10 +39,11 @@ Canonical stage order:
2. `transcribe` 2. `transcribe`
3. `merge` 3. `merge`
4. `polish` 4. `polish`
5. `trim` 5. `normalize`
6. `analyze` 6. `trim`
7. `archive` 7. `analyze`
8. `notify` 8. `archive`
9. `notify`
Boundary rules: Boundary rules:
@@ -155,29 +157,30 @@ trim:
## 6. Transcript Tiers ## 6. Transcript Tiers
Narratio currently produces and uses three transcript tiers: Narratio currently produces and uses four transcript tiers:
- `transcripts/merged.json`: canonical deterministic merged transcript from Seriatim merge - `transcripts/merged.json`: canonical deterministic merged transcript from Seriatim merge
- `transcripts/processed.json`: full polished transcript from Audita polish (includes pre/post-game content) - `transcripts/processed.json`: full polished transcript from Audita polish (includes pre/post-game content)
- `transcripts/normalized.json`: placeholder normalized transcript (Seriatim-compatible schema pass-through for now)
- `transcripts/trimmed.json`: gameplay-only polished transcript from trim stage - `transcripts/trimmed.json`: gameplay-only polished transcript from trim stage
Trim reads `transcripts/processed.json`, validates bounds IDs against that same transcript ID space, and writes `transcripts/trimmed.json`. Trim reads `transcripts/normalized.json`, validates bounds IDs against that same transcript ID space, and writes `transcripts/trimmed.json`.
## 7. Trim Stage (Current Implementation) ## 7. Trim Stage (Current Implementation)
Trim stage behavior: Trim stage behavior:
- stage order position: after `polish` and before `analyze` - stage order position: after `normalize` and before `analyze`
- discovers processed transcript from manifest polish outputs (`transcript_processed`) when present, else `work/<session_id>/transcripts/processed.json` - discovers normalized transcript from manifest normalize outputs (`transcript_normalized`) when present, else `work/<session_id>/transcripts/normalized.json`
- validates processed transcript JSON shape (`segments` array required) - validates normalized transcript JSON shape (`segments` array required)
- when `trim.enabled: false` (or trim config omitted), deterministically copies processed transcript to `transcripts/trimmed.json` and records `trim_action=copy_disabled` - when `trim.enabled: false` (or trim config omitted), deterministically copies normalized transcript to `transcripts/trimmed.json` and records `trim_action=copy_disabled`
- when `trim.enabled: true`: - when `trim.enabled: true`:
- runs Scriptorium bounds prompt using configured `trim.bounds.prompt_id` - runs Scriptorium bounds prompt using configured `trim.bounds.prompt_id`
- writes bounds output to configured path (typically `artifacts/session_bounds.json`) - writes bounds output to configured path (typically `artifacts/session_bounds.json`)
- parses and validates bounds output against the same processed transcript being trimmed - parses and validates bounds output against the same normalized transcript being trimmed
- converts bounds range to Seriatim keep selector (for example `10-868`) - converts bounds range to Seriatim keep selector (for example `10-868`)
- runs Seriatim `trim` to produce `transcripts/trimmed.json` - runs Seriatim `trim` to produce `transcripts/trimmed.json`
- supports no-trim bounds actions (`none`/`copy`) by copying processed transcript unchanged - supports no-trim bounds actions (`none`/`copy`) by copying normalized transcript unchanged
- validates trimmed transcript JSON shape (`segments` array required) - validates trimmed transcript JSON shape (`segments` array required)
Expected trim outputs and diagnostics: Expected trim outputs and diagnostics:
@@ -252,8 +255,8 @@ If prepared inputs or prompt/runtime configuration change (for example glossary
Examples: Examples:
- glossary or autocorrect changes usually require rerunning at least `merge`, `polish`, `trim`, and `analyze` - glossary or autocorrect changes usually require rerunning at least `merge`, `polish`, `normalize`, `trim`, and `analyze`
- trim prompt/profile changes require rerunning at least `trim` and `analyze` - trim prompt/profile changes require rerunning at least `normalize`, `trim`, and `analyze`
- session recap prompt/profile/input-source changes require rerunning `analyze` - session recap prompt/profile/input-source changes require rerunning `analyze`
## 12. Roadmap ## 12. Roadmap

View File

@@ -31,8 +31,8 @@ func TestExecuteValidCommands(t *testing.T) {
args []string args []string
wantOut string wantOut string
}{ }{
{name: "run", args: []string{"run", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=8 skipped=0; manifest="}, {name: "run", args: []string{"run", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=9 skipped=0; manifest="},
{name: "plan", args: []string{"plan", "--config", pipelinePath, "--session", sessionPath}, wantOut: "prepare: skip\ntranscribe: skip\nmerge: skip\npolish: skip\ntrim: skip\nanalyze: skip\narchive: skip\nnotify: skip"}, {name: "plan", args: []string{"plan", "--config", pipelinePath, "--session", sessionPath}, wantOut: "prepare: skip\ntranscribe: skip\nmerge: skip\npolish: skip\nnormalize: skip\ntrim: skip\nanalyze: skip\narchive: skip\nnotify: skip"},
{name: "status", args: []string{"status", "--manifest", manifestPath}, wantOut: "session_id: 2026-05-03"}, {name: "status", args: []string{"status", "--manifest", manifestPath}, wantOut: "session_id: 2026-05-03"},
{name: "resume", args: []string{"resume", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio resume: session 2026-05-03 has no remaining stages"}, {name: "resume", args: []string{"resume", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio resume: session 2026-05-03 has no remaining stages"},
{name: "run-stage", args: []string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "polish"}, wantOut: "narratio run-stage: stage=polish executed=0 skipped=1 force=false; manifest="}, {name: "run-stage", args: []string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "polish"}, wantOut: "narratio run-stage: stage=polish executed=0 skipped=1 force=false; manifest="},
@@ -106,19 +106,21 @@ func TestExecuteRunStageUnknownFails(t *testing.T) {
} }
} }
func TestExecuteRunStageNormalizeIsRejected(t *testing.T) { func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) {
workspaceRoot := t.TempDir() workspaceRoot := t.TempDir()
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe") pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
workRoot := filepath.Join(workspaceRoot, "work", "2026-05-03")
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1}]}`)
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
code := Execute([]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "normalize"}, &stdout, &stderr) code := Execute([]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "normalize"}, &stdout, &stderr)
if code == 0 { if code != 0 {
t.Fatal("exit code = 0, want non-zero") t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
} }
if !strings.Contains(stderr.String(), "unknown stage") { if !strings.Contains(stdout.String(), "stage=normalize executed=1 skipped=0") {
t.Fatalf("stderr = %q, want unknown stage error", stderr.String()) t.Fatalf("stdout = %q, want normalize stage execution", stdout.String())
} }
} }

View File

@@ -27,12 +27,12 @@ func TestPlanCreatesAndReusesWorkdir(t *testing.T) {
if !strings.Contains(got, "narratio plan: workdir prepared at") { if !strings.Contains(got, "narratio plan: workdir prepared at") {
t.Fatalf("first output = %q, want workdir prepared", got) t.Fatalf("first output = %q, want workdir prepared", got)
} }
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} { for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze", "archive", "notify"} {
if !strings.Contains(got, name+": run") { if !strings.Contains(got, name+": run") {
t.Fatalf("first output = %q, missing stage %q", got, name) t.Fatalf("first output = %q, missing stage %q", got, name)
} }
} }
if !strings.Contains(got, "totals: run=8 skip=0") { if !strings.Contains(got, "totals: run=9 skip=0") {
t.Fatalf("first output = %q, want totals", got) t.Fatalf("first output = %q, want totals", got)
} }
@@ -84,8 +84,8 @@ func TestPlanShowsRunAndSkipFromManifest(t *testing.T) {
if !strings.Contains(got, "trim: run") { if !strings.Contains(got, "trim: run") {
t.Fatalf("output = %q, want trim run", got) t.Fatalf("output = %q, want trim run", got)
} }
if !strings.Contains(got, "totals: run=6 skip=2") { if !strings.Contains(got, "totals: run=7 skip=2") {
t.Fatalf("output = %q, want totals run=6 skip=2", got) t.Fatalf("output = %q, want totals run=7 skip=2", got)
} }
} }

View File

@@ -4,7 +4,7 @@ import "testing"
func TestBuildFullPlanOrder(t *testing.T) { func TestBuildFullPlanOrder(t *testing.T) {
got := BuildFullPlan() got := BuildFullPlan()
want := []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} want := []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze", "archive", "notify"}
if len(got) != len(want) { if len(got) != len(want) {
t.Fatalf("len(plan) = %d, want %d", len(got), len(want)) t.Fatalf("len(plan) = %d, want %d", len(got), len(want))
} }

View File

@@ -36,8 +36,8 @@ func TestResumeStartsAfterCompletedStages(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Resume() error = %v", err) t.Fatalf("Resume() error = %v", err)
} }
if !strings.Contains(out.String(), "executed=6 skipped=0") { if !strings.Contains(out.String(), "executed=7 skipped=0") {
t.Fatalf("output = %q, want executed=6 skipped=0", out.String()) t.Fatalf("output = %q, want executed=7 skipped=0", out.String())
} }
loaded, err := store.Load(context.Background(), manifestPath) loaded, err := store.Load(context.Background(), manifestPath)
@@ -56,7 +56,7 @@ func TestResumeNoRemainingStages(t *testing.T) {
store := &manifest.LocalStore{} store := &manifest.LocalStore{}
m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC))
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} { for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze", "archive", "notify"} {
m.MarkStageSucceeded(name, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) m.MarkStageSucceeded(name, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil)
} }
if err := store.Save(context.Background(), manifestPath, m); err != nil { if err := store.Save(context.Background(), manifestPath, m); err != nil {
@@ -85,7 +85,7 @@ func TestResumeForceRerunsSucceeded(t *testing.T) {
store := &manifest.LocalStore{} store := &manifest.LocalStore{}
m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC))
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} { for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze", "archive", "notify"} {
m.MarkStageSucceeded(name, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) m.MarkStageSucceeded(name, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil)
} }
if err := store.Save(context.Background(), manifestPath, m); err != nil { if err := store.Save(context.Background(), manifestPath, m); err != nil {
@@ -97,7 +97,7 @@ func TestResumeForceRerunsSucceeded(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Resume() error = %v", err) t.Fatalf("Resume() error = %v", err)
} }
if !strings.Contains(out.String(), "executed=8 skipped=0") { if !strings.Contains(out.String(), "executed=9 skipped=0") {
t.Fatalf("output = %q, want forced full rerun", out.String()) t.Fatalf("output = %q, want forced full rerun", out.String())
} }
} }
@@ -171,7 +171,7 @@ func TestRunStageTrimExecutes(t *testing.T) {
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
manifestPath := filepath.Join(workspaceRoot, "work", "2026-05-03", "manifest.json") manifestPath := filepath.Join(workspaceRoot, "work", "2026-05-03", "manifest.json")
workRoot := filepath.Join(workspaceRoot, "work", "2026-05-03") workRoot := filepath.Join(workspaceRoot, "work", "2026-05-03")
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
var out bytes.Buffer var out bytes.Buffer
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "trim"}, &out) err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "trim"}, &out)
@@ -194,3 +194,29 @@ func TestRunStageTrimExecutes(t *testing.T) {
t.Fatalf("trim stage metadata = %#v, want trim_action=copy_disabled", m.Stages["trim"].Metadata) t.Fatalf("trim stage metadata = %#v, want trim_action=copy_disabled", m.Stages["trim"].Metadata)
} }
} }
func TestRunStageNormalizeExecutes(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
manifestPath := filepath.Join(workspaceRoot, "work", "2026-05-03", "manifest.json")
workRoot := filepath.Join(workspaceRoot, "work", "2026-05-03")
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1},{"id":2}]}`)
var out bytes.Buffer
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "normalize"}, &out)
if err != nil {
t.Fatalf("RunStage(normalize) error = %v", err)
}
if !strings.Contains(out.String(), "stage=normalize executed=1 skipped=0") {
t.Fatalf("output = %q, want stage=normalize executed", out.String())
}
store := &manifest.LocalStore{}
m, err := store.Load(context.Background(), manifestPath)
if err != nil {
t.Fatalf("load manifest: %v", err)
}
if m.Stages["normalize"] == nil || m.Stages["normalize"].Status != manifest.StatusSucceeded {
t.Fatalf("normalize stage = %#v, want succeeded", m.Stages["normalize"])
}
}

View File

@@ -51,8 +51,8 @@ func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("executeStages() error = %v", err) t.Fatalf("executeStages() error = %v", err)
} }
if len(summary.StageNames) != 8 || len(summary.Executed) != 8 || len(summary.Skipped) != 0 { if len(summary.StageNames) != 9 || len(summary.Executed) != 9 || len(summary.Skipped) != 0 {
t.Fatalf("summary = %#v, want all 8 executed", summary) t.Fatalf("summary = %#v, want all 9 executed", summary)
} }
store := &manifest.LocalStore{} store := &manifest.LocalStore{}
@@ -61,7 +61,7 @@ func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) {
t.Fatalf("Load manifest error = %v", err) t.Fatalf("Load manifest error = %v", err)
} }
for _, name := range []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} { for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze", "archive", "notify"} {
sr := m.Stages[name] sr := m.Stages[name]
if sr == nil { if sr == nil {
t.Fatalf("missing stage record %q", name) t.Fatalf("missing stage record %q", name)
@@ -114,6 +114,15 @@ func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) {
} }
continue continue
} }
if name == "normalize" {
if sr.Metadata == nil || sr.Metadata["stage"] != "normalize" {
t.Fatalf("normalize metadata missing stage=normalize: %#v", sr.Metadata)
}
if len(sr.Outputs) == 0 {
t.Fatalf("normalize outputs missing")
}
continue
}
if name == "analyze" { if name == "analyze" {
if sr.Metadata == nil || sr.Metadata["stage"] != "analyze" { if sr.Metadata == nil || sr.Metadata["stage"] != "analyze" {
t.Fatalf("analyze metadata missing stage=analyze: %#v", sr.Metadata) t.Fatalf("analyze metadata missing stage=analyze: %#v", sr.Metadata)

122
internal/stage/normalize.go Normal file
View File

@@ -0,0 +1,122 @@
package stage
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
type normalizeStage struct{}
func (normalizeStage) Name() string { return "normalize" }
func (normalizeStage) Declares() IODecl {
return IODecl{
Inputs: []artifacts.Ref{
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"},
},
Outputs: []artifacts.Ref{
{Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"},
},
}
}
func (normalizeStage) Run(_ context.Context, env *Env, m *manifest.Manifest) (*StageResult, error) {
if env == nil || env.Config == nil {
return nil, fmt.Errorf("normalize: stage environment config is required")
}
if env.ArtifactStore == nil {
return nil, fmt.Errorf("normalize: artifact store is required")
}
if env.Config.Pipeline == nil || env.Config.Session == nil {
return nil, fmt.Errorf("normalize: resolved config must include pipeline and session")
}
var sessionID string
if m != nil {
sessionID = strings.TrimSpace(m.SessionID)
}
if sessionID == "" {
sessionID = strings.TrimSpace(env.Config.Session.SessionID)
}
if sessionID == "" {
return nil, fmt.Errorf("normalize: session id is required")
}
paths := env.ArtifactStore.SessionPaths(sessionID)
processedPath, processedSource, err := discoverProcessedTranscript(m, paths)
if err != nil {
return nil, fmt.Errorf("normalize: resolve processed transcript: %w", err)
}
if processedPath == "" {
return nil, fmt.Errorf("normalize: processed transcript input is required")
}
if err := validateProcessedTranscriptOutput(processedPath); err != nil {
return nil, fmt.Errorf("normalize: processed transcript %q invalid: %w", processedPath, err)
}
normalizedPath := filepath.Join(paths.TranscriptsDir, "normalized.json")
if err := copyTranscript(env.ArtifactStore, processedPath, normalizedPath); err != nil {
return nil, fmt.Errorf("normalize: copy processed transcript to normalized output: %w", err)
}
if err := validateProcessedTranscriptOutput(normalizedPath); err != nil {
return nil, fmt.Errorf("normalize: normalized transcript %q invalid: %w", normalizedPath, err)
}
return &StageResult{
Outputs: []artifacts.Ref{{
Kind: "transcript_normalized",
Category: "transcripts",
SessionID: sessionID,
AbsolutePath: normalizedPath,
}},
Metadata: map[string]any{
"stage": "normalize",
"placeholder": true,
"normalize_action": "copy_placeholder",
"processed_transcript_path": processedPath,
"processed_transcript_source": processedSource,
"normalized_transcript_path": normalizedPath,
"normalized_transcript_source": "stage.normalize.output",
},
}, nil
}
func discoverNormalizedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths) (string, string, error) {
candidates := []string{}
if m != nil && m.Stages != nil {
if sr := m.Stages["normalize"]; sr != nil {
for _, out := range sr.Outputs {
if out.Kind != "transcript_normalized" {
continue
}
p := strings.TrimSpace(out.LocalPath)
if p == "" {
continue
}
resolved := artifacts.ResolveSessionLocalPathForRead(paths, p)
candidates = append(candidates, filepath.Clean(resolved))
}
}
}
deduped := dedupeAndSortPaths(candidates)
for _, p := range deduped {
if info, err := os.Stat(p); err == nil && !info.IsDir() {
return p, "manifest.normalize.outputs", nil
}
}
fallback := filepath.Join(paths.TranscriptsDir, "normalized.json")
if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
return filepath.Clean(fallback), "fallback.transcripts_dir", nil
}
if len(deduped) > 0 {
return deduped[0], "manifest.normalize.outputs", nil
}
return "", "", nil
}

View File

@@ -92,6 +92,7 @@ func All() []Stage {
transcribeStage{}, transcribeStage{},
mergeStage{}, mergeStage{},
polishStage{}, polishStage{},
normalizeStage{},
trimStage{}, trimStage{},
analyzeStage{}, analyzeStage{},
placeholderStage{name: "archive"}, placeholderStage{name: "archive"},

View File

@@ -111,6 +111,18 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
} }
continue continue
} }
if s.Name() == "normalize" {
if result.Metadata["stage"] != "normalize" {
t.Fatalf("normalize metadata = %#v, want stage=normalize", result.Metadata)
}
if result.Metadata["normalize_action"] != "copy_placeholder" {
t.Fatalf("normalize metadata = %#v, want normalize_action=copy_placeholder", result.Metadata)
}
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_normalized" {
t.Fatalf("normalize outputs = %#v, want transcript_normalized output", result.Outputs)
}
continue
}
if s.Name() == "analyze" { if s.Name() == "analyze" {
if result.Metadata["stage"] != "analyze" { if result.Metadata["stage"] != "analyze" {
t.Fatalf("analyze metadata = %#v, want stage=analyze", result.Metadata) t.Fatalf("analyze metadata = %#v, want stage=analyze", result.Metadata)

View File

@@ -23,7 +23,7 @@ func (trimStage) Name() string { return "trim" }
func (trimStage) Declares() IODecl { func (trimStage) Declares() IODecl {
return IODecl{ return IODecl{
Inputs: []artifacts.Ref{ Inputs: []artifacts.Ref{
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"}, {Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"},
}, },
Outputs: []artifacts.Ref{ Outputs: []artifacts.Ref{
{Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"}, {Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"},
@@ -55,15 +55,15 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
} }
paths := env.ArtifactStore.SessionPaths(sessionID) paths := env.ArtifactStore.SessionPaths(sessionID)
processedPath, processedSource, err := discoverProcessedTranscript(m, paths) normalizedPath, normalizedSource, err := discoverNormalizedTranscript(m, paths)
if err != nil { if err != nil {
return nil, fmt.Errorf("trim: resolve processed transcript: %w", err) return nil, fmt.Errorf("trim: resolve normalized transcript: %w", err)
} }
if processedPath == "" { if normalizedPath == "" {
return nil, fmt.Errorf("trim: processed transcript input is required") return nil, fmt.Errorf("trim: normalized transcript input is required")
} }
if err := validateProcessedTranscriptOutput(processedPath); err != nil { if err := validateProcessedTranscriptOutput(normalizedPath); err != nil {
return nil, fmt.Errorf("trim: processed transcript %q invalid: %w", processedPath, err) return nil, fmt.Errorf("trim: normalized transcript %q invalid: %w", normalizedPath, err)
} }
trimCfg := env.Config.Pipeline.Trim trimCfg := env.Config.Pipeline.Trim
@@ -77,16 +77,16 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
logPaths := []string{} logPaths := []string{}
generatedConfigs := []string{} generatedConfigs := []string{}
metadata := map[string]any{ metadata := map[string]any{
"stage": "trim", "stage": "trim",
"trim_enabled": enabled, "trim_enabled": enabled,
"processed_transcript_path": processedPath, "normalized_transcript_path": normalizedPath,
"processed_transcript_source": processedSource, "normalized_transcript_source": normalizedSource,
"trimmed_output_path": trimmedPath, "trimmed_output_path": trimmedPath,
} }
if !enabled { if !enabled {
if err := copyTranscript(env.ArtifactStore, processedPath, trimmedPath); err != nil { if err := copyTranscript(env.ArtifactStore, normalizedPath, trimmedPath); err != nil {
return nil, fmt.Errorf("trim: copy processed transcript to trimmed output: %w", err) return nil, fmt.Errorf("trim: copy normalized transcript to trimmed output: %w", err)
} }
if err := validateProcessedTranscriptOutput(trimmedPath); err != nil { if err := validateProcessedTranscriptOutput(trimmedPath); err != nil {
return nil, fmt.Errorf("trim: copied trimmed transcript %q invalid: %w", trimmedPath, err) return nil, fmt.Errorf("trim: copied trimmed transcript %q invalid: %w", trimmedPath, err)
@@ -127,7 +127,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
} }
inputPaths := map[string]string{ inputPaths := map[string]string{
boundsCfg.TranscriptInputName: processedPath, boundsCfg.TranscriptInputName: normalizedPath,
} }
vars := map[string]string{} vars := map[string]string{}
@@ -136,7 +136,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
metadata["bounds_output_path"] = boundsOutputPath metadata["bounds_output_path"] = boundsOutputPath
metadata["bounds_timeout"] = boundsTimeout.String() metadata["bounds_timeout"] = boundsTimeout.String()
metadata["bounds_input_name"] = boundsCfg.TranscriptInputName metadata["bounds_input_name"] = boundsCfg.TranscriptInputName
metadata["bounds_input_path"] = processedPath metadata["bounds_input_path"] = normalizedPath
metadata["bounds_render_debug_enabled"] = boundsCfg.RenderDebug metadata["bounds_render_debug_enabled"] = boundsCfg.RenderDebug
renderOutputPath := "" renderOutputPath := ""
@@ -233,8 +233,8 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
if err != nil { if err != nil {
return nil, fmt.Errorf("trim: parse bounds output %q: %w", finalBoundsOutputPath, err) return nil, fmt.Errorf("trim: parse bounds output %q: %w", finalBoundsOutputPath, err)
} }
if err := contracts.ValidateSessionBoundsAgainstTranscript(boundsPayload, processedPath); err != nil { if err := contracts.ValidateSessionBoundsAgainstTranscript(boundsPayload, normalizedPath); err != nil {
return nil, fmt.Errorf("trim: validate bounds output %q against transcript %q: %w", finalBoundsOutputPath, processedPath, err) return nil, fmt.Errorf("trim: validate bounds output %q against transcript %q: %w", finalBoundsOutputPath, normalizedPath, err)
} }
keepSelector, copyUnchanged, err := contracts.BuildSeriatimKeepSelector(boundsPayload) keepSelector, copyUnchanged, err := contracts.BuildSeriatimKeepSelector(boundsPayload)
@@ -272,8 +272,8 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
} }
if copyUnchanged { if copyUnchanged {
if err := copyTranscript(env.ArtifactStore, processedPath, trimmedPath); err != nil { if err := copyTranscript(env.ArtifactStore, normalizedPath, trimmedPath); err != nil {
return nil, fmt.Errorf("trim: copy processed transcript to trimmed output: %w", err) return nil, fmt.Errorf("trim: copy normalized transcript to trimmed output: %w", err)
} }
} else { } else {
trimStdoutLogPath := filepath.Join(paths.LogsDir, "seriatim.trim.stdout.log") trimStdoutLogPath := filepath.Join(paths.LogsDir, "seriatim.trim.stdout.log")
@@ -285,7 +285,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
} }
trimReq := seriatim.TrimRequest{ trimReq := seriatim.TrimRequest{
Binary: env.Config.Pipeline.Seriatim.Binary, Binary: env.Config.Pipeline.Seriatim.Binary,
InputTranscriptPath: processedPath, InputTranscriptPath: normalizedPath,
OutputTrimmedPath: trimmedPath, OutputTrimmedPath: trimmedPath,
KeepSelector: keepSelector, KeepSelector: keepSelector,
StdoutLogPath: trimStdoutLogPath, StdoutLogPath: trimStdoutLogPath,

View File

@@ -16,13 +16,13 @@ import (
"gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/manifest"
) )
func TestTrimStageConsumesProcessedAndProducesTrimmedTranscript(t *testing.T) { func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
env, m, scr, ser := setupTrimEnv(t) env, m, scr, ser := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
processed := filepath.Join(paths.TranscriptsDir, "processed.json") normalized := filepath.Join(paths.TranscriptsDir, "normalized.json")
writeFile(t, processed, `{"segments":[{"id":10},{"id":868}]}`) writeFile(t, normalized, `{"segments":[{"id":10},{"id":868}]}`)
m.MarkStageSucceeded("polish", time.Now().UTC(), []manifest.ArtifactRecord{ m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
{Kind: "transcript_processed", LocalPath: processed}, {Kind: "transcript_normalized", LocalPath: normalized},
}) })
scr.BoundsBody = `{"confidence":"high","trim_action":"trim","start_segment_id":10,"end_segment_id":868,"warnings":[]}` scr.BoundsBody = `{"confidence":"high","trim_action":"trim","start_segment_id":10,"end_segment_id":868,"warnings":[]}`
@@ -38,8 +38,8 @@ func TestTrimStageConsumesProcessedAndProducesTrimmedTranscript(t *testing.T) {
t.Fatalf("seriatim trim requests = %d, want 1", len(ser.TrimRequests)) t.Fatalf("seriatim trim requests = %d, want 1", len(ser.TrimRequests))
} }
trimReq := ser.TrimRequests[0] trimReq := ser.TrimRequests[0]
if trimReq.InputTranscriptPath != processed { if trimReq.InputTranscriptPath != normalized {
t.Fatalf("trim input = %q, want %q", trimReq.InputTranscriptPath, processed) t.Fatalf("trim input = %q, want %q", trimReq.InputTranscriptPath, normalized)
} }
if trimReq.KeepSelector != "10-868" { if trimReq.KeepSelector != "10-868" {
t.Fatalf("keep selector = %q, want %q", trimReq.KeepSelector, "10-868") t.Fatalf("keep selector = %q, want %q", trimReq.KeepSelector, "10-868")
@@ -71,8 +71,8 @@ func TestTrimStageConsumesProcessedAndProducesTrimmedTranscript(t *testing.T) {
func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) { func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
processed := filepath.Join(paths.TranscriptsDir, "processed.json") normalized := filepath.Join(paths.TranscriptsDir, "normalized.json")
writeFile(t, processed, `{"segments":[{"id":10},{"id":11}]}`) writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`)
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11}`
cfg := env.Config.Pipeline.Trim cfg := env.Config.Pipeline.Trim
@@ -87,15 +87,15 @@ func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) {
t.Fatalf("scriptorium run requests = %d, want 1", len(scr.RunRequests)) t.Fatalf("scriptorium run requests = %d, want 1", len(scr.RunRequests))
} }
req := scr.RunRequests[0] req := scr.RunRequests[0]
if req.InputPaths["transcript_body"] != processed { if req.InputPaths["transcript_body"] != normalized {
t.Fatalf("configured input path = %q, want %q", req.InputPaths["transcript_body"], processed) t.Fatalf("configured input path = %q, want %q", req.InputPaths["transcript_body"], normalized)
} }
} }
func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) { func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
result, err := (trimStage{}).Run(context.Background(), env, m) result, err := (trimStage{}).Run(context.Background(), env, m)
@@ -115,7 +115,7 @@ func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) {
func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) { func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
cfg := *env.Config.Pipeline.Trim cfg := *env.Config.Pipeline.Trim
@@ -139,21 +139,21 @@ func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) {
} }
} }
func TestTrimStageFailsWhenProcessedTranscriptMissing(t *testing.T) { func TestTrimStageFailsWhenNormalizedTranscriptMissing(t *testing.T) {
env, m, _, _ := setupTrimEnv(t) env, m, _, _ := setupTrimEnv(t)
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
} }
if !strings.Contains(err.Error(), "processed transcript input is required") { if !strings.Contains(err.Error(), "normalized transcript input is required") {
t.Fatalf("error = %q", err.Error()) t.Fatalf("error = %q", err.Error())
} }
} }
func TestTrimStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) { func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) {
env, m, _, _ := setupTrimEnv(t) env, m, _, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), "not-json") writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), "not-json")
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
@@ -163,10 +163,10 @@ func TestTrimStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
} }
} }
func TestTrimStageFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T) { func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T) {
env, m, _, _ := setupTrimEnv(t) env, m, _, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"schema":"audita.processed.v1"}`) writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"schema":"audita.processed.v1"}`)
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
@@ -179,7 +179,7 @@ func TestTrimStageFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T)
func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) { func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
scr.BoundsBody = "not-json" scr.BoundsBody = "not-json"
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -193,7 +193,7 @@ func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) {
func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) { func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":2,"end_segment_id":1}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":2,"end_segment_id":1}`
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -207,7 +207,7 @@ func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) {
func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) { func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":20},{"id":21}]}`) writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":20},{"id":21}]}`)
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":21}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":21}`
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -221,7 +221,7 @@ func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) {
func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) { func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
scr.RunErr = errors.New("bounds failed") scr.RunErr = errors.New("bounds failed")
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -235,7 +235,7 @@ func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) {
func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) { func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) {
env, m, scr, ser := setupTrimEnv(t) env, m, scr, ser := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
ser.TrimErr = errors.New("trim failed") ser.TrimErr = errors.New("trim failed")
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
@@ -247,12 +247,12 @@ func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) {
} }
} }
func TestTrimStageDisabledCopiesProcessedTranscript(t *testing.T) { func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) {
env, m, scr, ser := setupTrimEnv(t) env, m, scr, ser := setupTrimEnv(t)
paths := env.ArtifactStore.SessionPaths(m.SessionID) paths := env.ArtifactStore.SessionPaths(m.SessionID)
processed := filepath.Join(paths.TranscriptsDir, "processed.json") normalized := filepath.Join(paths.TranscriptsDir, "normalized.json")
processedBody := `{"segments":[{"id":1,"text":"alpha"},{"id":2,"text":"beta"}]}` normalizedBody := `{"segments":[{"id":1,"text":"alpha"},{"id":2,"text":"beta"}]}`
writeFile(t, processed, processedBody) writeFile(t, normalized, normalizedBody)
disabled := *env.Config.Pipeline.Trim disabled := *env.Config.Pipeline.Trim
disabled.Enabled = false disabled.Enabled = false
@@ -273,8 +273,8 @@ func TestTrimStageDisabledCopiesProcessedTranscript(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("read trimmed transcript: %v", err) t.Fatalf("read trimmed transcript: %v", err)
} }
if string(data) != processedBody { if string(data) != normalizedBody {
t.Fatalf("trimmed body = %q, want copied processed body", string(data)) t.Fatalf("trimmed body = %q, want copied normalized body", string(data))
} }
} }