Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 717451512a | |||
| 3ddb3a947b | |||
| c6632d5576 |
@@ -44,7 +44,7 @@ using configured object storage.
|
||||
- Session files must be concrete; unresolved `{{ ... }}` placeholders fail load.
|
||||
- Pipeline defaults are applied before validation.
|
||||
- Campaign and session identities must agree.
|
||||
- Stable files (`speakers_file`, `autocorrect_file`, `glossary_file`) resolve from session overrides when provided, otherwise from campaign defaults.
|
||||
- Stable files (`speakers_file`, `autocorrect_file`, `glossary_file`, `players_file`, `party_file`) resolve from session overrides when provided, otherwise from campaign defaults.
|
||||
- Exactly one audio mode must be configured in session input:
|
||||
- local (`audio_dir` or `audio_files`), or
|
||||
- S3 (`audio_s3.prefix`).
|
||||
@@ -69,6 +69,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
```
|
||||
|
||||
`session.yml` (local audio)
|
||||
@@ -184,12 +186,12 @@ Rules:
|
||||
| `pipeline.normalize.output_path` | string | No | `transcripts/final.json` |
|
||||
| `pipeline.normalize.output_schema` | string | No | `seriatim-intermediate` |
|
||||
| `pipeline.normalize.report` | bool | No | `true` |
|
||||
| `pipeline.trim.enabled` | bool | No | `false` |
|
||||
| `pipeline.trim.output_path` | string | Conditional | required when trim enabled |
|
||||
| `pipeline.trim.bounds.prompt_id` | string | Conditional | required when trim enabled |
|
||||
| `pipeline.trim.enabled` | bool | No | `true` |
|
||||
| `pipeline.trim.output_path` | string | No | `transcripts/final.trimmed.json` |
|
||||
| `pipeline.trim.bounds.prompt_id` | string | No | `dnd.session_bounds` |
|
||||
| `pipeline.trim.bounds.profile_id` | string | No | empty |
|
||||
| `pipeline.trim.bounds.transcript_input_name` | string | Conditional | required when trim enabled |
|
||||
| `pipeline.trim.bounds.output_path` | string | Conditional | required when trim enabled |
|
||||
| `pipeline.trim.bounds.transcript_input_name` | string | No | `transcript` |
|
||||
| `pipeline.trim.bounds.output_path` | string | No | `artifacts/session_bounds.json` |
|
||||
| `pipeline.trim.bounds.timeout` | duration | No | `10m` |
|
||||
| `pipeline.trim.bounds.render_debug` | bool | No | `false` |
|
||||
| `pipeline.trim.bounds.render_output_path` | string | Conditional | required when `render_debug` is true |
|
||||
@@ -198,7 +200,7 @@ Rules:
|
||||
| `pipeline.render.format` | string | No | `markdown` (only supported value) |
|
||||
| `pipeline.render.title` | string | No | empty (falls back to `session.title` when set) |
|
||||
| `pipeline.render.include_timestamps` | bool | No | `true` |
|
||||
| `pipeline.render.include_segment_ids` | bool | No | `false` |
|
||||
| `pipeline.render.include_segment_ids` | bool | No | `true` |
|
||||
| `pipeline.render.include_metadata` | bool | No | `false` |
|
||||
| `pipeline.scriptorium.binary` | string | No | `scriptorium` |
|
||||
| `pipeline.scriptorium.config_path` | string | No | empty |
|
||||
@@ -229,7 +231,7 @@ For each artifact input `pipeline.scriptorium.artifacts.<name>.inputs.<input_nam
|
||||
|
||||
| Field | Type | Required | Rule |
|
||||
| --- | --- | --- | --- |
|
||||
| `source` | string | Yes | built-in runtime source, `narratio.artifact.<name>`, or `narratio.previous_session.artifact.<name>` |
|
||||
| `source` | string | Yes | built-in runtime source, prepared input source, `narratio.artifact.<name>`, or `narratio.previous_session.artifact.<name>` |
|
||||
| `artifact` | string | No | optional passthrough adapter field |
|
||||
| `path` | string | No | optional passthrough adapter field |
|
||||
| `required` | bool | No | optional input requirement |
|
||||
@@ -243,6 +245,8 @@ For each artifact input `pipeline.scriptorium.artifacts.<name>.inputs.<input_nam
|
||||
| `inputs.speakers_file` | string | Yes | stable input default |
|
||||
| `inputs.autocorrect_file` | string | Yes | stable input default |
|
||||
| `inputs.glossary_file` | string | Yes | stable input default |
|
||||
| `inputs.players_file` | string | Yes | stable input default |
|
||||
| `inputs.party_file` | string | Yes | stable input default |
|
||||
|
||||
### Session
|
||||
|
||||
@@ -256,6 +260,8 @@ For each artifact input `pipeline.scriptorium.artifacts.<name>.inputs.<input_nam
|
||||
| `inputs.speakers_file` | string | No | overrides campaign stable input |
|
||||
| `inputs.autocorrect_file` | string | No | overrides campaign stable input |
|
||||
| `inputs.glossary_file` | string | No | overrides campaign stable input |
|
||||
| `inputs.players_file` | string | No | overrides campaign stable input |
|
||||
| `inputs.party_file` | string | No | overrides campaign stable input |
|
||||
| `inputs.audio_dir` | string | Conditional | local audio mode |
|
||||
| `inputs.audio_files[]` | list[string] | Conditional | local audio mode |
|
||||
| `inputs.audio_s3.prefix` | string | Conditional | S3 audio mode |
|
||||
|
||||
@@ -10,6 +10,7 @@ Execute selected configured Scriptorium artifacts in dependency order and materi
|
||||
|
||||
Supported source families:
|
||||
- built-ins: `narratio.transcript.*`, `narratio.bounds.session`
|
||||
- prepared stable inputs: `narratio.input.players`, `narratio.input.party`, `narratio.input.glossary`
|
||||
- configured artifacts: `narratio.artifact.<key>`
|
||||
- previous-session cache: `narratio.previous_session.artifact.<key>`
|
||||
|
||||
@@ -23,12 +24,14 @@ Supported source families:
|
||||
- marks non-executable configured artifacts as reusable when output files already exist.
|
||||
- validates selected artifact dependency order (cycle-safe topo ordering).
|
||||
- resolves required/optional inputs per artifact source definition.
|
||||
- resolves prepared stable input sources from `inputs/*.yml` materialized by `prepare`.
|
||||
- resolves previous-session sources from local `previous/` cache only.
|
||||
- runs optional render-debug, then artifact execution.
|
||||
- validates non-empty output files and materializes canonical outputs.
|
||||
|
||||
## Failure Semantics
|
||||
- required missing configured/previous-session inputs fail.
|
||||
- missing required prepared stable input source includes prepare rerun guidance.
|
||||
- missing required previous-session source includes prepare rerun guidance.
|
||||
- missing required `narratio.transcript.final_markdown` or `narratio.transcript.final_trimmed_markdown` inputs includes render rerun guidance.
|
||||
- dependency cycles or unavailable required dependencies fail.
|
||||
|
||||
@@ -5,7 +5,7 @@ Materialize canonical current-session inputs before processing stages.
|
||||
|
||||
## Inputs
|
||||
- resolved `campaign.yml`, `session.yml`, and pipeline config
|
||||
- stable input files (`speakers`, `autocorrect`, `glossary`)
|
||||
- stable input files (`speakers`, `autocorrect`, `glossary`, `players`, `party`)
|
||||
- audio source:
|
||||
- local `audio_dir`/`audio_files`, or
|
||||
- S3 `audio_s3.prefix`
|
||||
@@ -18,6 +18,8 @@ Materialize canonical current-session inputs before processing stages.
|
||||
- `inputs/speakers.yml`
|
||||
- `inputs/autocorrect.yml`
|
||||
- `inputs/glossary.yml`
|
||||
- `inputs/players.yml`
|
||||
- `inputs/party.yml`
|
||||
- `audio/*.flac`
|
||||
- optional `previous/manifest.json`
|
||||
- optional `previous/artifacts/**`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Stage: trim
|
||||
|
||||
## Purpose
|
||||
Produce a final-trimmed transcript; optionally generate bounds-driven trim.
|
||||
Produce a final-trimmed transcript. By default, the stage generates bounds and applies a bounds-driven trim.
|
||||
|
||||
## Inputs
|
||||
- `transcripts/final.json`
|
||||
@@ -11,9 +11,6 @@ Produce a final-trimmed transcript; optionally generate bounds-driven trim.
|
||||
- when trim enabled: `artifacts/session_bounds.json`
|
||||
|
||||
## Key Behavior
|
||||
When `trim.enabled=false`:
|
||||
- copies normalized transcript to trimmed output.
|
||||
|
||||
When `trim.enabled=true`:
|
||||
- runs Scriptorium bounds artifact generation;
|
||||
- optionally runs render-debug output generation;
|
||||
@@ -22,6 +19,9 @@ When `trim.enabled=true`:
|
||||
- either copies unchanged transcript or runs Seriatim trim;
|
||||
- validates trimmed transcript and materializes bounds output.
|
||||
|
||||
When `trim.enabled=false`:
|
||||
- copies normalized transcript to trimmed output.
|
||||
|
||||
## Invariants
|
||||
- normalized transcript is required input.
|
||||
- bounds output exists only in enabled trim path.
|
||||
|
||||
@@ -36,6 +36,8 @@ narratio session init 2026-04-04 --remote --force
|
||||
|
||||
If `campaign.yml` sets `session_template_file`, `session init` renders it. Template variables must resolve to concrete values.
|
||||
|
||||
Campaigns must provide stable input files for speakers, autocorrect, glossary, players, and party. Session files may override those paths for one session. The `prepare` stage materializes them under `inputs/`; configured Scriptorium artifacts can reference prepared `players`, `party`, and `glossary` files with `narratio.input.players`, `narratio.input.party`, and `narratio.input.glossary`.
|
||||
|
||||
## Standard Session Workflow
|
||||
|
||||
1. Select pipeline/campaign/session config.
|
||||
|
||||
@@ -4,3 +4,5 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
|
||||
2
examples/campaigns/sample-campaign/party.yml
Normal file
2
examples/campaigns/sample-campaign/party.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
- name: Example Hero
|
||||
type: pc
|
||||
2
examples/campaigns/sample-campaign/players.yml
Normal file
2
examples/campaigns/sample-campaign/players.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
- name: Example Player
|
||||
role: player
|
||||
@@ -110,17 +110,16 @@ normalize:
|
||||
report: true
|
||||
|
||||
trim:
|
||||
# Keep disabled unless bounds prompt integration is configured.
|
||||
enabled: false
|
||||
# Optional; defaults shown explicitly.
|
||||
enabled: true
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd.session_bounds
|
||||
profile_id: local-fast
|
||||
profile_id: ""
|
||||
transcript_input_name: transcript
|
||||
output_path: reports/session_bounds.json
|
||||
output_path: artifacts/session_bounds.json
|
||||
timeout: 10m
|
||||
render_debug: false
|
||||
render_output_path: reports/session_bounds.render.json
|
||||
seriatim:
|
||||
report: false
|
||||
|
||||
@@ -144,6 +143,15 @@ scriptorium:
|
||||
previous_recap:
|
||||
source: narratio.previous_session.artifact.session_recap
|
||||
required: false
|
||||
players:
|
||||
source: narratio.input.players
|
||||
required: true
|
||||
party:
|
||||
source: narratio.input.party
|
||||
required: true
|
||||
glossary:
|
||||
source: narratio.input.glossary
|
||||
required: false
|
||||
vars:
|
||||
session_id: true
|
||||
session_date: true
|
||||
|
||||
@@ -71,9 +71,6 @@ normalize:
|
||||
output_schema: seriatim-intermediate
|
||||
report: true
|
||||
|
||||
trim:
|
||||
enabled: false
|
||||
|
||||
scriptorium:
|
||||
binary: scriptorium
|
||||
config_path: /usr/local/etc/scriptorium/config.yml
|
||||
@@ -93,6 +90,15 @@ scriptorium:
|
||||
previous_recap:
|
||||
source: narratio.previous_session.artifact.session_recap
|
||||
required: false
|
||||
players:
|
||||
source: narratio.input.players
|
||||
required: true
|
||||
party:
|
||||
source: narratio.input.party
|
||||
required: true
|
||||
glossary:
|
||||
source: narratio.input.glossary
|
||||
required: false
|
||||
vars:
|
||||
session_id: true
|
||||
session_date: true
|
||||
|
||||
@@ -577,9 +577,9 @@ func buildRenderArgs(req RenderRequest, format string) []string {
|
||||
"--input-file", req.InputTranscriptPath,
|
||||
"--output-file", req.OutputRenderedPath,
|
||||
"--format", format,
|
||||
"--include-timestamps", strconv.FormatBool(req.IncludeTimestamps),
|
||||
"--include-segment-ids", strconv.FormatBool(req.IncludeSegmentIDs),
|
||||
"--include-metadata", strconv.FormatBool(req.IncludeMetadata),
|
||||
"--include-timestamps=" + strconv.FormatBool(req.IncludeTimestamps),
|
||||
"--include-segment-ids=" + strconv.FormatBool(req.IncludeSegmentIDs),
|
||||
"--include-metadata=" + strconv.FormatBool(req.IncludeMetadata),
|
||||
}
|
||||
if strings.TrimSpace(req.Title) != "" {
|
||||
args = append(args, "--title", req.Title)
|
||||
|
||||
@@ -628,9 +628,9 @@ func TestSubprocessRunnerRenderSuccessInvocationAndProvenance(t *testing.T) {
|
||||
"--input-file", req.InputTranscriptPath,
|
||||
"--output-file", req.OutputRenderedPath,
|
||||
"--format", req.Format,
|
||||
"--include-timestamps", "true",
|
||||
"--include-segment-ids", "false",
|
||||
"--include-metadata", "true",
|
||||
"--include-timestamps=true",
|
||||
"--include-segment-ids=true",
|
||||
"--include-metadata=false",
|
||||
"--title", req.Title,
|
||||
}
|
||||
if strings.Join(rec.Args, "\n") != strings.Join(wantArgs, "\n") {
|
||||
@@ -946,8 +946,8 @@ func renderReqForTest(t *testing.T) RenderRequest {
|
||||
Format: "markdown",
|
||||
Title: "Session 42",
|
||||
IncludeTimestamps: true,
|
||||
IncludeSegmentIDs: false,
|
||||
IncludeMetadata: true,
|
||||
IncludeSegmentIDs: true,
|
||||
IncludeMetadata: false,
|
||||
GeneratedConfigPath: filepath.Join(dir, "seriatim.render.generated.yml"),
|
||||
StdoutLogPath: filepath.Join(dir, "seriatim.render.stdout.log"),
|
||||
StderrLogPath: filepath.Join(dir, "seriatim.render.stderr.log"),
|
||||
|
||||
@@ -227,6 +227,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline.yml: %v", err)
|
||||
@@ -290,6 +292,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline.yml: %v", err)
|
||||
@@ -385,10 +389,14 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`)
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "players.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "party.yml"), "[]\n")
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
@@ -466,10 +474,13 @@ func writeValidConfigFiles(t *testing.T, workspaceRoot string, transcribeURL ...
|
||||
url = transcribeURL[0]
|
||||
}
|
||||
seriatimBinary := writeSeriatimAppTestWrapper(t)
|
||||
scriptoriumBinary := writeScriptoriumAppTestWrapper(t)
|
||||
auditaBinary := writeAuditaAppTestWrapper(t)
|
||||
t.Setenv("GO_WANT_APP_SERIATIM_HELPER", "1")
|
||||
t.Setenv("GO_WANT_APP_SCRIPTORIUM_HELPER", "1")
|
||||
t.Setenv("GO_WANT_APP_AUDITA_HELPER", "1")
|
||||
t.Setenv("AUDITA_LLM_API_KEY", "test-audita-key")
|
||||
t.Setenv("PATH", filepath.Dir(scriptoriumBinary)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
pipelineYAML := `workspace:
|
||||
root: ` + workspaceRoot + `
|
||||
@@ -514,6 +525,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
@@ -532,6 +545,8 @@ inputs:
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "players.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "party.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
||||
|
||||
return pipelinePath, campaignPath, sessionPath
|
||||
@@ -545,6 +560,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||
t.Fatalf("write campaign.yml: %v", err)
|
||||
@@ -591,6 +608,60 @@ func writeSeriatimAppTestWrapper(t *testing.T) string {
|
||||
return path
|
||||
}
|
||||
|
||||
func writeScriptoriumAppTestWrapper(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(), "scriptorium")
|
||||
content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestScriptoriumAppHelper -- \"$@\"\n"
|
||||
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
||||
t.Fatalf("WriteFile(%q): %v", path, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestScriptoriumAppHelper(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_APP_SCRIPTORIUM_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)
|
||||
}
|
||||
runArgs := args[start:]
|
||||
|
||||
outputPath := appSeriatimFlagValue(runArgs, "--out")
|
||||
if strings.TrimSpace(outputPath) == "" {
|
||||
outputPath = appSeriatimFlagValue(runArgs, "--output")
|
||||
}
|
||||
if strings.TrimSpace(outputPath) == "" {
|
||||
_, _ = os.Stderr.WriteString("missing output flag\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(`{"trim_action":"copy","warnings":[]}`), 0o644); err != nil {
|
||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("write output: %v\n", err))
|
||||
os.Exit(2)
|
||||
}
|
||||
_, _ = os.Stdout.WriteString("scriptorium helper stdout\n")
|
||||
_, _ = os.Stderr.WriteString("scriptorium helper stderr\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestSeriatimAppHelper(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_APP_SERIATIM_HELPER") != "1" {
|
||||
return
|
||||
|
||||
@@ -109,12 +109,16 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`), 0o644); err != nil {
|
||||
t.Fatalf("write explicit campaign: %v", err)
|
||||
}
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n")
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "players.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "party.yml"), "[]\n")
|
||||
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
@@ -445,6 +449,9 @@ inputs:
|
||||
if !strings.Contains(stdout.String(), "OK audio") {
|
||||
t.Fatalf("stdout = %q, want OK audio", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "OK inputs players:") || !strings.Contains(stdout.String(), "OK inputs party:") {
|
||||
t.Fatalf("stdout = %q, want players and party input findings", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
|
||||
@@ -862,6 +869,8 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
|
||||
"narratio.transcript.final_trimmed locked",
|
||||
"narratio.transcript.final_trimmed locked remote=published",
|
||||
"narratio.transcript.final dest=transcripts/full.json remote=published",
|
||||
"Stable input players:",
|
||||
"Stable input party:",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("stdout = %q, want %q", out, want)
|
||||
|
||||
@@ -57,6 +57,8 @@ func inspectStableInputs(cfg *config.Config) []stableInputCheck {
|
||||
{name: "speakers", in: cfg.StableInputs.SpeakersFile},
|
||||
{name: "autocorrect", in: cfg.StableInputs.AutocorrectFile},
|
||||
{name: "glossary", in: cfg.StableInputs.GlossaryFile},
|
||||
{name: "players", in: cfg.StableInputs.PlayersFile},
|
||||
{name: "party", in: cfg.StableInputs.PartyFile},
|
||||
}
|
||||
out := make([]stableInputCheck, 0, len(items))
|
||||
for _, item := range items {
|
||||
|
||||
@@ -118,6 +118,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline.yml: %v", err)
|
||||
|
||||
@@ -397,6 +397,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline config: %v", err)
|
||||
@@ -407,6 +409,8 @@ inputs:
|
||||
mustWriteTestFile(t, filepath.Join(dir, "speakers.yml"), "alice: alice.flac\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "players.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "party.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
||||
|
||||
return pipelinePath, campaignPath, sessionPath
|
||||
|
||||
@@ -236,8 +236,8 @@ func TestRunStageTrimExecutes(t *testing.T) {
|
||||
if m.Stages["trim"] == nil || m.Stages["trim"].Status != manifest.StatusSucceeded {
|
||||
t.Fatalf("trim stage = %#v, want succeeded", m.Stages["trim"])
|
||||
}
|
||||
if m.Stages["trim"].Metadata == nil || m.Stages["trim"].Metadata["trim_action"] != "copy_disabled" {
|
||||
t.Fatalf("trim stage metadata = %#v, want trim_action=copy_disabled", m.Stages["trim"].Metadata)
|
||||
if m.Stages["trim"].Metadata == nil || m.Stages["trim"].Metadata["trim_action"] != "copy" {
|
||||
t.Fatalf("trim stage metadata = %#v, want trim_action=copy", m.Stages["trim"].Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1024,11 +1024,13 @@ func testConfig(t *testing.T) *config.Config {
|
||||
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
||||
|
||||
mustWriteFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
||||
mustWriteFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n")
|
||||
mustWriteFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n")
|
||||
mustWriteFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\ninputs:\n audio_dir: ./audio\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "players.yml"), "[]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "party.yml"), "[]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "audio", "alice.flac"), "audio")
|
||||
|
||||
return &config.Config{
|
||||
@@ -1053,6 +1055,16 @@ func testConfig(t *testing.T) *config.Config {
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PlayersFile: config.ResolvedInputFile{
|
||||
Path: "./players.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PartyFile: config.ResolvedInputFile{
|
||||
Path: "./party.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
},
|
||||
Session: &config.SessionConfig{
|
||||
SessionID: "2026-05-03",
|
||||
@@ -1062,6 +1074,8 @@ func testConfig(t *testing.T) *config.Config {
|
||||
SpeakersFile: "./speakers.yml",
|
||||
AutocorrectFile: "./autocorrect.yml",
|
||||
GlossaryFile: "./glossary.yml",
|
||||
PlayersFile: "./players.yml",
|
||||
PartyFile: "./party.yml",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1084,6 +1098,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
sessionYAML := `session_id: 2026-05-03
|
||||
campaign: sample-campaign
|
||||
@@ -1092,6 +1108,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
mustWriteFile(t, pipelinePath, pipelineYAML)
|
||||
mustWriteFile(t, campaignPath, campaignYAML)
|
||||
|
||||
@@ -22,6 +22,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionTemplate), 0o644); err != nil {
|
||||
t.Fatalf("write session template: %v", err)
|
||||
@@ -75,6 +77,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
|
||||
@@ -13,6 +13,10 @@ import (
|
||||
const (
|
||||
SourceBoundsSession = "narratio.bounds.session"
|
||||
|
||||
SourceInputPlayers = "narratio.input.players"
|
||||
SourceInputParty = "narratio.input.party"
|
||||
SourceInputGlossary = "narratio.input.glossary"
|
||||
|
||||
configuredSourcePrefix = "narratio.artifact."
|
||||
previousConfiguredSrcPrefix = "narratio.previous_session.artifact."
|
||||
)
|
||||
@@ -31,6 +35,7 @@ const (
|
||||
SourceKindBuiltIn SourceKind = "built_in"
|
||||
SourceKindConfiguredArtifact SourceKind = "configured_artifact"
|
||||
SourceKindPreviousArtifact SourceKind = "previous_session_configured_artifact"
|
||||
SourceKindStableInput SourceKind = "stable_input"
|
||||
)
|
||||
|
||||
// Source describes one normalized artifact source identifier.
|
||||
@@ -118,6 +123,11 @@ func DescribeScriptoriumInputSource(source string) (ScriptoriumInputSourceDescri
|
||||
if trimmed == "" {
|
||||
return ScriptoriumInputSourceDescriptor{}, ErrUnsupportedScriptoriumInputSource
|
||||
}
|
||||
if IsStableInputSource(trimmed) {
|
||||
return ScriptoriumInputSourceDescriptor{
|
||||
Source: Source{ID: trimmed, Kind: SourceKindStableInput},
|
||||
}, nil
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "narratio.previous_session.artifact") {
|
||||
descriptor, err := DescribePreviousSessionSource(trimmed)
|
||||
if err != nil {
|
||||
@@ -140,6 +150,17 @@ func DescribeScriptoriumInputSource(source string) (ScriptoriumInputSourceDescri
|
||||
return ScriptoriumInputSourceDescriptor{Source: classified}, nil
|
||||
}
|
||||
|
||||
// IsStableInputSource reports whether source is a prepared stable input source
|
||||
// available only to Scriptorium input resolution.
|
||||
func IsStableInputSource(source string) bool {
|
||||
switch strings.TrimSpace(source) {
|
||||
case SourceInputPlayers, SourceInputParty, SourceInputGlossary:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// DescribePreviousSessionSource validates a canonical previous-session source id
|
||||
// and returns both previous and configured-source vocabulary descriptors.
|
||||
func DescribePreviousSessionSource(source string) (PreviousSessionSourceDescriptor, error) {
|
||||
|
||||
@@ -112,6 +112,9 @@ func TestDescribeScriptoriumInputSource(t *testing.T) {
|
||||
}{
|
||||
{name: "built in", source: "narratio.transcript.final_trimmed", wantKind: SourceKindBuiltIn},
|
||||
{name: "built in markdown", source: "narratio.transcript.final_markdown", wantKind: SourceKindBuiltIn},
|
||||
{name: "prepared players input", source: "narratio.input.players", wantKind: SourceKindStableInput},
|
||||
{name: "prepared party input", source: "narratio.input.party", wantKind: SourceKindStableInput},
|
||||
{name: "prepared glossary input", source: "narratio.input.glossary", wantKind: SourceKindStableInput},
|
||||
{name: "configured", source: "narratio.artifact.session_recap", wantKind: SourceKindConfiguredArtifact, wantKey: "session_recap"},
|
||||
{name: "previous", source: "narratio.previous_session.artifact.session_recap", wantKind: SourceKindPreviousArtifact, wantKey: "session_recap", wantPrev: true},
|
||||
{name: "invalid previous", source: "narratio.previous_session.artifact.", wantErr: ErrInvalidPreviousSessionSource},
|
||||
|
||||
@@ -37,7 +37,7 @@ notification:
|
||||
|
||||
func TestCampaignStrictDecodeAcceptsCampaignID(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestCampaignStrictDecodeAcceptsCampaignID(t *testing.T) {
|
||||
|
||||
func TestCampaignStrictDecodeRejectsLegacyCampaignField(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestCampaignStrictDecodeRejectsLegacyCampaignField(t *testing.T) {
|
||||
|
||||
func TestCampaignStrictDecodeRejectsUnknownFields(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\nunknown: true\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"campaign_id: sample-campaign\nunknown: true\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestCampaignStrictDecodeRejectsUnknownFields(t *testing.T) {
|
||||
|
||||
func TestCampaignStrictDecodeAcceptsSessionTemplateFile(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\nsession_template_file: ./session.template.yml\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"campaign_id: sample-campaign\nsession_template_file: ./session.template.yml\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestCampaignStrictDecodeAcceptsSessionTemplateFile(t *testing.T) {
|
||||
|
||||
func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n players_file: ./campaign-players.yml\n party_file: ./campaign-party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -114,12 +114,14 @@ func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
|
||||
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./campaign-speakers.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "./campaign-players.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./campaign-party.yml", campaignPath, "campaign_config")
|
||||
}
|
||||
|
||||
func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n",
|
||||
"session_id: 2026-05-03\ncampaign: sample-campaign\ninputs:\n audio_dir: ./audio\n speakers_file: ./session-speakers.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n players_file: ./campaign-players.yml\n party_file: ./campaign-party.yml\n",
|
||||
"session_id: 2026-05-03\ncampaign: sample-campaign\ninputs:\n audio_dir: ./audio\n speakers_file: ./session-speakers.yml\n players_file: ./session-players.yml\n party_file: ./session-party.yml\n",
|
||||
)
|
||||
|
||||
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
@@ -132,11 +134,32 @@ func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
|
||||
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./session-speakers.yml", sessionPath, "session_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "./session-players.yml", sessionPath, "session_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./session-party.yml", sessionPath, "session_config")
|
||||
}
|
||||
|
||||
func TestCampaignRequiresPlayersAndPartyInputs(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||
}
|
||||
err = Validate(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "campaign.inputs.players_file is required") {
|
||||
t.Fatalf("error = %q, want players_file required", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCampaignSessionMismatchFails(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ncampaign: other-campaign\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -151,7 +174,7 @@ func TestCampaignSessionMismatchFails(t *testing.T) {
|
||||
|
||||
func TestLoadMissingCampaignFileFails(t *testing.T) {
|
||||
pipelinePath, _, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
missingCampaignPath := filepath.Join(filepath.Dir(sessionPath), "missing-campaign.yml")
|
||||
|
||||
@@ -51,6 +51,8 @@ type CampaignInputsConfig struct {
|
||||
SpeakersFile string `yaml:"speakers_file"`
|
||||
AutocorrectFile string `yaml:"autocorrect_file"`
|
||||
GlossaryFile string `yaml:"glossary_file"`
|
||||
PlayersFile string `yaml:"players_file"`
|
||||
PartyFile string `yaml:"party_file"`
|
||||
}
|
||||
|
||||
// SessionConfig contains per-session inputs and metadata.
|
||||
@@ -188,7 +190,7 @@ type NormalizeConfig struct {
|
||||
|
||||
// TrimConfig configures trim-stage transcript boundary behavior.
|
||||
type TrimConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
OutputPath string `yaml:"output_path"`
|
||||
Bounds TrimBoundsConfig `yaml:"bounds"`
|
||||
Seriatim TrimSeriatimConfig `yaml:"seriatim"`
|
||||
@@ -216,7 +218,7 @@ type RenderConfig struct {
|
||||
Format string `yaml:"format"`
|
||||
Title string `yaml:"title"`
|
||||
IncludeTimestamps *bool `yaml:"include_timestamps"`
|
||||
IncludeSegmentIDs bool `yaml:"include_segment_ids"`
|
||||
IncludeSegmentIDs *bool `yaml:"include_segment_ids"`
|
||||
IncludeMetadata bool `yaml:"include_metadata"`
|
||||
}
|
||||
|
||||
@@ -265,6 +267,8 @@ type SessionInputsConfig struct {
|
||||
SpeakersFile string `yaml:"speakers_file"`
|
||||
AutocorrectFile string `yaml:"autocorrect_file"`
|
||||
GlossaryFile string `yaml:"glossary_file"`
|
||||
PlayersFile string `yaml:"players_file"`
|
||||
PartyFile string `yaml:"party_file"`
|
||||
}
|
||||
|
||||
// SessionAudioS3Input configures S3 session-audio input discovery.
|
||||
@@ -278,6 +282,8 @@ type ResolvedStableInputs struct {
|
||||
SpeakersFile ResolvedInputFile
|
||||
AutocorrectFile ResolvedInputFile
|
||||
GlossaryFile ResolvedInputFile
|
||||
PlayersFile ResolvedInputFile
|
||||
PartyFile ResolvedInputFile
|
||||
}
|
||||
|
||||
// ResolvedInputFile records one merged config path and its source config file.
|
||||
|
||||
@@ -38,14 +38,19 @@ const (
|
||||
DefaultScriptoriumTimeout = "10m"
|
||||
DefaultScriptoriumArtifactOutputRoot = "artifacts"
|
||||
|
||||
DefaultTrimBoundsTimeout = "10m"
|
||||
DefaultTrimSeriatimReport = false
|
||||
DefaultRenderEnabled = true
|
||||
DefaultRenderFormat = "markdown"
|
||||
DefaultRenderTitle = ""
|
||||
DefaultRenderTimestamps = true
|
||||
DefaultRenderSegmentIDs = false
|
||||
DefaultRenderMetadata = false
|
||||
DefaultTrimEnabled = true
|
||||
DefaultTrimOutputPath = artifactmodel.TranscriptPathFinalTrimmed
|
||||
DefaultTrimBoundsPromptID = "dnd.session_bounds"
|
||||
DefaultTrimBoundsTranscriptInputName = "transcript"
|
||||
DefaultTrimBoundsOutputPath = "artifacts/session_bounds.json"
|
||||
DefaultTrimBoundsTimeout = "10m"
|
||||
DefaultTrimSeriatimReport = false
|
||||
DefaultRenderEnabled = true
|
||||
DefaultRenderFormat = "markdown"
|
||||
DefaultRenderTitle = ""
|
||||
DefaultRenderTimestamps = true
|
||||
DefaultRenderSegmentIDs = true
|
||||
DefaultRenderMetadata = false
|
||||
|
||||
DefaultNormalizeOutputPath = artifactmodel.TranscriptPathFinal
|
||||
DefaultNormalizeOutputSchema = "seriatim-intermediate"
|
||||
|
||||
@@ -226,11 +226,25 @@ func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig
|
||||
campaignPath,
|
||||
sessionPath,
|
||||
),
|
||||
PlayersFile: selectStableInput(
|
||||
campaignCfg.Inputs.PlayersFile,
|
||||
sessionCfg.Inputs.PlayersFile,
|
||||
campaignPath,
|
||||
sessionPath,
|
||||
),
|
||||
PartyFile: selectStableInput(
|
||||
campaignCfg.Inputs.PartyFile,
|
||||
sessionCfg.Inputs.PartyFile,
|
||||
campaignPath,
|
||||
sessionPath,
|
||||
),
|
||||
}
|
||||
|
||||
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
|
||||
sessionCfg.Inputs.AutocorrectFile = stable.AutocorrectFile.Path
|
||||
sessionCfg.Inputs.GlossaryFile = stable.GlossaryFile.Path
|
||||
sessionCfg.Inputs.PlayersFile = stable.PlayersFile.Path
|
||||
sessionCfg.Inputs.PartyFile = stable.PartyFile.Path
|
||||
return stable, nil
|
||||
}
|
||||
|
||||
@@ -336,7 +350,13 @@ func applyPipelineDefaults(cfg *PipelineConfig) {
|
||||
cfg.Normalize = &NormalizeConfig{}
|
||||
}
|
||||
applyNormalizeDefaults(cfg.Normalize)
|
||||
if cfg.Trim == nil {
|
||||
cfg.Trim = &TrimConfig{}
|
||||
}
|
||||
applyTrimDefaults(cfg.Trim)
|
||||
if trimEnabled(cfg.Trim) && cfg.Scriptorium == nil {
|
||||
cfg.Scriptorium = &ScriptoriumConfig{}
|
||||
}
|
||||
applyRenderDefaults(&cfg.Render)
|
||||
applyScriptoriumDefaults(cfg.Scriptorium)
|
||||
}
|
||||
@@ -500,6 +520,21 @@ func applyTrimDefaults(cfg *TrimConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if cfg.Enabled == nil {
|
||||
cfg.Enabled = boolPtr(DefaultTrimEnabled)
|
||||
}
|
||||
if strings.TrimSpace(cfg.OutputPath) == "" {
|
||||
cfg.OutputPath = DefaultTrimOutputPath
|
||||
}
|
||||
if strings.TrimSpace(cfg.Bounds.PromptID) == "" {
|
||||
cfg.Bounds.PromptID = DefaultTrimBoundsPromptID
|
||||
}
|
||||
if strings.TrimSpace(cfg.Bounds.TranscriptInputName) == "" {
|
||||
cfg.Bounds.TranscriptInputName = DefaultTrimBoundsTranscriptInputName
|
||||
}
|
||||
if strings.TrimSpace(cfg.Bounds.OutputPath) == "" {
|
||||
cfg.Bounds.OutputPath = DefaultTrimBoundsOutputPath
|
||||
}
|
||||
if cfg.Bounds.Timeout == "" {
|
||||
cfg.Bounds.Timeout = DefaultTrimBoundsTimeout
|
||||
}
|
||||
@@ -508,6 +543,10 @@ func applyTrimDefaults(cfg *TrimConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
func trimEnabled(cfg *TrimConfig) bool {
|
||||
return cfg != nil && cfg.Enabled != nil && *cfg.Enabled
|
||||
}
|
||||
|
||||
func applyRenderDefaults(cfg **RenderConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
@@ -527,6 +566,9 @@ func applyRenderDefaults(cfg **RenderConfig) {
|
||||
if (*cfg).IncludeTimestamps == nil {
|
||||
(*cfg).IncludeTimestamps = boolPtr(DefaultRenderTimestamps)
|
||||
}
|
||||
if (*cfg).IncludeSegmentIDs == nil {
|
||||
(*cfg).IncludeSegmentIDs = boolPtr(DefaultRenderSegmentIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func applyNormalizeDefaults(cfg *NormalizeConfig) {
|
||||
|
||||
@@ -36,6 +36,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
checkDefault: true,
|
||||
wantRoot: "/tmp/narratio",
|
||||
@@ -55,6 +57,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
checkDefault: true,
|
||||
wantRoot: "/tmp/narratio",
|
||||
@@ -72,6 +76,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
checkDefault: true,
|
||||
wantRoot: DefaultWorkspaceRoot,
|
||||
@@ -88,6 +94,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantLoadErr: "pipeline file",
|
||||
},
|
||||
@@ -106,6 +114,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantLoadErr: "strict decode failed",
|
||||
},
|
||||
@@ -123,6 +133,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantLoadErr: "strict decode failed",
|
||||
},
|
||||
@@ -143,6 +155,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantLoadErr: "strict decode failed",
|
||||
},
|
||||
@@ -163,6 +177,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.secrets.env_dir must be non-empty when pipeline.secrets is configured",
|
||||
},
|
||||
@@ -177,6 +193,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
unknown_field: true
|
||||
`,
|
||||
wantLoadErr: "session file",
|
||||
@@ -196,6 +214,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "session config \"session.yml\" invalid: session.session_id is required",
|
||||
},
|
||||
@@ -215,6 +235,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -233,6 +255,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "session config \"session.yml\" invalid: session.previous_session_id must not equal session.session_id",
|
||||
},
|
||||
@@ -250,6 +274,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.transcribe_url is required",
|
||||
},
|
||||
@@ -268,6 +294,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.transcribe_url must be a valid URL",
|
||||
},
|
||||
@@ -287,6 +315,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.timeout must be a valid duration",
|
||||
},
|
||||
@@ -306,6 +336,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.retry_delay must be a valid duration",
|
||||
},
|
||||
@@ -325,6 +357,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.retries must be >= 0",
|
||||
},
|
||||
@@ -344,6 +378,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.whisperx.concurrency must be > 0",
|
||||
},
|
||||
@@ -363,6 +399,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantLoadErr: "strict decode failed",
|
||||
},
|
||||
@@ -383,6 +421,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantLoadErr: "strict decode failed",
|
||||
},
|
||||
@@ -401,6 +441,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -419,6 +461,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.seriatim.timeout must be a valid duration",
|
||||
},
|
||||
@@ -438,6 +482,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.seriatim.output_schema must be one of: seriatim-minimal, seriatim-intermediate, seriatim-full",
|
||||
},
|
||||
@@ -457,6 +503,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.seriatim.coalesce_gap must be >= 0",
|
||||
},
|
||||
@@ -477,6 +525,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.seriatim.env.overlap_word_run_gap must be > 0 when provided",
|
||||
},
|
||||
@@ -498,6 +548,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantLoadErr: "strict decode failed",
|
||||
},
|
||||
@@ -518,6 +570,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -538,6 +592,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.timeout must be a valid duration",
|
||||
},
|
||||
@@ -559,6 +615,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -581,6 +639,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.modules[1] must be non-empty",
|
||||
},
|
||||
@@ -604,6 +664,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.modules[1] must be one of: glossary, homophones, spoken_word, grammar",
|
||||
},
|
||||
@@ -625,6 +687,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.base_url must be a valid URL",
|
||||
},
|
||||
@@ -646,6 +710,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantLoadErr: "strict decode failed",
|
||||
},
|
||||
@@ -667,6 +733,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.total_llm_concurrency must be > 0",
|
||||
},
|
||||
@@ -688,6 +756,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.proposal_llm_concurrency must be > 0",
|
||||
},
|
||||
@@ -709,6 +779,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.validation_llm_concurrency must be > 0",
|
||||
},
|
||||
@@ -730,6 +802,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.output_schema must be one of: bare-segments, audita-v1",
|
||||
},
|
||||
@@ -751,6 +825,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.work_dir_retention must be one of: always, auto, never",
|
||||
},
|
||||
@@ -916,7 +992,16 @@ func TestValidateMissingAudioSource(t *testing.T) {
|
||||
Report: boolPtr(true),
|
||||
},
|
||||
},
|
||||
Campaign: &CampaignConfig{CampaignID: "sample-campaign"},
|
||||
Campaign: &CampaignConfig{
|
||||
CampaignID: "sample-campaign",
|
||||
Inputs: CampaignInputsConfig{
|
||||
SpeakersFile: "speakers.yml",
|
||||
AutocorrectFile: "autocorrect.yml",
|
||||
GlossaryFile: "glossary.yml",
|
||||
PlayersFile: "players.yml",
|
||||
PartyFile: "party.yml",
|
||||
},
|
||||
},
|
||||
Session: &SessionConfig{
|
||||
SessionID: "2026-05-03",
|
||||
Campaign: "sample-campaign",
|
||||
@@ -924,6 +1009,8 @@ func TestValidateMissingAudioSource(t *testing.T) {
|
||||
SpeakersFile: "speakers.yml",
|
||||
AutocorrectFile: "autocorrect.yml",
|
||||
GlossaryFile: "glossary.yml",
|
||||
PlayersFile: "players.yml",
|
||||
PartyFile: "party.yml",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1009,6 +1096,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||
t.Fatalf("write campaign.yml: %v", err)
|
||||
|
||||
@@ -30,8 +30,8 @@ func TestRenderLoadAndValidate(t *testing.T) {
|
||||
if cfg.Pipeline.Render.IncludeTimestamps == nil || !*cfg.Pipeline.Render.IncludeTimestamps {
|
||||
t.Fatalf("render.include_timestamps = %#v, want true", cfg.Pipeline.Render.IncludeTimestamps)
|
||||
}
|
||||
if cfg.Pipeline.Render.IncludeSegmentIDs {
|
||||
t.Fatalf("render.include_segment_ids = true, want false")
|
||||
if cfg.Pipeline.Render.IncludeSegmentIDs == nil || !*cfg.Pipeline.Render.IncludeSegmentIDs {
|
||||
t.Fatalf("render.include_segment_ids = %#v, want true", cfg.Pipeline.Render.IncludeSegmentIDs)
|
||||
}
|
||||
if cfg.Pipeline.Render.IncludeMetadata {
|
||||
t.Fatalf("render.include_metadata = true, want false")
|
||||
@@ -59,14 +59,26 @@ func TestRenderLoadAndValidate(t *testing.T) {
|
||||
if cfg.Pipeline.Render.IncludeTimestamps == nil || *cfg.Pipeline.Render.IncludeTimestamps {
|
||||
t.Fatalf("render.include_timestamps = %#v, want false", cfg.Pipeline.Render.IncludeTimestamps)
|
||||
}
|
||||
if !cfg.Pipeline.Render.IncludeSegmentIDs {
|
||||
t.Fatalf("render.include_segment_ids = false, want true")
|
||||
if cfg.Pipeline.Render.IncludeSegmentIDs == nil || !*cfg.Pipeline.Render.IncludeSegmentIDs {
|
||||
t.Fatalf("render.include_segment_ids = %#v, want true", cfg.Pipeline.Render.IncludeSegmentIDs)
|
||||
}
|
||||
if !cfg.Pipeline.Render.IncludeMetadata {
|
||||
t.Fatalf("render.include_metadata = false, want true")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "explicit segment ids false overrides default",
|
||||
renderYAML: `render:
|
||||
include_segment_ids: false
|
||||
`,
|
||||
assert: func(t *testing.T, cfg *Config) {
|
||||
t.Helper()
|
||||
if cfg.Pipeline.Render.IncludeSegmentIDs == nil || *cfg.Pipeline.Render.IncludeSegmentIDs {
|
||||
t.Fatalf("render.include_segment_ids = %#v, want false", cfg.Pipeline.Render.IncludeSegmentIDs)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid render format fails",
|
||||
renderYAML: `render:
|
||||
|
||||
@@ -215,6 +215,27 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
|
||||
transcript_markdown:
|
||||
source: narratio.transcript.final_markdown
|
||||
required: true
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "prepared stable input sources are accepted",
|
||||
scriptoriumYAML: `scriptorium:
|
||||
binary: scriptorium
|
||||
artifacts:
|
||||
session_recap:
|
||||
enabled: true
|
||||
prompt_id: dnd.session_recap
|
||||
output_path: artifacts/session_recap.md
|
||||
inputs:
|
||||
players:
|
||||
source: narratio.input.players
|
||||
required: true
|
||||
party:
|
||||
source: narratio.input.party
|
||||
required: true
|
||||
glossary:
|
||||
source: narratio.input.glossary
|
||||
required: false
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -575,4 +596,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
|
||||
@@ -17,6 +17,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
@@ -40,6 +42,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
@@ -65,6 +69,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
@@ -93,6 +99,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
@@ -124,6 +132,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
@@ -148,6 +158,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
|
||||
@@ -238,6 +238,15 @@ func TestPublishOutputValidation(t *testing.T) {
|
||||
`,
|
||||
wantErr: "source \"narratio.unknown\" is unsupported",
|
||||
},
|
||||
{
|
||||
name: "prepared input source rejected",
|
||||
ruleYML: `publish:
|
||||
outputs:
|
||||
- source: "narratio.input.players"
|
||||
dest: "inputs/players.yml"
|
||||
`,
|
||||
wantErr: "source \"narratio.input.players\" is unsupported",
|
||||
},
|
||||
{
|
||||
name: "duplicate destination rejected",
|
||||
ruleYML: `publish:
|
||||
@@ -426,6 +435,15 @@ publish:
|
||||
`,
|
||||
wantErr: "pipeline.publish.locks[0].source \"narratio.unknown\" is unsupported",
|
||||
},
|
||||
{
|
||||
name: "prepared input source rejected",
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
publish:
|
||||
locks:
|
||||
- source: narratio.input.players
|
||||
`,
|
||||
wantErr: "pipeline.publish.locks[0].source \"narratio.input.players\" is unsupported",
|
||||
},
|
||||
{
|
||||
name: "duplicate source rejected",
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
@@ -579,6 +597,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -591,6 +611,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantErr: "session.inputs.audio_s3.prefix must be a relative path",
|
||||
},
|
||||
@@ -604,6 +626,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantErr: "session.inputs.audio_s3.prefix must not contain path traversal",
|
||||
},
|
||||
@@ -618,6 +642,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantErr: "mutually exclusive",
|
||||
},
|
||||
@@ -664,6 +690,8 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, sessionYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
|
||||
@@ -13,6 +13,20 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
wantValidateErr string
|
||||
assert func(t *testing.T, cfg *Config)
|
||||
}{
|
||||
{
|
||||
name: "trim defaults when omitted",
|
||||
trimYAML: "",
|
||||
assert: func(t *testing.T, cfg *Config) {
|
||||
t.Helper()
|
||||
assertDefaultTrimConfig(t, cfg)
|
||||
if cfg.Pipeline.Scriptorium == nil {
|
||||
t.Fatal("scriptorium config should be defaulted when trim is enabled by default")
|
||||
}
|
||||
if cfg.Pipeline.Scriptorium.Binary != DefaultScriptoriumBinary {
|
||||
t.Fatalf("scriptorium.binary = %q, want %q", cfg.Pipeline.Scriptorium.Binary, DefaultScriptoriumBinary)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid trim config",
|
||||
trimYAML: `trim:
|
||||
@@ -34,8 +48,8 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
if cfg.Pipeline.Trim == nil {
|
||||
t.Fatal("trim config should be present")
|
||||
}
|
||||
if cfg.Pipeline.Trim.Enabled != true {
|
||||
t.Fatalf("trim.enabled = %t, want true", cfg.Pipeline.Trim.Enabled)
|
||||
if cfg.Pipeline.Trim.Enabled == nil || !*cfg.Pipeline.Trim.Enabled {
|
||||
t.Fatalf("trim.enabled = %#v, want true", cfg.Pipeline.Trim.Enabled)
|
||||
}
|
||||
if cfg.Pipeline.Trim.Bounds.ProfileID != "" {
|
||||
t.Fatalf("trim.bounds.profile_id = %q, want empty", cfg.Pipeline.Trim.Bounds.ProfileID)
|
||||
@@ -43,67 +57,25 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enabled omitted defaults disabled",
|
||||
name: "enabled omitted defaults enabled",
|
||||
trimYAML: `trim:
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
transcript_input_name: transcript
|
||||
output_path: artifacts/session_bounds.json
|
||||
`,
|
||||
assert: func(t *testing.T, cfg *Config) {
|
||||
t.Helper()
|
||||
if cfg.Pipeline.Trim == nil {
|
||||
t.Fatal("trim config should be present")
|
||||
}
|
||||
if cfg.Pipeline.Trim.Enabled {
|
||||
t.Fatal("trim.enabled should default to false when omitted")
|
||||
}
|
||||
assertDefaultTrimConfig(t, cfg)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing prompt id fails when enabled",
|
||||
name: "explicit disabled remains disabled",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
transcript_input_name: transcript
|
||||
output_path: artifacts/session_bounds.json
|
||||
enabled: false
|
||||
`,
|
||||
wantValidateErr: "pipeline.trim.bounds.prompt_id is required when pipeline.trim.enabled is true",
|
||||
},
|
||||
{
|
||||
name: "missing transcript input name fails when enabled",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
output_path: artifacts/session_bounds.json
|
||||
`,
|
||||
wantValidateErr: "pipeline.trim.bounds.transcript_input_name is required when pipeline.trim.enabled is true",
|
||||
},
|
||||
{
|
||||
name: "missing bounds output path fails when enabled",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
transcript_input_name: transcript
|
||||
`,
|
||||
wantValidateErr: "pipeline.trim.bounds.output_path is required when pipeline.trim.enabled is true",
|
||||
},
|
||||
{
|
||||
name: "missing trimmed output path fails when enabled",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
transcript_input_name: transcript
|
||||
output_path: artifacts/session_bounds.json
|
||||
`,
|
||||
wantValidateErr: "pipeline.trim.output_path is required when pipeline.trim.enabled is true",
|
||||
assert: func(t *testing.T, cfg *Config) {
|
||||
t.Helper()
|
||||
if cfg.Pipeline.Trim == nil || cfg.Pipeline.Trim.Enabled == nil || *cfg.Pipeline.Trim.Enabled {
|
||||
t.Fatalf("trim.enabled = %#v, want false", cfg.Pipeline.Trim)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid timeout fails",
|
||||
@@ -185,3 +157,31 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertDefaultTrimConfig(t *testing.T, cfg *Config) {
|
||||
t.Helper()
|
||||
if cfg.Pipeline.Trim == nil {
|
||||
t.Fatal("trim config should be present")
|
||||
}
|
||||
if cfg.Pipeline.Trim.Enabled == nil || !*cfg.Pipeline.Trim.Enabled {
|
||||
t.Fatalf("trim.enabled = %#v, want true", cfg.Pipeline.Trim.Enabled)
|
||||
}
|
||||
if cfg.Pipeline.Trim.OutputPath != DefaultTrimOutputPath {
|
||||
t.Fatalf("trim.output_path = %q, want %q", cfg.Pipeline.Trim.OutputPath, DefaultTrimOutputPath)
|
||||
}
|
||||
if cfg.Pipeline.Trim.Bounds.PromptID != DefaultTrimBoundsPromptID {
|
||||
t.Fatalf("trim.bounds.prompt_id = %q, want %q", cfg.Pipeline.Trim.Bounds.PromptID, DefaultTrimBoundsPromptID)
|
||||
}
|
||||
if cfg.Pipeline.Trim.Bounds.TranscriptInputName != DefaultTrimBoundsTranscriptInputName {
|
||||
t.Fatalf("trim.bounds.transcript_input_name = %q, want %q", cfg.Pipeline.Trim.Bounds.TranscriptInputName, DefaultTrimBoundsTranscriptInputName)
|
||||
}
|
||||
if cfg.Pipeline.Trim.Bounds.OutputPath != DefaultTrimBoundsOutputPath {
|
||||
t.Fatalf("trim.bounds.output_path = %q, want %q", cfg.Pipeline.Trim.Bounds.OutputPath, DefaultTrimBoundsOutputPath)
|
||||
}
|
||||
if cfg.Pipeline.Trim.Bounds.Timeout != DefaultTrimBoundsTimeout {
|
||||
t.Fatalf("trim.bounds.timeout = %q, want %q", cfg.Pipeline.Trim.Bounds.Timeout, DefaultTrimBoundsTimeout)
|
||||
}
|
||||
if cfg.Pipeline.Trim.Seriatim.Report == nil || *cfg.Pipeline.Trim.Seriatim.Report != DefaultTrimSeriatimReport {
|
||||
t.Fatalf("trim.seriatim.report = %#v, want %t", cfg.Pipeline.Trim.Seriatim.Report, DefaultTrimSeriatimReport)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,21 @@ func validateCampaign(cfg *CampaignConfig) error {
|
||||
if CampaignID(cfg) == "" {
|
||||
return fmt.Errorf("campaign.campaign_id is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Inputs.SpeakersFile) == "" {
|
||||
return fmt.Errorf("campaign.inputs.speakers_file is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Inputs.AutocorrectFile) == "" {
|
||||
return fmt.Errorf("campaign.inputs.autocorrect_file is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Inputs.GlossaryFile) == "" {
|
||||
return fmt.Errorf("campaign.inputs.glossary_file is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Inputs.PlayersFile) == "" {
|
||||
return fmt.Errorf("campaign.inputs.players_file is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Inputs.PartyFile) == "" {
|
||||
return fmt.Errorf("campaign.inputs.party_file is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -276,7 +291,10 @@ func validateTrim(cfg *TrimConfig) error {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if !cfg.Enabled {
|
||||
if cfg.Enabled == nil {
|
||||
return fmt.Errorf("pipeline.trim.enabled must be set (defaults should populate this)")
|
||||
}
|
||||
if !*cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -312,6 +330,9 @@ func validateRender(cfg *RenderConfig) error {
|
||||
if cfg.IncludeTimestamps == nil {
|
||||
return fmt.Errorf("pipeline.render.include_timestamps must be set (defaults should populate this)")
|
||||
}
|
||||
if cfg.IncludeSegmentIDs == nil {
|
||||
return fmt.Errorf("pipeline.render.include_segment_ids must be set (defaults should populate this)")
|
||||
}
|
||||
format := strings.TrimSpace(cfg.Format)
|
||||
if format != "markdown" {
|
||||
return fmt.Errorf("pipeline.render.format must be markdown")
|
||||
@@ -589,6 +610,12 @@ func validateSession(cfg *SessionConfig) error {
|
||||
if strings.TrimSpace(cfg.Inputs.GlossaryFile) == "" {
|
||||
return fmt.Errorf("session.inputs.glossary_file is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Inputs.PlayersFile) == "" {
|
||||
return fmt.Errorf("session.inputs.players_file is required")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Inputs.PartyFile) == "" {
|
||||
return fmt.Errorf("session.inputs.party_file is required")
|
||||
}
|
||||
|
||||
hasAudioDir := strings.TrimSpace(cfg.Inputs.AudioDir) != ""
|
||||
hasAudioFiles := len(cfg.Inputs.AudioFiles) > 0
|
||||
|
||||
@@ -632,6 +632,16 @@ func resolveScriptoriumInput(
|
||||
if describeErr != nil {
|
||||
return "", false, nil, describeErr
|
||||
}
|
||||
if descriptor.Source.Kind == artifactpolicy.SourceKindStableInput {
|
||||
resolvedPath, ok, err := resolvePreparedStableInput(descriptor.Source.ID, paths)
|
||||
if err != nil {
|
||||
if inputCfg.Required {
|
||||
return "", false, nil, err
|
||||
}
|
||||
return "", false, nil, nil
|
||||
}
|
||||
return resolvedPath, ok, nil, nil
|
||||
}
|
||||
if descriptor.Source.Kind == artifactpolicy.SourceKindPreviousArtifact {
|
||||
resolved, err := artifacts.ResolvePreviousSessionArtifactWithCatalog(paths, m, source, runtimeCatalog)
|
||||
if err == nil {
|
||||
@@ -684,6 +694,36 @@ func resolveScriptoriumInput(
|
||||
}
|
||||
}
|
||||
|
||||
func resolvePreparedStableInput(sourceID string, paths artifacts.SessionPaths) (string, bool, error) {
|
||||
filename, ok := preparedStableInputFilename(sourceID)
|
||||
if !ok {
|
||||
return "", false, fmt.Errorf("unsupported prepared input source %q", sourceID)
|
||||
}
|
||||
path := filepath.Join(paths.InputsDir, filename)
|
||||
if err := requireNonEmptyFile(path, "prepared input "+sourceID); err != nil {
|
||||
return "", false, fmt.Errorf(
|
||||
"prepared input source %q is unavailable; run narratio run-stage prepare %s --force: %w",
|
||||
sourceID,
|
||||
paths.SessionID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
return path, true, nil
|
||||
}
|
||||
|
||||
func preparedStableInputFilename(sourceID string) (string, bool) {
|
||||
switch strings.TrimSpace(sourceID) {
|
||||
case artifactpolicy.SourceInputPlayers:
|
||||
return "players.yml", true
|
||||
case artifactpolicy.SourceInputParty:
|
||||
return "party.yml", true
|
||||
case artifactpolicy.SourceInputGlossary:
|
||||
return "glossary.yml", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func buildAnalyzeRuntimeArtifactCatalog(
|
||||
paths artifacts.SessionPaths,
|
||||
scriptoriumCfg *config.ScriptoriumConfig,
|
||||
|
||||
@@ -986,6 +986,96 @@ func TestAnalyzeSupportsRenderedMarkdownTranscriptSourceWhenConfigured(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeResolvesPreparedStableInputSources(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
playersPath := filepath.Join(paths.InputsDir, "players.yml")
|
||||
partyPath := filepath.Join(paths.InputsDir, "party.yml")
|
||||
glossaryPath := filepath.Join(paths.InputsDir, "glossary.yml")
|
||||
writeAnalyzeFile(t, playersPath, "- Eric\n")
|
||||
writeAnalyzeFile(t, partyPath, "- Arannis\n")
|
||||
writeAnalyzeFile(t, glossaryPath, "- term: Ten Towns\n")
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["players"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.input.players",
|
||||
Required: true,
|
||||
}
|
||||
artifact.Inputs["party"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.input.party",
|
||||
Required: true,
|
||||
}
|
||||
artifact.Inputs["glossary"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.input.glossary",
|
||||
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["players"] != playersPath {
|
||||
t.Fatalf("players input = %q, want %q", fake.RunRequests[0].InputPaths["players"], playersPath)
|
||||
}
|
||||
if fake.RunRequests[0].InputPaths["party"] != partyPath {
|
||||
t.Fatalf("party input = %q, want %q", fake.RunRequests[0].InputPaths["party"], partyPath)
|
||||
}
|
||||
if fake.RunRequests[0].InputPaths["glossary"] != glossaryPath {
|
||||
t.Fatalf("glossary input = %q, want %q", fake.RunRequests[0].InputPaths["glossary"], glossaryPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeMissingRequiredPreparedStableInputFailsWithPrepareGuidance(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["players"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.input.players",
|
||||
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(), "prepared input source \"narratio.input.players\" is unavailable") ||
|
||||
!strings.Contains(err.Error(), "run narratio run-stage prepare 2026-05-03 --force") {
|
||||
t.Fatalf("error = %q, want prepared input guidance", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeMissingOptionalPreparedStableInputIsOmitted(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["players"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.input.players",
|
||||
Required: false,
|
||||
}
|
||||
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 _, ok := fake.RunRequests[0].InputPaths["players"]; ok {
|
||||
t.Fatalf("players input should be omitted: %#v", fake.RunRequests[0].InputPaths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
|
||||
@@ -33,11 +33,13 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
||||
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
||||
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
||||
writeStageTestFile(t, sessionPath, "session_id: 2026-05-03\n")
|
||||
writeStageTestFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n")
|
||||
writeStageTestFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n")
|
||||
writeStageTestFile(t, pipelinePath, "workspace:\n root: "+root+"\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "players.yml"), "[]\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "party.yml"), "[]\n")
|
||||
writeStageTestFile(t, filepath.Join(cfgDir, "audio", "alice.flac"), "a")
|
||||
|
||||
wf := &whisperx.FakeClient{}
|
||||
@@ -82,6 +84,16 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PlayersFile: config.ResolvedInputFile{
|
||||
Path: "./players.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PartyFile: config.ResolvedInputFile{
|
||||
Path: "./party.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
},
|
||||
Session: &config.SessionConfig{
|
||||
SessionID: "2026-05-03",
|
||||
|
||||
@@ -32,6 +32,8 @@ func (prepareStage) Declares() IODecl {
|
||||
{Kind: "config", Category: "inputs", RelativePath: "speakers.yml"},
|
||||
{Kind: "config", Category: "inputs", RelativePath: "autocorrect.yml"},
|
||||
{Kind: "config", Category: "inputs", RelativePath: "glossary.yml"},
|
||||
{Kind: "config", Category: "inputs", RelativePath: "players.yml"},
|
||||
{Kind: "config", Category: "inputs", RelativePath: "party.yml"},
|
||||
{Kind: "audio", Category: "audio", RelativePath: "*.flac"},
|
||||
},
|
||||
}
|
||||
@@ -75,6 +77,8 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
speakersInput := stableInputSource(env.Config.StableInputs.SpeakersFile, env.Config.Session.Inputs.SpeakersFile, sessionSrc)
|
||||
autocorrectInput := stableInputSource(env.Config.StableInputs.AutocorrectFile, env.Config.Session.Inputs.AutocorrectFile, sessionSrc)
|
||||
glossaryInput := stableInputSource(env.Config.StableInputs.GlossaryFile, env.Config.Session.Inputs.GlossaryFile, sessionSrc)
|
||||
playersInput := stableInputSource(env.Config.StableInputs.PlayersFile, env.Config.Session.Inputs.PlayersFile, sessionSrc)
|
||||
partyInput := stableInputSource(env.Config.StableInputs.PartyFile, env.Config.Session.Inputs.PartyFile, sessionSrc)
|
||||
|
||||
speakersSrc, err := resolveConfigRelativePath(speakersInput)
|
||||
if err != nil {
|
||||
@@ -88,6 +92,14 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare: glossary path: %w", err)
|
||||
}
|
||||
playersSrc, err := resolveConfigRelativePath(playersInput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare: players path: %w", err)
|
||||
}
|
||||
partySrc, err := resolveConfigRelativePath(partyInput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare: party path: %w", err)
|
||||
}
|
||||
|
||||
for _, required := range []struct {
|
||||
path string
|
||||
@@ -96,6 +108,8 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
{path: speakersSrc, name: "speakers.yml"},
|
||||
{path: autocorrectSrc, name: "autocorrect.yml"},
|
||||
{path: glossarySrc, name: "glossary.yml"},
|
||||
{path: playersSrc, name: "players.yml"},
|
||||
{path: partySrc, name: "party.yml"},
|
||||
} {
|
||||
if err := requireFile(required.path, required.name); err != nil {
|
||||
return nil, fmt.Errorf("prepare: %w", err)
|
||||
@@ -107,7 +121,7 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
return nil, fmt.Errorf("prepare: resolve audio inputs: %w", err)
|
||||
}
|
||||
|
||||
inputs := make([]manifest.InputRecord, 0, 6+len(resolvedLocalAudio))
|
||||
inputs := make([]manifest.InputRecord, 0, 8+len(resolvedLocalAudio))
|
||||
registerInput := func(kind, path, checksum string) {
|
||||
inputs = append(inputs, manifest.InputRecord{Kind: kind, Path: path, Checksum: checksum})
|
||||
}
|
||||
@@ -166,6 +180,8 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
{kind: "speakers", src: speakersSrc, dst: filepath.Join(paths.InputsDir, "speakers.yml"), source: speakersInput.Source},
|
||||
{kind: "autocorrect", src: autocorrectSrc, dst: filepath.Join(paths.InputsDir, "autocorrect.yml"), source: autocorrectInput.Source},
|
||||
{kind: "glossary", src: glossarySrc, dst: filepath.Join(paths.InputsDir, "glossary.yml"), source: glossaryInput.Source},
|
||||
{kind: "players", src: playersSrc, dst: filepath.Join(paths.InputsDir, "players.yml"), source: playersInput.Source},
|
||||
{kind: "party", src: partySrc, dst: filepath.Join(paths.InputsDir, "party.yml"), source: partyInput.Source},
|
||||
} {
|
||||
checksum, err := copyFileIfChanged(env.ArtifactStore, cfgFile.src, cfgFile.dst)
|
||||
if err != nil {
|
||||
|
||||
@@ -41,6 +41,8 @@ func TestPrepareStageExplicitAudioFiles(t *testing.T) {
|
||||
filepath.Join(paths.InputsDir, "speakers.yml"),
|
||||
filepath.Join(paths.InputsDir, "autocorrect.yml"),
|
||||
filepath.Join(paths.InputsDir, "glossary.yml"),
|
||||
filepath.Join(paths.InputsDir, "players.yml"),
|
||||
filepath.Join(paths.InputsDir, "party.yml"),
|
||||
filepath.Join(paths.AudioDir, "alice.flac"),
|
||||
filepath.Join(paths.AudioDir, "bob.flac"),
|
||||
} {
|
||||
@@ -49,8 +51,8 @@ func TestPrepareStageExplicitAudioFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.Inputs) != 8 {
|
||||
t.Fatalf("manifest inputs len = %d, want 8", len(m.Inputs))
|
||||
if len(m.Inputs) != 10 {
|
||||
t.Fatalf("manifest inputs len = %d, want 10", len(m.Inputs))
|
||||
}
|
||||
for _, in := range m.Inputs {
|
||||
if in.Checksum == "" {
|
||||
@@ -613,11 +615,15 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`)
|
||||
writeFile(t, sessionPath, "session_id: 2026-05-03\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "players.yml"), "[]\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "party.yml"), "[]\n")
|
||||
|
||||
cfg := &config.Config{
|
||||
Pipeline: &config.PipelineConfig{
|
||||
@@ -651,6 +657,16 @@ inputs:
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PlayersFile: config.ResolvedInputFile{
|
||||
Path: "./players.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PartyFile: config.ResolvedInputFile{
|
||||
Path: "./party.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,10 @@ func (renderStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
||||
}
|
||||
title := resolveRenderTitle(renderCfg, env.Config.Session)
|
||||
includeTimestamps := renderCfg.IncludeTimestamps == nil || *renderCfg.IncludeTimestamps
|
||||
includeSegmentIDs := renderCfg.IncludeSegmentIDs
|
||||
includeSegmentIDs := config.DefaultRenderSegmentIDs
|
||||
if renderCfg.IncludeSegmentIDs != nil {
|
||||
includeSegmentIDs = *renderCfg.IncludeSegmentIDs
|
||||
}
|
||||
includeMetadata := renderCfg.IncludeMetadata
|
||||
|
||||
meta := map[string]any{
|
||||
@@ -237,11 +240,12 @@ func renderConfigOrDefault(cfg *config.RenderConfig) *config.RenderConfig {
|
||||
}
|
||||
enabled := true
|
||||
includeTimestamps := true
|
||||
includeSegmentIDs := config.DefaultRenderSegmentIDs
|
||||
return &config.RenderConfig{
|
||||
Enabled: &enabled,
|
||||
Format: config.DefaultRenderFormat,
|
||||
IncludeTimestamps: &includeTimestamps,
|
||||
IncludeSegmentIDs: config.DefaultRenderSegmentIDs,
|
||||
IncludeSegmentIDs: &includeSegmentIDs,
|
||||
IncludeMetadata: config.DefaultRenderMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ func setupRenderEnv(t *testing.T) (*Env, *manifest.Manifest, *seriatim.FakeRunne
|
||||
|
||||
enabled := true
|
||||
includeTimestamps := true
|
||||
includeSegmentIDs := false
|
||||
seriatimReport := false
|
||||
cfg := &config.Config{
|
||||
PipelinePath: pipelinePath,
|
||||
@@ -183,7 +184,7 @@ func setupRenderEnv(t *testing.T) (*Env, *manifest.Manifest, *seriatim.FakeRunne
|
||||
Format: "markdown",
|
||||
Title: "Pipeline Title",
|
||||
IncludeTimestamps: &includeTimestamps,
|
||||
IncludeSegmentIDs: false,
|
||||
IncludeSegmentIDs: &includeSegmentIDs,
|
||||
IncludeMetadata: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -236,10 +236,12 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
|
||||
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
||||
writeFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\n")
|
||||
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
||||
writeFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n")
|
||||
writeFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "players.yml"), "[]\n")
|
||||
writeFile(t, filepath.Join(cfgDir, "party.yml"), "[]\n")
|
||||
|
||||
retries := 3
|
||||
concurrency := 2
|
||||
@@ -267,12 +269,16 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
|
||||
SpeakersFile: "./speakers.yml",
|
||||
AutocorrectFile: "./autocorrect.yml",
|
||||
GlossaryFile: "./glossary.yml",
|
||||
PlayersFile: "./players.yml",
|
||||
PartyFile: "./party.yml",
|
||||
},
|
||||
},
|
||||
StableInputs: config.ResolvedStableInputs{
|
||||
SpeakersFile: config.ResolvedInputFile{Path: "./speakers.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||
AutocorrectFile: config.ResolvedInputFile{Path: "./autocorrect.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||
GlossaryFile: config.ResolvedInputFile{Path: "./glossary.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||
PlayersFile: config.ResolvedInputFile{Path: "./players.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||
PartyFile: config.ResolvedInputFile{Path: "./party.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
|
||||
}
|
||||
|
||||
trimCfg := env.Config.Pipeline.Trim
|
||||
enabled := trimCfg != nil && trimCfg.Enabled
|
||||
enabled := trimCfg != nil && trimCfg.Enabled != nil && *trimCfg.Enabled
|
||||
|
||||
canonicalTrimmedPath, err := resolveTrimmedOutputPath(paths, trimCfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -275,7 +275,8 @@ func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) {
|
||||
writeFile(t, normalized, normalizedBody)
|
||||
|
||||
disabled := *env.Config.Pipeline.Trim
|
||||
disabled.Enabled = false
|
||||
enabled := false
|
||||
disabled.Enabled = &enabled
|
||||
env.Config.Pipeline.Trim = &disabled
|
||||
|
||||
result, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
@@ -424,6 +425,7 @@ func setupTrimEnv(t *testing.T) (*Env, *manifest.Manifest, *boundsScriptoriumRun
|
||||
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
||||
|
||||
seriatimReport := false
|
||||
trimEnabled := true
|
||||
cfg := &config.Config{
|
||||
PipelinePath: pipelinePath,
|
||||
SessionPath: sessionPath,
|
||||
@@ -437,7 +439,7 @@ func setupTrimEnv(t *testing.T) (*Env, *manifest.Manifest, *boundsScriptoriumRun
|
||||
Report: &seriatimReport,
|
||||
},
|
||||
Trim: &config.TrimConfig{
|
||||
Enabled: true,
|
||||
Enabled: &trimEnabled,
|
||||
OutputPath: "transcripts/final.trimmed.json",
|
||||
Bounds: config.TrimBoundsConfig{
|
||||
PromptID: "dnd_session.bounds",
|
||||
|
||||
Reference in New Issue
Block a user