1 Commits

Author SHA1 Message Date
58c6ab2d54 Updated audita configuration to reflect the new audita public CLI
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
2026-05-16 08:46:48 -05:00
13 changed files with 516 additions and 240 deletions

View File

@@ -66,6 +66,33 @@ YAML decoding is strict (`KnownFields(true)`), so unknown fields fail fast.
- `transcripts/normalized.json`: Seriatim-normalized transcript from the normalize stage - `transcripts/normalized.json`: Seriatim-normalized transcript from the normalize stage
- `transcripts/trimmed.json`: gameplay-only normalized polished transcript from trim stage - `transcripts/trimmed.json`: gameplay-only normalized polished transcript from trim stage
## Audita Configuration
`pipeline.audita` configures the real Audita subprocess adapter used by `polish`.
Required:
- `binary`
- `timeout`
- `base_url`
- `model`
Optional:
- `llm_api_key_env` (when set, Narratio requires that env var and passes it to Audita as `AUDITA_LLM_API_KEY`)
- `modules` override list (when empty/omitted, Narratio does not pass `--modules`)
- `transcript_description`
- `config_path`
- `output_schema` (`bare-segments` or `audita-v1`)
- `work_dir_retention` (`always`, `auto`, or `never`)
- `total_llm_concurrency` (> 0 when provided)
- `proposal_llm_concurrency` (> 0 when provided)
- `validation_model`
- `validation_llm_concurrency` (> 0 when provided)
- `report` (defaults to `true`)
Narratio passes only configured optional Audita flags. Omitted optional values are left to Audita runtime defaults.
## Normalize Configuration ## Normalize Configuration
`pipeline.normalize` is optional. When omitted, Narratio defaults to: `pipeline.normalize` is optional. When omitted, Narratio defaults to:

View File

@@ -116,6 +116,31 @@ Optional pipeline secrets directory:
`pipeline.normalize` is optional. Existing pipelines without normalize config continue to work. `pipeline.normalize` is optional. Existing pipelines without normalize config continue to work.
`pipeline.audita` drives the real Audita subprocess adapter for the `polish` stage.
Audita required fields:
- `binary`
- `timeout`
- `base_url`
- `model`
Audita optional fields:
- `llm_api_key_env` (enforced only when configured)
- `modules` override list (when omitted/empty, Narratio does not pass `--modules`)
- `transcript_description`
- `config_path`
- `output_schema` (`bare-segments` or `audita-v1`)
- `work_dir_retention` (`always`, `auto`, `never`)
- `total_llm_concurrency` (> 0 when provided)
- `proposal_llm_concurrency` (> 0 when provided)
- `validation_model`
- `validation_llm_concurrency` (> 0 when provided)
- `report` (default `true`)
Narratio passes only configured optional Audita flags; omitted optional values defer to Audita defaults.
When `pipeline.normalize` is omitted, defaults are applied: When `pipeline.normalize` is omitted, defaults are applied:
- `output_path: transcripts/normalized.json` - `output_path: transcripts/normalized.json`

View File

@@ -33,17 +33,16 @@ audita:
binary: "audita" binary: "audita"
timeout: "3h" timeout: "3h"
llm_api_key_env: "AUDITA_LLM_API_KEY" llm_api_key_env: "AUDITA_LLM_API_KEY"
modules: # Optional: pass only when overriding Audita's default module sequence.
- glossary modules: []
- homophones
- glossary
- spoken_word
- grammar
- homophones
- glossary
base_url: "https://openrouter.ai/api/v1" base_url: "https://openrouter.ai/api/v1"
model: "openrouter/google/gemma-4-31b-it" model: "openrouter/google/gemma-4-31b-it"
llm_concurrency: 1 transcript_description: ""
config_path: ""
output_schema: "audita-v1"
work_dir_retention: "auto"
total_llm_concurrency: 1
proposal_llm_concurrency: 1
validation_model: "" validation_model: ""
validation_llm_concurrency: 1 validation_llm_concurrency: 1
report: true report: true

View File

@@ -24,6 +24,12 @@ type PolishRequest struct {
Modules []string Modules []string
BaseURL string BaseURL string
Model string Model string
TranscriptDescription string
ConfigPath string
OutputSchema string
WorkDirRetention string
TotalLLMConcurrency *int
ProposalLLMConcurrency *int
ValidationModel string ValidationModel string
ValidationLLMConcurrency *int ValidationLLMConcurrency *int
StdoutLogPath string StdoutLogPath string

View File

@@ -21,7 +21,12 @@ type SubprocessRunnerConfig struct {
Modules []string Modules []string
BaseURL string BaseURL string
Model string Model string
LLMConcurrency *int TranscriptDescription string
ConfigPath string
OutputSchema string
WorkDirRetention string
TotalLLMConcurrency *int
ProposalLLMConcurrency *int
ValidationModel string ValidationModel string
ValidationLLMConcurrency *int ValidationLLMConcurrency *int
Report bool Report bool
@@ -35,7 +40,12 @@ type SubprocessRunner struct {
modules []string modules []string
baseURL string baseURL string
model string model string
llmConcurrency *int transcriptDescription string
configPath string
outputSchema string
workDirRetention string
totalLLMConcurrency *int
proposalLLMConcurrency *int
validationModel string validationModel string
validationLLMConcurrency *int validationLLMConcurrency *int
report bool report bool
@@ -49,7 +59,12 @@ func NewSubprocessRunnerFromConfigValues(
modules []string, modules []string,
baseURL string, baseURL string,
model string, model string,
llmConcurrency *int, transcriptDescription string,
configPath string,
outputSchema string,
workDirRetention string,
totalLLMConcurrency *int,
proposalLLMConcurrency *int,
validationModel string, validationModel string,
validationLLMConcurrency *int, validationLLMConcurrency *int,
report bool, report bool,
@@ -68,7 +83,12 @@ func NewSubprocessRunnerFromConfigValues(
Modules: modules, Modules: modules,
BaseURL: baseURL, BaseURL: baseURL,
Model: model, Model: model,
LLMConcurrency: llmConcurrency, TranscriptDescription: transcriptDescription,
ConfigPath: configPath,
OutputSchema: outputSchema,
WorkDirRetention: workDirRetention,
TotalLLMConcurrency: totalLLMConcurrency,
ProposalLLMConcurrency: proposalLLMConcurrency,
ValidationModel: validationModel, ValidationModel: validationModel,
ValidationLLMConcurrency: validationLLMConcurrency, ValidationLLMConcurrency: validationLLMConcurrency,
Report: report, Report: report,
@@ -83,9 +103,6 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error)
if cfg.Timeout <= 0 { if cfg.Timeout <= 0 {
return nil, fmt.Errorf("audita timeout must be > 0") return nil, fmt.Errorf("audita timeout must be > 0")
} }
if len(cfg.Modules) == 0 {
return nil, fmt.Errorf("audita modules must include at least one module")
}
for i, module := range cfg.Modules { for i, module := range cfg.Modules {
if strings.TrimSpace(module) == "" { if strings.TrimSpace(module) == "" {
return nil, fmt.Errorf("audita module at index %d is empty", i) return nil, fmt.Errorf("audita module at index %d is empty", i)
@@ -104,12 +121,25 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error)
if strings.TrimSpace(cfg.Model) == "" { if strings.TrimSpace(cfg.Model) == "" {
return nil, fmt.Errorf("audita model is required") return nil, fmt.Errorf("audita model is required")
} }
if cfg.LLMConcurrency != nil && *cfg.LLMConcurrency <= 0 { if cfg.TotalLLMConcurrency != nil && *cfg.TotalLLMConcurrency <= 0 {
return nil, fmt.Errorf("audita llm concurrency must be > 0 when provided") return nil, fmt.Errorf("audita total llm concurrency must be > 0 when provided")
}
if cfg.ProposalLLMConcurrency != nil && *cfg.ProposalLLMConcurrency <= 0 {
return nil, fmt.Errorf("audita proposal llm concurrency must be > 0 when provided")
} }
if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 { if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 {
return nil, fmt.Errorf("audita validation llm concurrency must be > 0 when provided") return nil, fmt.Errorf("audita validation llm concurrency must be > 0 when provided")
} }
switch strings.TrimSpace(cfg.OutputSchema) {
case "", "bare-segments", "audita-v1":
default:
return nil, fmt.Errorf("audita output schema must be one of: bare-segments, audita-v1")
}
switch strings.TrimSpace(cfg.WorkDirRetention) {
case "", "always", "auto", "never":
default:
return nil, fmt.Errorf("audita work dir retention must be one of: always, auto, never")
}
modules := make([]string, len(cfg.Modules)) modules := make([]string, len(cfg.Modules))
for i, m := range cfg.Modules { for i, m := range cfg.Modules {
@@ -123,7 +153,12 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error)
modules: modules, modules: modules,
baseURL: strings.TrimSpace(cfg.BaseURL), baseURL: strings.TrimSpace(cfg.BaseURL),
model: strings.TrimSpace(cfg.Model), model: strings.TrimSpace(cfg.Model),
llmConcurrency: cfg.LLMConcurrency, transcriptDescription: strings.TrimSpace(cfg.TranscriptDescription),
configPath: strings.TrimSpace(cfg.ConfigPath),
outputSchema: strings.TrimSpace(cfg.OutputSchema),
workDirRetention: strings.TrimSpace(cfg.WorkDirRetention),
totalLLMConcurrency: cfg.TotalLLMConcurrency,
proposalLLMConcurrency: cfg.ProposalLLMConcurrency,
validationModel: strings.TrimSpace(cfg.ValidationModel), validationModel: strings.TrimSpace(cfg.ValidationModel),
validationLLMConcurrency: cfg.ValidationLLMConcurrency, validationLLMConcurrency: cfg.ValidationLLMConcurrency,
report: cfg.Report, report: cfg.Report,
@@ -152,7 +187,7 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe
} }
reqModules := req.Modules reqModules := req.Modules
if len(reqModules) == 0 { if reqModules == nil {
reqModules = append([]string(nil), r.modules...) reqModules = append([]string(nil), r.modules...)
} }
args := r.buildArgs(req, reqModules) args := r.buildArgs(req, reqModules)
@@ -168,14 +203,9 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe
env["AUDITA_LLM_API_KEY"] = credential env["AUDITA_LLM_API_KEY"] = credential
credentialPresent = true credentialPresent = true
} }
primaryConcurrencyViaEnv := false
if r.llmConcurrency != nil {
env["AUDITA_LLM_CONCURRENCY"] = strconv.Itoa(*r.llmConcurrency)
primaryConcurrencyViaEnv = true
}
if req.GeneratedConfigPath != "" { if req.GeneratedConfigPath != "" {
if err := r.writeInvocationConfig(req, args, reqModules, credentialPresent, primaryConcurrencyViaEnv); err != nil { if err := r.writeInvocationConfig(req, args, reqModules, credentialPresent); err != nil {
return PolishResult{}, fmt.Errorf("write audita invocation config %q: %w", req.GeneratedConfigPath, err) return PolishResult{}, fmt.Errorf("write audita invocation config %q: %w", req.GeneratedConfigPath, err)
} }
} }
@@ -196,7 +226,7 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe
req.StderrLogPath, req.StderrLogPath,
) )
wrappedMessage = addSubprocessStreamHint(wrappedMessage, err) wrappedMessage = addSubprocessStreamHint(wrappedMessage, err)
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf( return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf(
"%s: %w", "%s: %w",
wrappedMessage, wrappedMessage,
err, err,
@@ -204,11 +234,11 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe
} }
if err := validateProcessedOutput(req.OutputProcessedPath); err != nil { if err := validateProcessedOutput(req.OutputProcessedPath); err != nil {
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err) return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err)
} }
if r.report { if r.report {
if err := validateJSONFile(req.ReportPath); err != nil { if err := validateJSONFile(req.ReportPath); err != nil {
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf("validate audita report output %q: %w", req.ReportPath, err) return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf("validate audita report output %q: %w", req.ReportPath, err)
} }
} }
@@ -223,21 +253,25 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe
Duration: runRes.Duration, Duration: runRes.Duration,
InvokedBinary: r.binary, InvokedBinary: r.binary,
Metadata: map[string]any{ Metadata: map[string]any{
"adapter": "audita_subprocess", "adapter": "audita_subprocess",
"modules": reqModules, "modules": reqModules,
"base_url": r.baseURL, "base_url": r.baseURL,
"model": r.model, "model": r.model,
"validation_model": r.validationModel, "transcript_description": r.transcriptDescription,
"validation_llm_concurrency": r.validationLLMConcurrency, "config_path": r.configPath,
"credential_env_var": r.llmAPIKeyEnv, "output_schema": r.outputSchema,
"credential_present": credentialPresent, "work_dir_retention": r.workDirRetention,
"primary_llm_concurrency_via_env": primaryConcurrencyViaEnv, "validation_model": r.validationModel,
"primary_llm_concurrency_env_name": "AUDITA_LLM_CONCURRENCY", "total_llm_concurrency": r.totalLLMConcurrency,
"proposal_llm_concurrency": r.proposalLLMConcurrency,
"validation_llm_concurrency": r.validationLLMConcurrency,
"credential_env_var": r.llmAPIKeyEnv,
"credential_present": credentialPresent,
}, },
}, nil }, nil
} }
func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, runRes subprocess.RunResult, credentialPresent bool, primaryConcurrencyViaEnv bool) PolishResult { func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, runRes subprocess.RunResult, credentialPresent bool) PolishResult {
return PolishResult{ return PolishResult{
ProcessedTranscriptPath: req.OutputProcessedPath, ProcessedTranscriptPath: req.OutputProcessedPath,
ReportPath: req.ReportPath, ReportPath: req.ReportPath,
@@ -249,16 +283,20 @@ func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, ru
Duration: runRes.Duration, Duration: runRes.Duration,
InvokedBinary: r.binary, InvokedBinary: r.binary,
Metadata: map[string]any{ Metadata: map[string]any{
"adapter": "audita_subprocess", "adapter": "audita_subprocess",
"modules": modules, "modules": modules,
"base_url": r.baseURL, "base_url": r.baseURL,
"model": r.model, "model": r.model,
"validation_model": r.validationModel, "transcript_description": r.transcriptDescription,
"validation_llm_concurrency": r.validationLLMConcurrency, "config_path": r.configPath,
"credential_env_var": r.llmAPIKeyEnv, "output_schema": r.outputSchema,
"credential_present": credentialPresent, "work_dir_retention": r.workDirRetention,
"primary_llm_concurrency_via_env": primaryConcurrencyViaEnv, "validation_model": r.validationModel,
"primary_llm_concurrency_env_name": "AUDITA_LLM_CONCURRENCY", "total_llm_concurrency": r.totalLLMConcurrency,
"proposal_llm_concurrency": r.proposalLLMConcurrency,
"validation_llm_concurrency": r.validationLLMConcurrency,
"credential_env_var": r.llmAPIKeyEnv,
"credential_present": credentialPresent,
}, },
} }
} }
@@ -269,14 +307,34 @@ func (r *SubprocessRunner) buildArgs(req PolishRequest, modules []string) []stri
req.MergedTranscriptPath, req.MergedTranscriptPath,
"--glossary", req.GlossaryPath, "--glossary", req.GlossaryPath,
"--output", req.OutputProcessedPath, "--output", req.OutputProcessedPath,
"--modules", strings.Join(modules, ","),
"--base-url", r.baseURL, "--base-url", r.baseURL,
"--model", r.model, "--model", r.model,
"--work-dir", req.WorkDir, "--work-dir", req.WorkDir,
} }
if len(modules) > 0 {
args = append(args, "--modules", strings.Join(modules, ","))
}
if r.report { if r.report {
args = append(args, "--report-json", req.ReportPath) args = append(args, "--report-json", req.ReportPath)
} }
if r.transcriptDescription != "" {
args = append(args, "--transcript-description", r.transcriptDescription)
}
if r.configPath != "" {
args = append(args, "--config", r.configPath)
}
if r.outputSchema != "" {
args = append(args, "--output-schema", r.outputSchema)
}
if r.workDirRetention != "" {
args = append(args, "--work-dir-retention", r.workDirRetention)
}
if r.totalLLMConcurrency != nil {
args = append(args, "--total-llm-concurrency", strconv.Itoa(*r.totalLLMConcurrency))
}
if r.proposalLLMConcurrency != nil {
args = append(args, "--proposal-llm-concurrency", strconv.Itoa(*r.proposalLLMConcurrency))
}
if r.validationModel != "" { if r.validationModel != "" {
args = append(args, "--validation-model", r.validationModel) args = append(args, "--validation-model", r.validationModel)
} }
@@ -286,29 +344,31 @@ func (r *SubprocessRunner) buildArgs(req PolishRequest, modules []string) []stri
return args return args
} }
func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args []string, modules []string, credentialPresent bool, primaryConcurrencyViaEnv bool) error { func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args []string, modules []string, credentialPresent bool) error {
payload := map[string]any{ payload := map[string]any{
"schema": "audita.generated.v1", "schema": "audita.generated.v1",
"binary": r.binary, "binary": r.binary,
"args": args, "args": args,
"timeout": r.timeout.String(), "timeout": r.timeout.String(),
"modules": modules, "modules": modules,
"base_url": r.baseURL, "base_url": r.baseURL,
"model": r.model, "model": r.model,
"validation_model": r.validationModel, "transcript_description": r.transcriptDescription,
"validation_llm_concurrency": r.validationLLMConcurrency, "config_path": r.configPath,
"report_enabled": r.report, "output_schema": r.outputSchema,
"merged_transcript_path": req.MergedTranscriptPath, "work_dir_retention": r.workDirRetention,
"glossary_path": req.GlossaryPath, "validation_model": r.validationModel,
"output_path": req.OutputProcessedPath, "total_llm_concurrency": r.totalLLMConcurrency,
"report_path": req.ReportPath, "proposal_llm_concurrency": r.proposalLLMConcurrency,
"work_dir": req.WorkDir, "validation_llm_concurrency": r.validationLLMConcurrency,
"credential_env_var": r.llmAPIKeyEnv, "report_enabled": r.report,
"credential_present": credentialPresent, "merged_transcript_path": req.MergedTranscriptPath,
"primary_llm_concurrency_via_env": primaryConcurrencyViaEnv, "glossary_path": req.GlossaryPath,
} "output_path": req.OutputProcessedPath,
if r.llmConcurrency != nil { "report_path": req.ReportPath,
payload["llm_concurrency"] = *r.llmConcurrency "work_dir": req.WorkDir,
"credential_env_var": r.llmAPIKeyEnv,
"credential_present": credentialPresent,
} }
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644) return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644)
} }

View File

@@ -25,7 +25,8 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath) t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath)
wrapper := writeAuditaHelperWrapper(t) wrapper := writeAuditaHelperWrapper(t)
llmConcurrency := 1 totalLLMConcurrency := 3
proposalLLMConcurrency := 2
validationLLMConcurrency := 2 validationLLMConcurrency := 2
runner, err := NewSubprocessRunner(SubprocessRunnerConfig{ runner, err := NewSubprocessRunner(SubprocessRunnerConfig{
Binary: wrapper, Binary: wrapper,
@@ -34,7 +35,12 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
Modules: []string{"glossary", "homophones", "glossary"}, Modules: []string{"glossary", "homophones", "glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, TranscriptDescription: "Campaign Session 42",
ConfigPath: "/etc/audita/config.yml",
OutputSchema: "audita-v1",
WorkDirRetention: "auto",
TotalLLMConcurrency: &totalLLMConcurrency,
ProposalLLMConcurrency: &proposalLLMConcurrency,
ValidationModel: "openrouter/google/gemma-4-31b-it", ValidationModel: "openrouter/google/gemma-4-31b-it",
ValidationLLMConcurrency: &validationLLMConcurrency, ValidationLLMConcurrency: &validationLLMConcurrency,
Report: true, Report: true,
@@ -93,11 +99,17 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
"process", req.MergedTranscriptPath, "process", req.MergedTranscriptPath,
"--glossary", req.GlossaryPath, "--glossary", req.GlossaryPath,
"--output", req.OutputProcessedPath, "--output", req.OutputProcessedPath,
"--modules", "glossary,homophones,glossary",
"--base-url", "https://openrouter.ai/api/v1", "--base-url", "https://openrouter.ai/api/v1",
"--model", "openrouter/google/gemma-4-31b-it", "--model", "openrouter/google/gemma-4-31b-it",
"--work-dir", req.WorkDir, "--work-dir", req.WorkDir,
"--modules", "glossary,homophones,glossary",
"--report-json", req.ReportPath, "--report-json", req.ReportPath,
"--transcript-description", "Campaign Session 42",
"--config", "/etc/audita/config.yml",
"--output-schema", "audita-v1",
"--work-dir-retention", "auto",
"--total-llm-concurrency", "3",
"--proposal-llm-concurrency", "2",
"--validation-model", "openrouter/google/gemma-4-31b-it", "--validation-model", "openrouter/google/gemma-4-31b-it",
"--validation-llm-concurrency", "2", "--validation-llm-concurrency", "2",
} }
@@ -107,8 +119,8 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
if rec.Env["AUDITA_LLM_API_KEY"] != "super-secret" { if rec.Env["AUDITA_LLM_API_KEY"] != "super-secret" {
t.Fatalf("AUDITA_LLM_API_KEY = %q, want propagated secret", rec.Env["AUDITA_LLM_API_KEY"]) t.Fatalf("AUDITA_LLM_API_KEY = %q, want propagated secret", rec.Env["AUDITA_LLM_API_KEY"])
} }
if rec.Env["AUDITA_LLM_CONCURRENCY"] != "1" { if rec.Env["AUDITA_LLM_CONCURRENCY"] != "" {
t.Fatalf("AUDITA_LLM_CONCURRENCY = %q, want 1", rec.Env["AUDITA_LLM_CONCURRENCY"]) t.Fatalf("AUDITA_LLM_CONCURRENCY = %q, want empty/omitted", rec.Env["AUDITA_LLM_CONCURRENCY"])
} }
cfgData, err := os.ReadFile(req.GeneratedConfigPath) cfgData, err := os.ReadFile(req.GeneratedConfigPath)
@@ -124,16 +136,14 @@ func TestSubprocessRunnerMissingConfiguredCredentialFails(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh") t.Skip("helper wrapper script uses /bin/sh")
} }
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "MISSING_AUDITA_KEY", LLMAPIKeyEnv: "MISSING_AUDITA_KEY",
Modules: []string{"glossary"}, Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, Report: false,
Report: false,
}) })
req := auditaReqForTest(t, false) req := auditaReqForTest(t, false)
@@ -155,16 +165,14 @@ func TestSubprocessRunnerUnconfiguredCredentialEnvOmitsCredential(t *testing.T)
recordPath := filepath.Join(t.TempDir(), "record.json") recordPath := filepath.Join(t.TempDir(), "record.json")
t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath) t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath)
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "", LLMAPIKeyEnv: "",
Modules: []string{"glossary"}, Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, Report: false,
Report: false,
}) })
req := auditaReqForTest(t, false) req := auditaReqForTest(t, false)
@@ -211,6 +219,35 @@ func TestSubprocessRunnerInheritsParentEnvironment(t *testing.T) {
} }
} }
func TestSubprocessRunnerOmitsModulesFlagWhenNotConfigured(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "success")
recordPath := filepath.Join(t.TempDir(), "record.json")
t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath)
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "",
BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it",
Report: false,
})
req := auditaReqForTest(t, false)
if _, err := runner.Run(context.Background(), req); err != nil {
t.Fatalf("Run() error = %v", err)
}
rec := readAuditaHelperRecord(t, recordPath)
for i := 0; i < len(rec.Args); i++ {
if rec.Args[i] == "--modules" {
t.Fatalf("args contained --modules unexpectedly: %#v", rec.Args)
}
}
}
func TestSubprocessRunnerSubprocessFailure(t *testing.T) { func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh") t.Skip("helper wrapper script uses /bin/sh")
@@ -220,16 +257,14 @@ func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
Modules: []string{"glossary"}, Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, Report: true,
Report: true,
}) })
req := auditaReqForTest(t, true) req := auditaReqForTest(t, true)
_, err := runner.Run(context.Background(), req) _, err := runner.Run(context.Background(), req)
@@ -256,16 +291,14 @@ func TestSubprocessRunnerSubprocessFailureAddsStderrDescriptorHint(t *testing.T)
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
Modules: []string{"glossary"}, Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, Report: true,
Report: true,
}) })
req := auditaReqForTest(t, true) req := auditaReqForTest(t, true)
_, err := runner.Run(context.Background(), req) _, err := runner.Run(context.Background(), req)
@@ -286,16 +319,14 @@ func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
Modules: []string{"glossary"}, Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, Report: false,
Report: false,
}) })
req := auditaReqForTest(t, false) req := auditaReqForTest(t, false)
_, err := runner.Run(context.Background(), req) _, err := runner.Run(context.Background(), req)
@@ -316,16 +347,14 @@ func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) {
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
Modules: []string{"glossary"}, Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, Report: false,
Report: false,
}) })
req := auditaReqForTest(t, false) req := auditaReqForTest(t, false)
_, err := runner.Run(context.Background(), req) _, err := runner.Run(context.Background(), req)
@@ -346,16 +375,14 @@ func TestSubprocessRunnerSegmentsMissingFails(t *testing.T) {
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
Modules: []string{"glossary"}, Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, Report: false,
Report: false,
}) })
req := auditaReqForTest(t, false) req := auditaReqForTest(t, false)
_, err := runner.Run(context.Background(), req) _, err := runner.Run(context.Background(), req)
@@ -376,16 +403,14 @@ func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) {
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE", LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
Modules: []string{"glossary"}, Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, Report: true,
Report: true,
}) })
req := auditaReqForTest(t, true) req := auditaReqForTest(t, true)
_, err := runner.Run(context.Background(), req) _, err := runner.Run(context.Background(), req)
@@ -398,11 +423,11 @@ func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) {
} }
func TestSubprocessRunnerConstructorValidation(t *testing.T) { func TestSubprocessRunnerConstructorValidation(t *testing.T) {
_, err := NewSubprocessRunnerFromConfigValues("", "3h", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", nil, "", nil, true) _, err := NewSubprocessRunnerFromConfigValues("", "3h", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", "", "", "", "", nil, nil, "", nil, true)
if err == nil { if err == nil {
t.Fatal("expected binary validation error") t.Fatal("expected binary validation error")
} }
_, err = NewSubprocessRunnerFromConfigValues("audita", "bad", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", nil, "", nil, true) _, err = NewSubprocessRunnerFromConfigValues("audita", "bad", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", "", "", "", "", nil, nil, "", nil, true)
if err == nil { if err == nil {
t.Fatal("expected timeout parse error") t.Fatal("expected timeout parse error")
} }

View File

@@ -230,7 +230,7 @@ func buildDefaultAuditaRunner(cfg *config.Config) (audita.Runner, error) {
} }
a := cfg.Pipeline.Audita a := cfg.Pipeline.Audita
if strings.TrimSpace(a.Binary) == "" || strings.TrimSpace(a.Timeout) == "" || len(a.Modules) == 0 || strings.TrimSpace(a.BaseURL) == "" || strings.TrimSpace(a.Model) == "" { if strings.TrimSpace(a.Binary) == "" || strings.TrimSpace(a.Timeout) == "" || strings.TrimSpace(a.BaseURL) == "" || strings.TrimSpace(a.Model) == "" {
// Compatibility fallback for tests or internal call paths that bypass config validation/defaults. // Compatibility fallback for tests or internal call paths that bypass config validation/defaults.
return &audita.NoopRunner{}, nil return &audita.NoopRunner{}, nil
} }
@@ -247,7 +247,12 @@ func buildDefaultAuditaRunner(cfg *config.Config) (audita.Runner, error) {
append([]string(nil), a.Modules...), append([]string(nil), a.Modules...),
a.BaseURL, a.BaseURL,
a.Model, a.Model,
a.LLMConcurrency, a.TranscriptDescription,
a.ConfigPath,
a.OutputSchema,
a.WorkDirRetention,
a.TotalLLMConcurrency,
a.ProposalLLMConcurrency,
a.ValidationModel, a.ValidationModel,
a.ValidationLLMConcurrency, a.ValidationLLMConcurrency,
report, report,

View File

@@ -85,9 +85,14 @@ type AuditaConfig struct {
Modules []string `yaml:"modules"` Modules []string `yaml:"modules"`
BaseURL string `yaml:"base_url"` BaseURL string `yaml:"base_url"`
Model string `yaml:"model"` Model string `yaml:"model"`
LLMConcurrency *int `yaml:"llm_concurrency"` TotalLLMConcurrency *int `yaml:"total_llm_concurrency"`
ProposalLLMConcurrency *int `yaml:"proposal_llm_concurrency"`
ValidationModel string `yaml:"validation_model"` ValidationModel string `yaml:"validation_model"`
ValidationLLMConcurrency *int `yaml:"validation_llm_concurrency"` ValidationLLMConcurrency *int `yaml:"validation_llm_concurrency"`
TranscriptDescription string `yaml:"transcript_description"`
ConfigPath string `yaml:"config_path"`
OutputSchema string `yaml:"output_schema"`
WorkDirRetention string `yaml:"work_dir_retention"`
Report *bool `yaml:"report"` Report *bool `yaml:"report"`
} }

View File

@@ -143,29 +143,12 @@ func applyAuditaDefaults(cfg *AuditaConfig) {
if cfg.Timeout == "" { if cfg.Timeout == "" {
cfg.Timeout = "3h" cfg.Timeout = "3h"
} }
if cfg.Modules == nil {
cfg.Modules = []string{
"glossary",
"homophones",
"glossary",
"spoken_word",
"grammar",
"homophones",
"glossary",
}
}
if cfg.BaseURL == "" { if cfg.BaseURL == "" {
cfg.BaseURL = "https://openrouter.ai/api/v1" cfg.BaseURL = "https://openrouter.ai/api/v1"
} }
if cfg.Model == "" { if cfg.Model == "" {
cfg.Model = "openrouter/google/gemma-4-31b-it" cfg.Model = "openrouter/google/gemma-4-31b-it"
} }
if cfg.LLMConcurrency == nil {
cfg.LLMConcurrency = intPtr(1)
}
if cfg.ValidationLLMConcurrency == nil {
cfg.ValidationLLMConcurrency = intPtr(1)
}
if cfg.Report == nil { if cfg.Report == nil {
cfg.Report = boolPtr(true) cfg.Report = boolPtr(true)
} }

View File

@@ -453,7 +453,7 @@ inputs:
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.timeout must be a valid duration", wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.timeout must be a valid duration",
}, },
{ {
name: "empty audita modules fails", name: "empty audita modules is valid override",
pipelineYAML: `workspace: pipelineYAML: `workspace:
root: /tmp/narratio root: /tmp/narratio
whisperx: whisperx:
@@ -471,7 +471,6 @@ inputs:
autocorrect_file: ./autocorrect.yml autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml glossary_file: ./glossary.yml
`, `,
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.modules must include at least one module",
}, },
{ {
name: "empty audita module item fails", name: "empty audita module item fails",
@@ -541,7 +540,7 @@ inputs:
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.base_url must be a valid URL", wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.base_url must be a valid URL",
}, },
{ {
name: "invalid audita llm_concurrency fails", name: "legacy audita llm_concurrency field fails strict decode",
pipelineYAML: `workspace: pipelineYAML: `workspace:
root: /tmp/narratio root: /tmp/narratio
whisperx: whisperx:
@@ -550,7 +549,7 @@ seriatim:
binary: seriatim binary: seriatim
audita: audita:
binary: audita binary: audita
llm_concurrency: 0 llm_concurrency: 1
`, `,
sessionYAML: `session_id: 2026-05-03 sessionYAML: `session_id: 2026-05-03
inputs: inputs:
@@ -559,7 +558,49 @@ inputs:
autocorrect_file: ./autocorrect.yml autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml glossary_file: ./glossary.yml
`, `,
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.llm_concurrency must be > 0", wantLoadErr: "strict decode failed",
},
{
name: "invalid audita total_llm_concurrency fails",
pipelineYAML: `workspace:
root: /tmp/narratio
whisperx:
transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe
seriatim:
binary: seriatim
audita:
binary: audita
total_llm_concurrency: 0
`,
sessionYAML: `session_id: 2026-05-03
inputs:
audio_dir: ./audio
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`,
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.total_llm_concurrency must be > 0",
},
{
name: "invalid audita proposal_llm_concurrency fails",
pipelineYAML: `workspace:
root: /tmp/narratio
whisperx:
transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe
seriatim:
binary: seriatim
audita:
binary: audita
proposal_llm_concurrency: 0
`,
sessionYAML: `session_id: 2026-05-03
inputs:
audio_dir: ./audio
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`,
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.proposal_llm_concurrency must be > 0",
}, },
{ {
name: "invalid audita validation_llm_concurrency fails", name: "invalid audita validation_llm_concurrency fails",
@@ -582,6 +623,48 @@ inputs:
`, `,
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.validation_llm_concurrency must be > 0", wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.validation_llm_concurrency must be > 0",
}, },
{
name: "invalid audita output_schema fails",
pipelineYAML: `workspace:
root: /tmp/narratio
whisperx:
transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe
seriatim:
binary: seriatim
audita:
binary: audita
output_schema: bad
`,
sessionYAML: `session_id: 2026-05-03
inputs:
audio_dir: ./audio
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`,
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.output_schema must be one of: bare-segments, audita-v1",
},
{
name: "invalid audita work_dir_retention fails",
pipelineYAML: `workspace:
root: /tmp/narratio
whisperx:
transcribe_url: https://transcription.ai.rakestrawhome.com/transcribe
seriatim:
binary: seriatim
audita:
binary: audita
work_dir_retention: sometimes
`,
sessionYAML: `session_id: 2026-05-03
inputs:
audio_dir: ./audio
speakers_file: ./speakers.yml
autocorrect_file: ./autocorrect.yml
glossary_file: ./glossary.yml
`,
wantValidate: "pipeline config \"pipeline.yml\" invalid: pipeline.audita.work_dir_retention must be one of: always, auto, never",
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -645,8 +728,8 @@ inputs:
if cfg.Pipeline.Audita.LLMAPIKeyEnv != "" { if cfg.Pipeline.Audita.LLMAPIKeyEnv != "" {
t.Fatalf("audita.llm_api_key_env = %q, want empty by default", cfg.Pipeline.Audita.LLMAPIKeyEnv) t.Fatalf("audita.llm_api_key_env = %q, want empty by default", cfg.Pipeline.Audita.LLMAPIKeyEnv)
} }
if got := strings.Join(cfg.Pipeline.Audita.Modules, ","); got != "glossary,homophones,glossary,spoken_word,grammar,homophones,glossary" { if cfg.Pipeline.Audita.Modules != nil {
t.Fatalf("audita.modules = %q, want default sequence", got) t.Fatalf("audita.modules = %#v, want nil default (optional override)", cfg.Pipeline.Audita.Modules)
} }
if cfg.Pipeline.Audita.BaseURL != "https://openrouter.ai/api/v1" { if cfg.Pipeline.Audita.BaseURL != "https://openrouter.ai/api/v1" {
t.Fatalf("audita.base_url = %q, want %q", cfg.Pipeline.Audita.BaseURL, "https://openrouter.ai/api/v1") t.Fatalf("audita.base_url = %q, want %q", cfg.Pipeline.Audita.BaseURL, "https://openrouter.ai/api/v1")
@@ -654,14 +737,17 @@ inputs:
if cfg.Pipeline.Audita.Model != "openrouter/google/gemma-4-31b-it" { if cfg.Pipeline.Audita.Model != "openrouter/google/gemma-4-31b-it" {
t.Fatalf("audita.model = %q, want %q", cfg.Pipeline.Audita.Model, "openrouter/google/gemma-4-31b-it") t.Fatalf("audita.model = %q, want %q", cfg.Pipeline.Audita.Model, "openrouter/google/gemma-4-31b-it")
} }
if cfg.Pipeline.Audita.LLMConcurrency == nil || *cfg.Pipeline.Audita.LLMConcurrency != 1 {
t.Fatalf("audita.llm_concurrency = %v, want 1", cfg.Pipeline.Audita.LLMConcurrency)
}
if cfg.Pipeline.Audita.ValidationModel != "" { if cfg.Pipeline.Audita.ValidationModel != "" {
t.Fatalf("audita.validation_model = %q, want empty default", cfg.Pipeline.Audita.ValidationModel) t.Fatalf("audita.validation_model = %q, want empty default", cfg.Pipeline.Audita.ValidationModel)
} }
if cfg.Pipeline.Audita.ValidationLLMConcurrency == nil || *cfg.Pipeline.Audita.ValidationLLMConcurrency != 1 { if cfg.Pipeline.Audita.TotalLLMConcurrency != nil {
t.Fatalf("audita.validation_llm_concurrency = %v, want 1", cfg.Pipeline.Audita.ValidationLLMConcurrency) t.Fatalf("audita.total_llm_concurrency = %v, want nil default", cfg.Pipeline.Audita.TotalLLMConcurrency)
}
if cfg.Pipeline.Audita.ProposalLLMConcurrency != nil {
t.Fatalf("audita.proposal_llm_concurrency = %v, want nil default", cfg.Pipeline.Audita.ProposalLLMConcurrency)
}
if cfg.Pipeline.Audita.ValidationLLMConcurrency != nil {
t.Fatalf("audita.validation_llm_concurrency = %v, want nil default", cfg.Pipeline.Audita.ValidationLLMConcurrency)
} }
if cfg.Pipeline.Audita.Report == nil || *cfg.Pipeline.Audita.Report != true { if cfg.Pipeline.Audita.Report == nil || *cfg.Pipeline.Audita.Report != true {
t.Fatalf("audita.report = %v, want true", cfg.Pipeline.Audita.Report) t.Fatalf("audita.report = %v, want true", cfg.Pipeline.Audita.Report)
@@ -725,7 +811,8 @@ func TestValidateMissingAudioSource(t *testing.T) {
Modules: []string{"glossary", "homophones"}, Modules: []string{"glossary", "homophones"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: intPtr(1), TotalLLMConcurrency: intPtr(1),
ProposalLLMConcurrency: intPtr(1),
ValidationModel: "", ValidationModel: "",
ValidationLLMConcurrency: intPtr(1), ValidationLLMConcurrency: intPtr(1),
Report: boolPtr(true), Report: boolPtr(true),

View File

@@ -200,15 +200,12 @@ func validateAudita(cfg AuditaConfig) error {
if err := validateDuration("pipeline.audita.timeout", cfg.Timeout); err != nil { if err := validateDuration("pipeline.audita.timeout", cfg.Timeout); err != nil {
return err return err
} }
if len(cfg.Modules) == 0 { for i, m := range cfg.Modules {
return fmt.Errorf("pipeline.audita.modules must include at least one module") module := strings.TrimSpace(m)
} if module == "" {
for i, mod := range cfg.Modules {
m := strings.TrimSpace(mod)
if m == "" {
return fmt.Errorf("pipeline.audita.modules[%d] must be non-empty", i) return fmt.Errorf("pipeline.audita.modules[%d] must be non-empty", i)
} }
switch m { switch module {
case "glossary", "homophones", "spoken_word", "grammar": case "glossary", "homophones", "spoken_word", "grammar":
default: default:
return fmt.Errorf("pipeline.audita.modules[%d] must be one of: glossary, homophones, spoken_word, grammar", i) return fmt.Errorf("pipeline.audita.modules[%d] must be one of: glossary, homophones, spoken_word, grammar", i)
@@ -226,18 +223,31 @@ func validateAudita(cfg AuditaConfig) error {
if strings.TrimSpace(cfg.Model) == "" { if strings.TrimSpace(cfg.Model) == "" {
return fmt.Errorf("pipeline.audita.model is required") return fmt.Errorf("pipeline.audita.model is required")
} }
if cfg.LLMConcurrency == nil { if cfg.TotalLLMConcurrency != nil && *cfg.TotalLLMConcurrency <= 0 {
return fmt.Errorf("pipeline.audita.llm_concurrency must be set (defaults should populate this)") return fmt.Errorf("pipeline.audita.total_llm_concurrency must be > 0")
} }
if *cfg.LLMConcurrency <= 0 { if cfg.ProposalLLMConcurrency != nil && *cfg.ProposalLLMConcurrency <= 0 {
return fmt.Errorf("pipeline.audita.llm_concurrency must be > 0") return fmt.Errorf("pipeline.audita.proposal_llm_concurrency must be > 0")
} }
if cfg.ValidationLLMConcurrency == nil { if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 {
return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be set (defaults should populate this)")
}
if *cfg.ValidationLLMConcurrency <= 0 {
return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be > 0") return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be > 0")
} }
if strings.TrimSpace(cfg.TranscriptDescription) == "" && cfg.TranscriptDescription != "" {
return fmt.Errorf("pipeline.audita.transcript_description must be non-empty when provided")
}
if strings.TrimSpace(cfg.ConfigPath) == "" && cfg.ConfigPath != "" {
return fmt.Errorf("pipeline.audita.config_path must be non-empty when provided")
}
switch strings.TrimSpace(cfg.OutputSchema) {
case "", "bare-segments", "audita-v1":
default:
return fmt.Errorf("pipeline.audita.output_schema must be one of: bare-segments, audita-v1")
}
switch strings.TrimSpace(cfg.WorkDirRetention) {
case "", "always", "auto", "never":
default:
return fmt.Errorf("pipeline.audita.work_dir_retention must be one of: always, auto, never")
}
return nil return nil
} }

View File

@@ -90,6 +90,12 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
Modules: append([]string(nil), env.Config.Pipeline.Audita.Modules...), Modules: append([]string(nil), env.Config.Pipeline.Audita.Modules...),
BaseURL: env.Config.Pipeline.Audita.BaseURL, BaseURL: env.Config.Pipeline.Audita.BaseURL,
Model: env.Config.Pipeline.Audita.Model, Model: env.Config.Pipeline.Audita.Model,
TranscriptDescription: env.Config.Pipeline.Audita.TranscriptDescription,
ConfigPath: env.Config.Pipeline.Audita.ConfigPath,
OutputSchema: env.Config.Pipeline.Audita.OutputSchema,
WorkDirRetention: env.Config.Pipeline.Audita.WorkDirRetention,
TotalLLMConcurrency: env.Config.Pipeline.Audita.TotalLLMConcurrency,
ProposalLLMConcurrency: env.Config.Pipeline.Audita.ProposalLLMConcurrency,
ValidationModel: env.Config.Pipeline.Audita.ValidationModel, ValidationModel: env.Config.Pipeline.Audita.ValidationModel,
ValidationLLMConcurrency: env.Config.Pipeline.Audita.ValidationLLMConcurrency, ValidationLLMConcurrency: env.Config.Pipeline.Audita.ValidationLLMConcurrency,
StdoutLogPath: stdoutPath, StdoutLogPath: stdoutPath,
@@ -141,44 +147,52 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
if env.Config.Pipeline.Audita.ValidationLLMConcurrency != nil { if env.Config.Pipeline.Audita.ValidationLLMConcurrency != nil {
validationConcurrency = *env.Config.Pipeline.Audita.ValidationLLMConcurrency validationConcurrency = *env.Config.Pipeline.Audita.ValidationLLMConcurrency
} }
var llmConcurrency any var totalLLMConcurrency any
if env.Config.Pipeline.Audita.LLMConcurrency != nil { if env.Config.Pipeline.Audita.TotalLLMConcurrency != nil {
llmConcurrency = *env.Config.Pipeline.Audita.LLMConcurrency totalLLMConcurrency = *env.Config.Pipeline.Audita.TotalLLMConcurrency
}
var proposalLLMConcurrency any
if env.Config.Pipeline.Audita.ProposalLLMConcurrency != nil {
proposalLLMConcurrency = *env.Config.Pipeline.Audita.ProposalLLMConcurrency
} }
meta := map[string]any{ meta := map[string]any{
"stage": "polish", "stage": "polish",
"merged_transcript_path": mergedPath, "merged_transcript_path": mergedPath,
"merged_transcript_source": source, "merged_transcript_source": source,
"glossary_path": glossaryPath, "glossary_path": glossaryPath,
"output_path": finalProcessedPath, "output_path": finalProcessedPath,
"report_path": finalReportPath, "report_path": finalReportPath,
"audita_work_dir": workDir, "audita_work_dir": workDir,
"report_enabled": reportEnabled, "report_enabled": reportEnabled,
"modules": append([]string(nil), req.Modules...), "modules": append([]string(nil), req.Modules...),
"base_url": req.BaseURL, "base_url": req.BaseURL,
"model": req.Model, "model": req.Model,
"validation_model": req.ValidationModel, "transcript_description": req.TranscriptDescription,
"llm_concurrency": llmConcurrency, "config_path": req.ConfigPath,
"validation_llm_concurrency": validationConcurrency, "output_schema": req.OutputSchema,
"llm_api_key_env": env.Config.Pipeline.Audita.LLMAPIKeyEnv, "work_dir_retention": req.WorkDirRetention,
"timeout": env.Config.Pipeline.Audita.Timeout, "validation_model": req.ValidationModel,
"binary": env.Config.Pipeline.Audita.Binary, "total_llm_concurrency": totalLLMConcurrency,
"generated_config_path": generatedConfigPath, "proposal_llm_concurrency": proposalLLMConcurrency,
"stdout_log_path": stdoutPath, "validation_llm_concurrency": validationConcurrency,
"stderr_log_path": stderrPath, "llm_api_key_env": env.Config.Pipeline.Audita.LLMAPIKeyEnv,
"adapter_duration_ms": res.Duration.Milliseconds(), "timeout": env.Config.Pipeline.Audita.Timeout,
"adapter_exit_code": res.ExitCode, "binary": env.Config.Pipeline.Audita.Binary,
"adapter_invoked_binary": res.InvokedBinary, "generated_config_path": generatedConfigPath,
"adapter_processed_output_path": res.ProcessedTranscriptPath, "stdout_log_path": stdoutPath,
"adapter_report_path": res.ReportPath, "stderr_log_path": stderrPath,
"adapter_generated_config_path": res.GeneratedConfigPath, "adapter_duration_ms": res.Duration.Milliseconds(),
"adapter_work_dir": res.WorkDir, "adapter_exit_code": res.ExitCode,
"adapter_stdout_log_path": res.StdoutLogPath, "adapter_invoked_binary": res.InvokedBinary,
"adapter_stderr_log_path": res.StderrLogPath, "adapter_processed_output_path": res.ProcessedTranscriptPath,
"credential_env_var": env.Config.Pipeline.Audita.LLMAPIKeyEnv, "adapter_report_path": res.ReportPath,
"credential_present": false, "adapter_generated_config_path": res.GeneratedConfigPath,
"primary_llm_concurrency_via_env": false, "adapter_work_dir": res.WorkDir,
"adapter_stdout_log_path": res.StdoutLogPath,
"adapter_stderr_log_path": res.StderrLogPath,
"credential_env_var": env.Config.Pipeline.Audita.LLMAPIKeyEnv,
"credential_present": false,
} }
if res.Metadata != nil { if res.Metadata != nil {
meta["adapter_metadata"] = res.Metadata meta["adapter_metadata"] = res.Metadata
@@ -188,9 +202,6 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
if value, ok := res.Metadata["credential_env_var"]; ok { if value, ok := res.Metadata["credential_env_var"]; ok {
meta["credential_env_var"] = value meta["credential_env_var"] = value
} }
if value, ok := res.Metadata["primary_llm_concurrency_via_env"]; ok {
meta["primary_llm_concurrency_via_env"] = value
}
} }
return &StageResult{ return &StageResult{

View File

@@ -60,6 +60,24 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
if req.ValidationModel != "openrouter/google/gemma-4-31b-it" { if req.ValidationModel != "openrouter/google/gemma-4-31b-it" {
t.Fatalf("validation model = %q", req.ValidationModel) t.Fatalf("validation model = %q", req.ValidationModel)
} }
if req.TranscriptDescription != "Campaign Session 42" {
t.Fatalf("transcript description = %q", req.TranscriptDescription)
}
if req.ConfigPath != "/etc/audita/config.yml" {
t.Fatalf("config path = %q", req.ConfigPath)
}
if req.OutputSchema != "audita-v1" {
t.Fatalf("output schema = %q", req.OutputSchema)
}
if req.WorkDirRetention != "auto" {
t.Fatalf("work dir retention = %q", req.WorkDirRetention)
}
if req.TotalLLMConcurrency == nil || *req.TotalLLMConcurrency != 3 {
t.Fatalf("total llm concurrency = %#v, want 3", req.TotalLLMConcurrency)
}
if req.ProposalLLMConcurrency == nil || *req.ProposalLLMConcurrency != 2 {
t.Fatalf("proposal llm concurrency = %#v, want 2", req.ProposalLLMConcurrency)
}
if req.ValidationLLMConcurrency == nil || *req.ValidationLLMConcurrency != 2 { if req.ValidationLLMConcurrency == nil || *req.ValidationLLMConcurrency != 2 {
t.Fatalf("validation llm concurrency = %#v, want 2", req.ValidationLLMConcurrency) t.Fatalf("validation llm concurrency = %#v, want 2", req.ValidationLLMConcurrency)
} }
@@ -89,6 +107,15 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
if result.Metadata["audita_work_dir"] != filepath.Join(paths.ArtifactsDir, "audita-work") { if result.Metadata["audita_work_dir"] != filepath.Join(paths.ArtifactsDir, "audita-work") {
t.Fatalf("metadata audita_work_dir = %#v", result.Metadata["audita_work_dir"]) t.Fatalf("metadata audita_work_dir = %#v", result.Metadata["audita_work_dir"])
} }
if result.Metadata["total_llm_concurrency"] != 3 {
t.Fatalf("metadata total_llm_concurrency = %#v, want 3", result.Metadata["total_llm_concurrency"])
}
if result.Metadata["proposal_llm_concurrency"] != 2 {
t.Fatalf("metadata proposal_llm_concurrency = %#v, want 2", result.Metadata["proposal_llm_concurrency"])
}
if result.Metadata["output_schema"] != "audita-v1" {
t.Fatalf("metadata output_schema = %#v, want audita-v1", result.Metadata["output_schema"])
}
} }
func TestPolishStageFallsBackToMergedTranscriptPath(t *testing.T) { func TestPolishStageFallsBackToMergedTranscriptPath(t *testing.T) {
@@ -221,7 +248,8 @@ func setupPolishEnv(t *testing.T) (*Env, *manifest.Manifest) {
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n") writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
report := true report := true
llmConcurrency := 1 totalLLMConcurrency := 3
proposalLLMConcurrency := 2
validationLLMConcurrency := 2 validationLLMConcurrency := 2
cfg := &config.Config{ cfg := &config.Config{
@@ -236,8 +264,13 @@ func setupPolishEnv(t *testing.T) (*Env, *manifest.Manifest) {
Modules: []string{"glossary", "homophones", "grammar"}, Modules: []string{"glossary", "homophones", "grammar"},
BaseURL: "https://openrouter.ai/api/v1", BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it", Model: "openrouter/google/gemma-4-31b-it",
TranscriptDescription: "Campaign Session 42",
ConfigPath: "/etc/audita/config.yml",
OutputSchema: "audita-v1",
WorkDirRetention: "auto",
ValidationModel: "openrouter/google/gemma-4-31b-it", ValidationModel: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency, TotalLLMConcurrency: &totalLLMConcurrency,
ProposalLLMConcurrency: &proposalLLMConcurrency,
ValidationLLMConcurrency: &validationLLMConcurrency, ValidationLLMConcurrency: &validationLLMConcurrency,
Report: &report, Report: &report,
}, },