Implemented new campaign/session stable inputs and corresponding input source references
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
This commit is contained in:
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user