Add normalized transcript analysis source
This commit is contained in:
@@ -194,8 +194,10 @@ Analyze-stage session recap behavior:
|
|||||||
|
|
||||||
- session recap should use gameplay-only transcript input (`source: trimmed_transcript`)
|
- session recap should use gameplay-only transcript input (`source: trimmed_transcript`)
|
||||||
- Narratio resolves `trimmed_transcript` from trim manifest output (`transcript_trimmed`) or fallback `transcripts/trimmed.json`
|
- Narratio resolves `trimmed_transcript` from trim manifest output (`transcript_trimmed`) or fallback `transcripts/trimmed.json`
|
||||||
|
- Narratio resolves `normalized_transcript` from normalize manifest output (`transcript_normalized`) or fallback `transcripts/normalized.json`
|
||||||
- missing trimmed transcript fails clearly and advises running trim stage first
|
- missing trimmed transcript fails clearly and advises running trim stage first
|
||||||
- full polished transcript input (`source: processed_transcript`) remains supported for future table/meta-analysis artifacts
|
- `normalized_transcript` is the preferred full-transcript source for future table/meta-analysis artifacts
|
||||||
|
- `processed_transcript` remains supported for advanced/debug use cases
|
||||||
- optionally includes `previous_recap` when configured and resolvable
|
- optionally includes `previous_recap` when configured and resolvable
|
||||||
- omits optional previous recap when unavailable
|
- omits optional previous recap when unavailable
|
||||||
- fails if required inputs are missing
|
- fails if required inputs are missing
|
||||||
|
|||||||
@@ -227,8 +227,10 @@ Behavior:
|
|||||||
- if enabled artifacts exist but `session_recap` is not enabled, analyze fails clearly
|
- if enabled artifacts exist but `session_recap` is not enabled, analyze fails clearly
|
||||||
- `session_recap` should use `trimmed_transcript` input (`transcripts/trimmed.json`) for in-universe recap generation
|
- `session_recap` should use `trimmed_transcript` input (`transcripts/trimmed.json`) for in-universe recap generation
|
||||||
- `trimmed_transcript` input is resolved from manifest (`trim` output kind `transcript_trimmed`) when available, otherwise fallback path `work/<session_id>/transcripts/trimmed.json`
|
- `trimmed_transcript` input is resolved from manifest (`trim` output kind `transcript_trimmed`) when available, otherwise fallback path `work/<session_id>/transcripts/trimmed.json`
|
||||||
|
- `normalized_transcript` input is resolved from manifest (`normalize` output kind `transcript_normalized`) when available, otherwise fallback path `work/<session_id>/transcripts/normalized.json`
|
||||||
- `processed_transcript` input is resolved from manifest (`polish` output kind `transcript_processed`) when available, otherwise fallback path `work/<session_id>/transcripts/processed.json`
|
- `processed_transcript` input is resolved from manifest (`polish` output kind `transcript_processed`) when available, otherwise fallback path `work/<session_id>/transcripts/processed.json`
|
||||||
- `processed_transcript` remains available for future table/meta-analysis artifacts
|
- `normalized_transcript` is the preferred full-transcript source for future table/meta-analysis artifacts
|
||||||
|
- `processed_transcript` remains available for advanced/debug use cases
|
||||||
- transcript inputs are validated as JSON with top-level `segments` array
|
- transcript inputs are validated as JSON with top-level `segments` array
|
||||||
- configured inputs are resolved by source
|
- configured inputs are resolved by source
|
||||||
- optional `previous_recap` is omitted when unavailable
|
- optional `previous_recap` is omitted when unavailable
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func (analyzeStage) Declares() IODecl {
|
|||||||
return IODecl{
|
return IODecl{
|
||||||
Inputs: []artifacts.Ref{
|
Inputs: []artifacts.Ref{
|
||||||
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"},
|
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"},
|
||||||
|
{Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"},
|
||||||
{Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"},
|
{Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"},
|
||||||
},
|
},
|
||||||
Outputs: []artifacts.Ref{
|
Outputs: []artifacts.Ref{
|
||||||
@@ -86,16 +87,22 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("analyze: resolve processed transcript: %w", err)
|
return nil, fmt.Errorf("analyze: resolve processed transcript: %w", err)
|
||||||
}
|
}
|
||||||
|
normalizedTranscriptPath, normalizedSource, err := discoverNormalizedTranscript(m, paths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("analyze: resolve normalized transcript: %w", err)
|
||||||
|
}
|
||||||
trimmedTranscriptPath, trimmedSource, err := discoverTrimmedTranscript(m, paths)
|
trimmedTranscriptPath, trimmedSource, err := discoverTrimmedTranscript(m, paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("analyze: resolve trimmed transcript: %w", err)
|
return nil, fmt.Errorf("analyze: resolve trimmed transcript: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
transcriptInputs := analyzeTranscriptInputs{
|
transcriptInputs := analyzeTranscriptInputs{
|
||||||
ProcessedPath: processedTranscriptPath,
|
ProcessedPath: processedTranscriptPath,
|
||||||
ProcessedSource: processedSource,
|
ProcessedSource: processedSource,
|
||||||
TrimmedPath: trimmedTranscriptPath,
|
NormalizedPath: normalizedTranscriptPath,
|
||||||
TrimmedSource: trimmedSource,
|
NormalizedSource: normalizedSource,
|
||||||
|
TrimmedPath: trimmedTranscriptPath,
|
||||||
|
TrimmedSource: trimmedSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
inputPaths := map[string]string{}
|
inputPaths := map[string]string{}
|
||||||
@@ -139,22 +146,24 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
logPaths := []string{}
|
logPaths := []string{}
|
||||||
generatedConfigs := []string{}
|
generatedConfigs := []string{}
|
||||||
meta := map[string]any{
|
meta := map[string]any{
|
||||||
"stage": "analyze",
|
"stage": "analyze",
|
||||||
"artifact_name": artifactName,
|
"artifact_name": artifactName,
|
||||||
"prompt_id": artifactCfg.PromptID,
|
"prompt_id": artifactCfg.PromptID,
|
||||||
"profile_id": artifactCfg.ProfileID,
|
"profile_id": artifactCfg.ProfileID,
|
||||||
"binary": env.Config.Pipeline.Scriptorium.Binary,
|
"binary": env.Config.Pipeline.Scriptorium.Binary,
|
||||||
"config_path": env.Config.Pipeline.Scriptorium.ConfigPath,
|
"config_path": env.Config.Pipeline.Scriptorium.ConfigPath,
|
||||||
"input_paths": inputPaths,
|
"input_paths": inputPaths,
|
||||||
"input_names": sortedMapKeys(inputPaths),
|
"input_names": sortedMapKeys(inputPaths),
|
||||||
"omitted_optional_inputs": omittedOptionalInputs,
|
"omitted_optional_inputs": omittedOptionalInputs,
|
||||||
"vars": vars,
|
"vars": vars,
|
||||||
"timeout": timeout.String(),
|
"timeout": timeout.String(),
|
||||||
"processed_transcript_path": processedTranscriptPath,
|
"processed_transcript_path": processedTranscriptPath,
|
||||||
"processed_transcript_source": processedSource,
|
"processed_transcript_source": processedSource,
|
||||||
"trimmed_transcript_path": trimmedTranscriptPath,
|
"normalized_transcript_path": normalizedTranscriptPath,
|
||||||
"trimmed_transcript_source": trimmedSource,
|
"normalized_transcript_source": normalizedSource,
|
||||||
"render_debug_enabled": resolveRenderDebugEnabled(env.Config.Pipeline.Scriptorium.RenderDebug, artifactCfg.RenderDebug),
|
"trimmed_transcript_path": trimmedTranscriptPath,
|
||||||
|
"trimmed_transcript_source": trimmedSource,
|
||||||
|
"render_debug_enabled": resolveRenderDebugEnabled(env.Config.Pipeline.Scriptorium.RenderDebug, artifactCfg.RenderDebug),
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta["render_debug_enabled"] == true {
|
if meta["render_debug_enabled"] == true {
|
||||||
@@ -376,10 +385,12 @@ func discoverTrimmedTranscript(m *manifest.Manifest, paths artifacts.SessionPath
|
|||||||
}
|
}
|
||||||
|
|
||||||
type analyzeTranscriptInputs struct {
|
type analyzeTranscriptInputs struct {
|
||||||
ProcessedPath string
|
ProcessedPath string
|
||||||
ProcessedSource string
|
ProcessedSource string
|
||||||
TrimmedPath string
|
NormalizedPath string
|
||||||
TrimmedSource string
|
NormalizedSource string
|
||||||
|
TrimmedPath string
|
||||||
|
TrimmedSource string
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveScriptoriumInput(
|
func resolveScriptoriumInput(
|
||||||
@@ -398,6 +409,14 @@ func resolveScriptoriumInput(
|
|||||||
return "", false, fmt.Errorf("processed transcript %q invalid: %w", transcriptInputs.ProcessedPath, err)
|
return "", false, fmt.Errorf("processed transcript %q invalid: %w", transcriptInputs.ProcessedPath, err)
|
||||||
}
|
}
|
||||||
return transcriptInputs.ProcessedPath, true, nil
|
return transcriptInputs.ProcessedPath, true, nil
|
||||||
|
case "normalized_transcript":
|
||||||
|
if strings.TrimSpace(transcriptInputs.NormalizedPath) == "" {
|
||||||
|
return "", false, fmt.Errorf("normalized transcript input is unavailable; run normalize stage first")
|
||||||
|
}
|
||||||
|
if err := validateProcessedTranscriptOutput(transcriptInputs.NormalizedPath); err != nil {
|
||||||
|
return "", false, fmt.Errorf("normalized transcript %q invalid: %w", transcriptInputs.NormalizedPath, err)
|
||||||
|
}
|
||||||
|
return transcriptInputs.NormalizedPath, true, nil
|
||||||
case "trimmed_transcript":
|
case "trimmed_transcript":
|
||||||
if strings.TrimSpace(transcriptInputs.TrimmedPath) == "" {
|
if strings.TrimSpace(transcriptInputs.TrimmedPath) == "" {
|
||||||
return "", false, fmt.Errorf("trimmed transcript input is unavailable; run trim stage first")
|
return "", false, fmt.Errorf("trimmed transcript input is unavailable; run trim stage first")
|
||||||
|
|||||||
@@ -447,6 +447,85 @@ func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||||
|
env, m, fake := setupAnalyzeEnv(t)
|
||||||
|
paths := env.ArtifactStore.SessionPaths(m.SessionID)
|
||||||
|
normalizedPath := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||||
|
writeAnalyzeFile(t, normalizedPath, `{"segments":[{"id":1}]}`)
|
||||||
|
|
||||||
|
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||||
|
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||||
|
Source: "normalized_transcript",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||||
|
|
||||||
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(fake.RunRequests) != 1 {
|
||||||
|
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests))
|
||||||
|
}
|
||||||
|
if fake.RunRequests[0].InputPaths["transcript"] != normalizedPath {
|
||||||
|
t.Fatalf("transcript input = %q, want normalized transcript path", fake.RunRequests[0].InputPaths["transcript"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyzeSupportsNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
|
||||||
|
env, m, fake := setupAnalyzeEnv(t)
|
||||||
|
paths := env.ArtifactStore.SessionPaths(m.SessionID)
|
||||||
|
fallbackPath := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||||
|
manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json")
|
||||||
|
writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`)
|
||||||
|
writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`)
|
||||||
|
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||||
|
{Kind: "transcript_normalized", LocalPath: manifestPath},
|
||||||
|
})
|
||||||
|
|
||||||
|
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||||
|
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||||
|
Source: "normalized_transcript",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||||
|
|
||||||
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(fake.RunRequests) != 1 {
|
||||||
|
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests))
|
||||||
|
}
|
||||||
|
if fake.RunRequests[0].InputPaths["transcript"] != manifestPath {
|
||||||
|
t.Fatalf("transcript input = %q, want manifest normalized transcript path", fake.RunRequests[0].InputPaths["transcript"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyzeFailsWhenNormalizedTranscriptMissing(t *testing.T) {
|
||||||
|
env, m, fake := setupAnalyzeEnv(t)
|
||||||
|
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||||
|
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||||
|
Source: "normalized_transcript",
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||||
|
|
||||||
|
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "normalized transcript input is unavailable") {
|
||||||
|
t.Fatalf("error = %q, want missing normalized transcript context", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "run normalize stage first") {
|
||||||
|
t.Fatalf("error = %q, want guidance to run normalize stage first", err.Error())
|
||||||
|
}
|
||||||
|
if len(fake.RunRequests) != 0 {
|
||||||
|
t.Fatalf("run requests = %d, want 0 on missing normalized transcript", len(fake.RunRequests))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
|
func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
|
||||||
env, m, _ := setupAnalyzeEnv(t)
|
env, m, _ := setupAnalyzeEnv(t)
|
||||||
paths := env.ArtifactStore.SessionPaths(m.SessionID)
|
paths := env.ArtifactStore.SessionPaths(m.SessionID)
|
||||||
|
|||||||
Reference in New Issue
Block a user